Home | History | Annotate | Download | only in email
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email;
     18 
     19 import android.content.ContentUris;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.text.TextUtils;
     25 
     26 import com.android.emailcommon.Logging;
     27 import com.android.emailcommon.internet.MimeBodyPart;
     28 import com.android.emailcommon.internet.MimeHeader;
     29 import com.android.emailcommon.internet.MimeMessage;
     30 import com.android.emailcommon.internet.MimeMultipart;
     31 import com.android.emailcommon.internet.MimeUtility;
     32 import com.android.emailcommon.internet.TextBody;
     33 import com.android.emailcommon.mail.Address;
     34 import com.android.emailcommon.mail.Base64Body;
     35 import com.android.emailcommon.mail.Flag;
     36 import com.android.emailcommon.mail.Message;
     37 import com.android.emailcommon.mail.Message.RecipientType;
     38 import com.android.emailcommon.mail.MessagingException;
     39 import com.android.emailcommon.mail.Multipart;
     40 import com.android.emailcommon.mail.Part;
     41 import com.android.emailcommon.provider.EmailContent;
     42 import com.android.emailcommon.provider.EmailContent.Attachment;
     43 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     44 import com.android.emailcommon.provider.Mailbox;
     45 import com.android.emailcommon.utility.AttachmentUtilities;
     46 import com.android.mail.providers.UIProvider;
     47 import com.android.mail.utils.LogUtils;
     48 import com.google.common.annotations.VisibleForTesting;
     49 
     50 import org.apache.commons.io.IOUtils;
     51 
     52 import java.io.ByteArrayInputStream;
     53 import java.io.File;
     54 import java.io.FileNotFoundException;
     55 import java.io.FileOutputStream;
     56 import java.io.IOException;
     57 import java.io.InputStream;
     58 import java.util.ArrayList;
     59 import java.util.Date;
     60 import java.util.HashMap;
     61 
     62 public class LegacyConversions {
     63 
     64     /** DO NOT CHECK IN "TRUE" */
     65     private static final boolean DEBUG_ATTACHMENTS = false;
     66 
     67     /** Used for mapping folder names to type codes (e.g. inbox, drafts, trash) */
     68     private static final HashMap<String, Integer>
     69             sServerMailboxNames = new HashMap<String, Integer>();
     70 
     71     /**
     72      * Copy field-by-field from a "store" message to a "provider" message
     73      *
     74      * @param message      The message we've just downloaded (must be a MimeMessage)
     75      * @param localMessage The message we'd like to write into the DB
     76      * @return true if dirty (changes were made)
     77      */
     78     public static boolean updateMessageFields(final EmailContent.Message localMessage,
     79             final Message message, final long accountId, final long mailboxId)
     80             throws MessagingException {
     81 
     82         final Address[] from = message.getFrom();
     83         final Address[] to = message.getRecipients(Message.RecipientType.TO);
     84         final Address[] cc = message.getRecipients(Message.RecipientType.CC);
     85         final Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
     86         final Address[] replyTo = message.getReplyTo();
     87         final String subject = message.getSubject();
     88         final Date sentDate = message.getSentDate();
     89         final Date internalDate = message.getInternalDate();
     90 
     91         if (from != null && from.length > 0) {
     92             localMessage.mDisplayName = from[0].toFriendly();
     93         }
     94         if (sentDate != null) {
     95             localMessage.mTimeStamp = sentDate.getTime();
     96         } else if (internalDate != null) {
     97             LogUtils.w(Logging.LOG_TAG, "No sentDate, falling back to internalDate");
     98             localMessage.mTimeStamp = internalDate.getTime();
     99         }
    100         if (subject != null) {
    101             localMessage.mSubject = subject;
    102         }
    103         localMessage.mFlagRead = message.isSet(Flag.SEEN);
    104         if (message.isSet(Flag.ANSWERED)) {
    105             localMessage.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
    106         }
    107 
    108         // Keep the message in the "unloaded" state until it has (at least) a display name.
    109         // This prevents early flickering of empty messages in POP download.
    110         if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
    111             if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
    112                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
    113             } else {
    114                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
    115             }
    116         }
    117         localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);
    118 //        public boolean mFlagAttachment = false;
    119 //        public int mFlags = 0;
    120 
    121         localMessage.mServerId = message.getUid();
    122         if (internalDate != null) {
    123             localMessage.mServerTimeStamp = internalDate.getTime();
    124         }
    125 //        public String mClientId;
    126 
    127         // Only replace the local message-id if a new one was found.  This is seen in some ISP's
    128         // which may deliver messages w/o a message-id header.
    129         final String messageId = message.getMessageId();
    130         if (messageId != null) {
    131             localMessage.mMessageId = messageId;
    132         }
    133 
    134 //        public long mBodyKey;
    135         localMessage.mMailboxKey = mailboxId;
    136         localMessage.mAccountKey = accountId;
    137 
    138         if (from != null && from.length > 0) {
    139             localMessage.mFrom = Address.toString(from);
    140         }
    141 
    142         localMessage.mTo = Address.toString(to);
    143         localMessage.mCc = Address.toString(cc);
    144         localMessage.mBcc = Address.toString(bcc);
    145         localMessage.mReplyTo = Address.toString(replyTo);
    146 
    147 //        public String mText;
    148 //        public String mHtml;
    149 //        public String mTextReply;
    150 //        public String mHtmlReply;
    151 
    152 //        // Can be used while building messages, but is NOT saved by the Provider
    153 //        transient public ArrayList<Attachment> mAttachments = null;
    154 
    155         return true;
    156     }
    157 
    158     /**
    159      * Copy attachments from MimeMessage to provider Message.
    160      *
    161      * @param context      a context for file operations
    162      * @param localMessage the attachments will be built against this message
    163      * @param attachments  the attachments to add
    164      */
    165     public static void updateAttachments(final Context context,
    166             final EmailContent.Message localMessage, final ArrayList<Part> attachments)
    167             throws MessagingException, IOException {
    168         localMessage.mAttachments = null;
    169         for (Part attachmentPart : attachments) {
    170             addOneAttachment(context, localMessage, attachmentPart);
    171         }
    172     }
    173 
    174     public static void updateInlineAttachments(final Context context,
    175             final EmailContent.Message localMessage, final ArrayList<Part> inlineAttachments)
    176             throws MessagingException, IOException {
    177         for (final Part inlinePart : inlineAttachments) {
    178             final String disposition = MimeUtility.getHeaderParameter(
    179                     MimeUtility.unfoldAndDecode(inlinePart.getDisposition()), null);
    180             if (!TextUtils.isEmpty(disposition)) {
    181                 // Treat inline parts as attachments
    182                 addOneAttachment(context, localMessage, inlinePart);
    183             }
    184         }
    185     }
    186 
    187     /**
    188      * Convert a MIME Part object into an Attachment object. Separated for unit testing.
    189      *
    190      * @param part MIME part object to convert
    191      * @return Populated Account object
    192      * @throws MessagingException
    193      */
    194     @VisibleForTesting
    195     protected static Attachment mimePartToAttachment(final Part part) throws MessagingException {
    196         // Transfer fields from mime format to provider format
    197         final String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
    198 
    199         String name = MimeUtility.getHeaderParameter(contentType, "name");
    200         if (TextUtils.isEmpty(name)) {
    201             final String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
    202             name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
    203         }
    204 
    205         // Incoming attachment: Try to pull size from disposition (if not downloaded yet)
    206         long size = 0;
    207         final String disposition = part.getDisposition();
    208         if (!TextUtils.isEmpty(disposition)) {
    209             String s = MimeUtility.getHeaderParameter(disposition, "size");
    210             if (!TextUtils.isEmpty(s)) {
    211                 try {
    212                     size = Long.parseLong(s);
    213                 } catch (final NumberFormatException e) {
    214                     LogUtils.d(LogUtils.TAG, e, "Could not decode size \"%s\" from attachment part",
    215                             size);
    216                 }
    217             }
    218         }
    219 
    220         // Get partId for unloaded IMAP attachments (if any)
    221         // This is only provided (and used) when we have structure but not the actual attachment
    222         final String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    223         final String partId = partIds != null ? partIds[0] : null;
    224 
    225         final Attachment localAttachment = new Attachment();
    226 
    227         // Run the mime type through inferMimeType in case we have something generic and can do
    228         // better using the filename extension
    229         localAttachment.mMimeType = AttachmentUtilities.inferMimeType(name, part.getMimeType());
    230         localAttachment.mFileName = name;
    231         localAttachment.mSize = size;
    232         localAttachment.mContentId = part.getContentId();
    233         localAttachment.setContentUri(null); // Will be rewritten by saveAttachmentBody
    234         localAttachment.mLocation = partId;
    235         localAttachment.mEncoding = "B"; // TODO - convert other known encodings
    236 
    237         return localAttachment;
    238     }
    239 
    240     /**
    241      * Add a single attachment part to the message
    242      *
    243      * This will skip adding attachments if they are already found in the attachments table.
    244      * The heuristic for this will fail (false-positive) if two identical attachments are
    245      * included in a single POP3 message.
    246      * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments
    247      * position within the list of multipart/mixed elements.  This would make every POP3 attachment
    248      * unique, and might also simplify the code (since we could just look at the positions, and
    249      * ignore the filename, etc.)
    250      *
    251      * TODO: Take a closer look at encoding and deal with it if necessary.
    252      *
    253      * @param context      a context for file operations
    254      * @param localMessage the attachments will be built against this message
    255      * @param part         a single attachment part from POP or IMAP
    256      */
    257     public static void addOneAttachment(final Context context,
    258             final EmailContent.Message localMessage, final Part part)
    259             throws MessagingException, IOException {
    260         final Attachment localAttachment = mimePartToAttachment(part);
    261         localAttachment.mMessageKey = localMessage.mId;
    262         localAttachment.mAccountKey = localMessage.mAccountKey;
    263 
    264         if (DEBUG_ATTACHMENTS) {
    265             LogUtils.d(Logging.LOG_TAG, "Add attachment " + localAttachment);
    266         }
    267 
    268         // To prevent duplication - do we already have a matching attachment?
    269         // The fields we'll check for equality are:
    270         //  mFileName, mMimeType, mContentId, mMessageKey, mLocation
    271         // NOTE:  This will false-positive if you attach the exact same file, twice, to a POP3
    272         // message.  We can live with that - you'll get one of the copies.
    273         final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    274         final Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
    275                 null, null, null);
    276         boolean attachmentFoundInDb = false;
    277         try {
    278             while (cursor.moveToNext()) {
    279                 final Attachment dbAttachment = new Attachment();
    280                 dbAttachment.restore(cursor);
    281                 // We test each of the fields here (instead of in SQL) because they may be
    282                 // null, or may be strings.
    283                 if (!TextUtils.equals(dbAttachment.mFileName, localAttachment.mFileName) ||
    284                         !TextUtils.equals(dbAttachment.mMimeType, localAttachment.mMimeType) ||
    285                         !TextUtils.equals(dbAttachment.mContentId, localAttachment.mContentId) ||
    286                         !TextUtils.equals(dbAttachment.mLocation, localAttachment.mLocation)) {
    287                     continue;
    288                 }
    289                 // We found a match, so use the existing attachment id, and stop looking/looping
    290                 attachmentFoundInDb = true;
    291                 localAttachment.mId = dbAttachment.mId;
    292                 if (DEBUG_ATTACHMENTS) {
    293                     LogUtils.d(Logging.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
    294                 }
    295                 break;
    296             }
    297         } finally {
    298             cursor.close();
    299         }
    300 
    301         // Save the attachment (so far) in order to obtain an id
    302         if (!attachmentFoundInDb) {
    303             localAttachment.save(context);
    304         }
    305 
    306         // If an attachment body was actually provided, we need to write the file now
    307         saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
    308 
    309         if (localMessage.mAttachments == null) {
    310             localMessage.mAttachments = new ArrayList<Attachment>();
    311         }
    312         localMessage.mAttachments.add(localAttachment);
    313         localMessage.mFlagAttachment = true;
    314     }
    315 
    316     /**
    317      * Save the body part of a single attachment, to a file in the attachments directory.
    318      */
    319     public static void saveAttachmentBody(final Context context, final Part part,
    320             final Attachment localAttachment, long accountId)
    321             throws MessagingException, IOException {
    322         if (part.getBody() != null) {
    323             final long attachmentId = localAttachment.mId;
    324 
    325             final File saveIn = AttachmentUtilities.getAttachmentDirectory(context, accountId);
    326 
    327             if (!saveIn.isDirectory() && !saveIn.mkdirs()) {
    328                 throw new IOException("Could not create attachment directory");
    329             }
    330             final File saveAs = AttachmentUtilities.getAttachmentFilename(context, accountId,
    331                     attachmentId);
    332 
    333             InputStream in = null;
    334             FileOutputStream out = null;
    335             final long copySize;
    336             try {
    337                 in = part.getBody().getInputStream();
    338                 out = new FileOutputStream(saveAs);
    339                 copySize = IOUtils.copyLarge(in, out);
    340             } finally {
    341                 if (in != null) {
    342                     in.close();
    343                 }
    344                 if (out != null) {
    345                     out.close();
    346                 }
    347             }
    348 
    349             // update the attachment with the extra information we now know
    350             final String contentUriString = AttachmentUtilities.getAttachmentUri(
    351                     accountId, attachmentId).toString();
    352 
    353             localAttachment.mSize = copySize;
    354             localAttachment.setContentUri(contentUriString);
    355 
    356             // update the attachment in the database as well
    357             final ContentValues cv = new ContentValues(3);
    358             cv.put(AttachmentColumns.SIZE, copySize);
    359             cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
    360             cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
    361             final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
    362             context.getContentResolver().update(uri, cv, null, null);
    363         }
    364     }
    365 
    366     /**
    367      * Read a complete Provider message into a legacy message (for IMAP upload).  This
    368      * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch().
    369      */
    370     public static Message makeMessage(final Context context,
    371             final EmailContent.Message localMessage)
    372             throws MessagingException {
    373         final MimeMessage message = new MimeMessage();
    374 
    375         // LocalFolder.getMessages() equivalent:  Copy message fields
    376         message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject);
    377         final Address[] from = Address.fromHeader(localMessage.mFrom);
    378         if (from.length > 0) {
    379             message.setFrom(from[0]);
    380         }
    381         message.setSentDate(new Date(localMessage.mTimeStamp));
    382         message.setUid(localMessage.mServerId);
    383         message.setFlag(Flag.DELETED,
    384                 localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED);
    385         message.setFlag(Flag.SEEN, localMessage.mFlagRead);
    386         message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite);
    387 //      message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey);
    388         message.setRecipients(RecipientType.TO, Address.fromHeader(localMessage.mTo));
    389         message.setRecipients(RecipientType.CC, Address.fromHeader(localMessage.mCc));
    390         message.setRecipients(RecipientType.BCC, Address.fromHeader(localMessage.mBcc));
    391         message.setReplyTo(Address.fromHeader(localMessage.mReplyTo));
    392         message.setInternalDate(new Date(localMessage.mServerTimeStamp));
    393         message.setMessageId(localMessage.mMessageId);
    394 
    395         // LocalFolder.fetch() equivalent: build body parts
    396         message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
    397         final MimeMultipart mp = new MimeMultipart();
    398         mp.setSubType("mixed");
    399         message.setBody(mp);
    400 
    401         try {
    402             addTextBodyPart(mp, "text/html",
    403                     EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId));
    404         } catch (RuntimeException rte) {
    405             LogUtils.d(Logging.LOG_TAG, "Exception while reading html body " + rte.toString());
    406         }
    407 
    408         try {
    409             addTextBodyPart(mp, "text/plain",
    410                     EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId));
    411         } catch (RuntimeException rte) {
    412             LogUtils.d(Logging.LOG_TAG, "Exception while reading text body " + rte.toString());
    413         }
    414 
    415         // Attachments
    416         final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    417         final Cursor attachments =
    418                 context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
    419                         null, null, null);
    420 
    421         try {
    422             while (attachments != null && attachments.moveToNext()) {
    423                 final Attachment att = new Attachment();
    424                 att.restore(attachments);
    425                 try {
    426                     final InputStream content;
    427                     if (att.mContentBytes != null) {
    428                         // This is generally only the case for synthetic attachments, such as those
    429                         // generated by unit tests or calendar invites
    430                         content = new ByteArrayInputStream(att.mContentBytes);
    431                     } else {
    432                         String contentUriString = att.getCachedFileUri();
    433                         if (TextUtils.isEmpty(contentUriString)) {
    434                             contentUriString = att.getContentUri();
    435                         }
    436                         if (TextUtils.isEmpty(contentUriString)) {
    437                             content = null;
    438                         } else {
    439                             final Uri contentUri = Uri.parse(contentUriString);
    440                             content = context.getContentResolver().openInputStream(contentUri);
    441                         }
    442                     }
    443                     final String mimeType = att.mMimeType;
    444                     final Long contentSize = att.mSize;
    445                     final String contentId = att.mContentId;
    446                     final String filename = att.mFileName;
    447                     if (content != null) {
    448                         addAttachmentPart(mp, mimeType, contentSize, filename, contentId, content);
    449                     } else {
    450                         LogUtils.e(LogUtils.TAG, "Could not open attachment file for upsync");
    451                     }
    452                 } catch (final FileNotFoundException e) {
    453                     LogUtils.e(LogUtils.TAG, "File Not Found error on %s while upsyncing message",
    454                             att.getCachedFileUri());
    455                 }
    456             }
    457         } finally {
    458             if (attachments != null) {
    459                 attachments.close();
    460             }
    461         }
    462 
    463         return message;
    464     }
    465 
    466     /**
    467      * Helper method to add a body part for a given type of text, if found
    468      *
    469      * @param mp          The text body part will be added to this multipart
    470      * @param contentType The content-type of the text being added
    471      * @param partText    The text to add.  If null, nothing happens
    472      */
    473     private static void addTextBodyPart(final MimeMultipart mp, final String contentType,
    474             final String partText)
    475             throws MessagingException {
    476         if (partText == null) {
    477             return;
    478         }
    479         final TextBody body = new TextBody(partText);
    480         final MimeBodyPart bp = new MimeBodyPart(body, contentType);
    481         mp.addBodyPart(bp);
    482     }
    483 
    484     /**
    485      * Helper method to add an attachment part
    486      *
    487      * @param mp          Multipart message to append attachment part to
    488      * @param contentType Mime type
    489      * @param contentSize Attachment metadata: unencoded file size
    490      * @param filename    Attachment metadata: file name
    491      * @param contentId   as referenced from cid: uris in the message body (if applicable)
    492      * @param content     unencoded bytes
    493      */
    494     @VisibleForTesting
    495     protected static void addAttachmentPart(final Multipart mp, final String contentType,
    496             final Long contentSize, final String filename, final String contentId,
    497             final InputStream content) throws MessagingException {
    498         final Base64Body body = new Base64Body(content);
    499         final MimeBodyPart bp = new MimeBodyPart(body, contentType);
    500         bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
    501         bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "attachment;\n "
    502                 + (!TextUtils.isEmpty(filename) ? "filename=\"" + filename + "\";" : "")
    503                 + "size=" + contentSize);
    504         if (contentId != null) {
    505             bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
    506         }
    507         mp.addBodyPart(bp);
    508     }
    509 
    510     /**
    511      * Infer mailbox type from mailbox name.  Used by MessagingController (for live folder sync).
    512      *
    513      * Deprecation: this should be configured in the UI, in conjunction with RF6154 support
    514      */
    515     @Deprecated
    516     public static synchronized int inferMailboxTypeFromName(Context context, String mailboxName) {
    517         if (sServerMailboxNames.size() == 0) {
    518             // preload the hashmap, one time only
    519             sServerMailboxNames.put(
    520                     context.getString(R.string.mailbox_name_server_inbox),
    521                     Mailbox.TYPE_INBOX);
    522             sServerMailboxNames.put(
    523                     context.getString(R.string.mailbox_name_server_outbox),
    524                     Mailbox.TYPE_OUTBOX);
    525             sServerMailboxNames.put(
    526                     context.getString(R.string.mailbox_name_server_drafts),
    527                     Mailbox.TYPE_DRAFTS);
    528             sServerMailboxNames.put(
    529                     context.getString(R.string.mailbox_name_server_trash),
    530                     Mailbox.TYPE_TRASH);
    531             sServerMailboxNames.put(
    532                     context.getString(R.string.mailbox_name_server_sent),
    533                     Mailbox.TYPE_SENT);
    534             sServerMailboxNames.put(
    535                     context.getString(R.string.mailbox_name_server_junk),
    536                     Mailbox.TYPE_JUNK);
    537         }
    538         if (mailboxName == null || mailboxName.length() == 0) {
    539             return Mailbox.TYPE_MAIL;
    540         }
    541         Integer type = sServerMailboxNames.get(mailboxName);
    542         if (type != null) {
    543             return type;
    544         }
    545         return Mailbox.TYPE_MAIL;
    546     }
    547 }
    548