summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/LegacyConversions.java
blob: 3de9b68ccad7c21ebaf1702571754c533d0a9923 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;

import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeBodyPart;
import com.android.emailcommon.internet.MimeHeader;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.internet.MimeMultipart;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.internet.TextBody;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.mail.Base64Body;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.Message.RecipientType;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Multipart;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;

import org.apache.commons.io.IOUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

public class LegacyConversions {

    /** DO NOT CHECK IN "TRUE" */
    private static final boolean DEBUG_ATTACHMENTS = false;

    /** Used for mapping folder names to type codes (e.g. inbox, drafts, trash) */
    private static final HashMap<String, Integer>
            sServerMailboxNames = new HashMap<String, Integer>();

    /**
     * Copy field-by-field from a "store" message to a "provider" message
     *
     * @param message      The message we've just downloaded (must be a MimeMessage)
     * @param localMessage The message we'd like to write into the DB
     * @return true if dirty (changes were made)
     */
    public static boolean updateMessageFields(final EmailContent.Message localMessage,
            final Message message, final long accountId, final long mailboxId)
            throws MessagingException {

        final Address[] from = message.getFrom();
        final Address[] to = message.getRecipients(Message.RecipientType.TO);
        final Address[] cc = message.getRecipients(Message.RecipientType.CC);
        final Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
        final Address[] replyTo = message.getReplyTo();
        final String subject = message.getSubject();
        final Date sentDate = message.getSentDate();
        final Date internalDate = message.getInternalDate();

        if (from != null && from.length > 0) {
            localMessage.mDisplayName = from[0].toFriendly();
        }
        if (sentDate != null) {
            localMessage.mTimeStamp = sentDate.getTime();
        } else if (internalDate != null) {
            LogUtils.w(Logging.LOG_TAG, "No sentDate, falling back to internalDate");
            localMessage.mTimeStamp = internalDate.getTime();
        }
        if (subject != null) {
            localMessage.mSubject = subject;
        }
        localMessage.mFlagRead = message.isSet(Flag.SEEN);
        if (message.isSet(Flag.ANSWERED)) {
            localMessage.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
        }

        // Keep the message in the "unloaded" state until it has (at least) a display name.
        // This prevents early flickering of empty messages in POP download.
        if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
            if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
                localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
            } else {
                localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
            }
        }
        localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);
//        public boolean mFlagAttachment = false;
//        public int mFlags = 0;

        localMessage.mServerId = message.getUid();
        if (internalDate != null) {
            localMessage.mServerTimeStamp = internalDate.getTime();
        }
//        public String mClientId;

        // Only replace the local message-id if a new one was found.  This is seen in some ISP's
        // which may deliver messages w/o a message-id header.
        final String messageId = message.getMessageId();
        if (messageId != null) {
            localMessage.mMessageId = messageId;
        }

//        public long mBodyKey;
        localMessage.mMailboxKey = mailboxId;
        localMessage.mAccountKey = accountId;

        if (from != null && from.length > 0) {
            localMessage.mFrom = Address.toString(from);
        }

        localMessage.mTo = Address.toString(to);
        localMessage.mCc = Address.toString(cc);
        localMessage.mBcc = Address.toString(bcc);
        localMessage.mReplyTo = Address.toString(replyTo);

//        public String mText;
//        public String mHtml;
//        public String mTextReply;
//        public String mHtmlReply;

//        // Can be used while building messages, but is NOT saved by the Provider
//        transient public ArrayList<Attachment> mAttachments = null;

        return true;
    }

    /**
     * Copy attachments from MimeMessage to provider Message.
     *
     * @param context      a context for file operations
     * @param localMessage the attachments will be built against this message
     * @param attachments  the attachments to add
     */
    public static void updateAttachments(final Context context,
            final EmailContent.Message localMessage, final ArrayList<Part> attachments)
            throws MessagingException, IOException {
        localMessage.mAttachments = null;
        for (Part attachmentPart : attachments) {
            addOneAttachment(context, localMessage, attachmentPart);
        }
    }

    public static void updateInlineAttachments(final Context context,
            final EmailContent.Message localMessage, final ArrayList<Part> inlineAttachments)
            throws MessagingException, IOException {
        for (final Part inlinePart : inlineAttachments) {
            final String disposition = MimeUtility.getHeaderParameter(
                    MimeUtility.unfoldAndDecode(inlinePart.getDisposition()), null);
            if (!TextUtils.isEmpty(disposition)) {
                // Treat inline parts as attachments
                addOneAttachment(context, localMessage, inlinePart);
            }
        }
    }

    /**
     * Convert a MIME Part object into an Attachment object. Separated for unit testing.
     *
     * @param part MIME part object to convert
     * @return Populated Account object
     * @throws MessagingException
     */
    @VisibleForTesting
    protected static Attachment mimePartToAttachment(final Part part) throws MessagingException {
        // Transfer fields from mime format to provider format
        final String contentType = MimeUtility.unfoldAndDecode(part.getContentType());

        String name = MimeUtility.getHeaderParameter(contentType, "name");
        if (TextUtils.isEmpty(name)) {
            final String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
            name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
        }

        // Incoming attachment: Try to pull size from disposition (if not downloaded yet)
        long size = 0;
        final String disposition = part.getDisposition();
        if (!TextUtils.isEmpty(disposition)) {
            String s = MimeUtility.getHeaderParameter(disposition, "size");
            if (!TextUtils.isEmpty(s)) {
                try {
                    size = Long.parseLong(s);
                } catch (final NumberFormatException e) {
                    LogUtils.d(LogUtils.TAG, e, "Could not decode size \"%s\" from attachment part",
                            size);
                }
            }
        }

        // Get partId for unloaded IMAP attachments (if any)
        // This is only provided (and used) when we have structure but not the actual attachment
        final String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
        final String partId = partIds != null ? partIds[0] : null;

        final Attachment localAttachment = new Attachment();

        // Run the mime type through inferMimeType in case we have something generic and can do
        // better using the filename extension
        localAttachment.mMimeType = AttachmentUtilities.inferMimeType(name, part.getMimeType());
        localAttachment.mFileName = name;
        localAttachment.mSize = size;
        localAttachment.mContentId = part.getContentId();
        localAttachment.setContentUri(null); // Will be rewritten by saveAttachmentBody
        localAttachment.mLocation = partId;
        localAttachment.mEncoding = "B"; // TODO - convert other known encodings

        return localAttachment;
    }

    /**
     * Add a single attachment part to the message
     *
     * This will skip adding attachments if they are already found in the attachments table.
     * The heuristic for this will fail (false-positive) if two identical attachments are
     * included in a single POP3 message.
     * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments
     * position within the list of multipart/mixed elements.  This would make every POP3 attachment
     * unique, and might also simplify the code (since we could just look at the positions, and
     * ignore the filename, etc.)
     *
     * TODO: Take a closer look at encoding and deal with it if necessary.
     *
     * @param context      a context for file operations
     * @param localMessage the attachments will be built against this message
     * @param part         a single attachment part from POP or IMAP
     */
    public static void addOneAttachment(final Context context,
            final EmailContent.Message localMessage, final Part part)
            throws MessagingException, IOException {
        final Attachment localAttachment = mimePartToAttachment(part);
        localAttachment.mMessageKey = localMessage.mId;
        localAttachment.mAccountKey = localMessage.mAccountKey;

        if (DEBUG_ATTACHMENTS) {
            LogUtils.d(Logging.LOG_TAG, "Add attachment " + localAttachment);
        }

        // To prevent duplication - do we already have a matching attachment?
        // The fields we'll check for equality are:
        //  mFileName, mMimeType, mContentId, mMessageKey, mLocation
        // NOTE:  This will false-positive if you attach the exact same file, twice, to a POP3
        // message.  We can live with that - you'll get one of the copies.
        final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
        final Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
                null, null, null);
        boolean attachmentFoundInDb = false;
        try {
            while (cursor.moveToNext()) {
                final Attachment dbAttachment = new Attachment();
                dbAttachment.restore(cursor);
                // We test each of the fields here (instead of in SQL) because they may be
                // null, or may be strings.
                if (!TextUtils.equals(dbAttachment.mFileName, localAttachment.mFileName) ||
                        !TextUtils.equals(dbAttachment.mMimeType, localAttachment.mMimeType) ||
                        !TextUtils.equals(dbAttachment.mContentId, localAttachment.mContentId) ||
                        !TextUtils.equals(dbAttachment.mLocation, localAttachment.mLocation)) {
                    continue;
                }
                // We found a match, so use the existing attachment id, and stop looking/looping
                attachmentFoundInDb = true;
                localAttachment.mId = dbAttachment.mId;
                if (DEBUG_ATTACHMENTS) {
                    LogUtils.d(Logging.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
                }
                break;
            }
        } finally {
            cursor.close();
        }

        // Save the attachment (so far) in order to obtain an id
        if (!attachmentFoundInDb) {
            localAttachment.save(context);
        }

        // If an attachment body was actually provided, we need to write the file now
        saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);

        if (localMessage.mAttachments == null) {
            localMessage.mAttachments = new ArrayList<Attachment>();
        }
        localMessage.mAttachments.add(localAttachment);
        localMessage.mFlagAttachment = true;
    }

    /**
     * Save the body part of a single attachment, to a file in the attachments directory.
     */
    public static void saveAttachmentBody(final Context context, final Part part,
            final Attachment localAttachment, long accountId)
            throws MessagingException, IOException {
        if (part.getBody() != null) {
            final long attachmentId = localAttachment.mId;

            final File saveIn = AttachmentUtilities.getAttachmentDirectory(context, accountId);

            if (!saveIn.isDirectory() && !saveIn.mkdirs()) {
                throw new IOException("Could not create attachment directory");
            }
            final File saveAs = AttachmentUtilities.getAttachmentFilename(context, accountId,
                    attachmentId);

            InputStream in = null;
            FileOutputStream out = null;
            final long copySize;
            try {
                in = part.getBody().getInputStream();
                out = new FileOutputStream(saveAs);
                copySize = IOUtils.copyLarge(in, out);
            } finally {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            }

            // update the attachment with the extra information we now know
            final String contentUriString = AttachmentUtilities.getAttachmentUri(
                    accountId, attachmentId).toString();

            localAttachment.mSize = copySize;
            localAttachment.setContentUri(contentUriString);

            // update the attachment in the database as well
            final ContentValues cv = new ContentValues(3);
            cv.put(AttachmentColumns.SIZE, copySize);
            cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
            cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
            final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
            context.getContentResolver().update(uri, cv, null, null);
        }
    }

    /**
     * Read a complete Provider message into a legacy message (for IMAP upload).  This
     * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch().
     */
    public static Message makeMessage(final Context context,
            final EmailContent.Message localMessage)
            throws MessagingException {
        final MimeMessage message = new MimeMessage();

        // LocalFolder.getMessages() equivalent:  Copy message fields
        message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject);
        final Address[] from = Address.fromHeader(localMessage.mFrom);
        if (from.length > 0) {
            message.setFrom(from[0]);
        }
        message.setSentDate(new Date(localMessage.mTimeStamp));
        message.setUid(localMessage.mServerId);
        message.setFlag(Flag.DELETED,
                localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED);
        message.setFlag(Flag.SEEN, localMessage.mFlagRead);
        message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite);
//      message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey);
        message.setRecipients(RecipientType.TO, Address.fromHeader(localMessage.mTo));
        message.setRecipients(RecipientType.CC, Address.fromHeader(localMessage.mCc));
        message.setRecipients(RecipientType.BCC, Address.fromHeader(localMessage.mBcc));
        message.setReplyTo(Address.fromHeader(localMessage.mReplyTo));
        message.setInternalDate(new Date(localMessage.mServerTimeStamp));
        message.setMessageId(localMessage.mMessageId);

        // LocalFolder.fetch() equivalent: build body parts
        message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
        final MimeMultipart mp = new MimeMultipart();
        mp.setSubType("mixed");
        message.setBody(mp);

        try {
            addTextBodyPart(mp, "text/html",
                    EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId));
        } catch (RuntimeException rte) {
            LogUtils.d(Logging.LOG_TAG, "Exception while reading html body " + rte.toString());
        }

        try {
            addTextBodyPart(mp, "text/plain",
                    EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId));
        } catch (RuntimeException rte) {
            LogUtils.d(Logging.LOG_TAG, "Exception while reading text body " + rte.toString());
        }

        // Attachments
        final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
        final Cursor attachments =
                context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
                        null, null, null);

        try {
            while (attachments != null && attachments.moveToNext()) {
                final Attachment att = new Attachment();
                att.restore(attachments);
                try {
                    final InputStream content;
                    if (att.mContentBytes != null) {
                        // This is generally only the case for synthetic attachments, such as those
                        // generated by unit tests or calendar invites
                        content = new ByteArrayInputStream(att.mContentBytes);
                    } else {
                        String contentUriString = att.getCachedFileUri();
                        if (TextUtils.isEmpty(contentUriString)) {
                            contentUriString = att.getContentUri();
                        }
                        if (TextUtils.isEmpty(contentUriString)) {
                            content = null;
                        } else {
                            final Uri contentUri = Uri.parse(contentUriString);
                            content = context.getContentResolver().openInputStream(contentUri);
                        }
                    }
                    final String mimeType = att.mMimeType;
                    final Long contentSize = att.mSize;
                    final String contentId = att.mContentId;
                    final String filename = att.mFileName;
                    if (content != null) {
                        addAttachmentPart(mp, mimeType, contentSize, filename, contentId, content);
                    } else {
                        LogUtils.e(LogUtils.TAG, "Could not open attachment file for upsync");
                    }
                } catch (final FileNotFoundException e) {
                    LogUtils.e(LogUtils.TAG, "File Not Found error on %s while upsyncing message",
                            att.getCachedFileUri());
                }
            }
        } finally {
            if (attachments != null) {
                attachments.close();
            }
        }

        return message;
    }

    /**
     * Helper method to add a body part for a given type of text, if found
     *
     * @param mp          The text body part will be added to this multipart
     * @param contentType The content-type of the text being added
     * @param partText    The text to add.  If null, nothing happens
     */
    private static void addTextBodyPart(final MimeMultipart mp, final String contentType,
            final String partText)
            throws MessagingException {
        if (partText == null) {
            return;
        }
        final TextBody body = new TextBody(partText);
        final MimeBodyPart bp = new MimeBodyPart(body, contentType);
        mp.addBodyPart(bp);
    }

    /**
     * Helper method to add an attachment part
     *
     * @param mp          Multipart message to append attachment part to
     * @param contentType Mime type
     * @param contentSize Attachment metadata: unencoded file size
     * @param filename    Attachment metadata: file name
     * @param contentId   as referenced from cid: uris in the message body (if applicable)
     * @param content     unencoded bytes
     */
    @VisibleForTesting
    protected static void addAttachmentPart(final Multipart mp, final String contentType,
            final Long contentSize, final String filename, final String contentId,
            final InputStream content) throws MessagingException {
        final Base64Body body = new Base64Body(content);
        final MimeBodyPart bp = new MimeBodyPart(body, contentType);
        bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
        bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "attachment;\n "
                + (!TextUtils.isEmpty(filename) ? "filename=\"" + filename + "\";" : "")
                + "size=" + contentSize);
        if (contentId != null) {
            bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
        }
        mp.addBodyPart(bp);
    }

    /**
     * Infer mailbox type from mailbox name.  Used by MessagingController (for live folder sync).
     *
     * Deprecation: this should be configured in the UI, in conjunction with RF6154 support
     */
    @Deprecated
    public static synchronized int inferMailboxTypeFromName(Context context, String mailboxName) {
        if (sServerMailboxNames.size() == 0) {
            // preload the hashmap, one time only
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_inbox),
                    Mailbox.TYPE_INBOX);
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_outbox),
                    Mailbox.TYPE_OUTBOX);
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_drafts),
                    Mailbox.TYPE_DRAFTS);
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_trash),
                    Mailbox.TYPE_TRASH);
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_sent),
                    Mailbox.TYPE_SENT);
            sServerMailboxNames.put(
                    context.getString(R.string.mailbox_name_server_junk),
                    Mailbox.TYPE_JUNK);
        }
        if (mailboxName == null || mailboxName.length() == 0) {
            return Mailbox.TYPE_MAIL;
        }
        Integer type = sServerMailboxNames.get(mailboxName);
        if (type != null) {
            return type;
        }
        return Mailbox.TYPE_MAIL;
    }
}