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 com.android.email.mail.Address;
     20 import com.android.email.mail.Body;
     21 import com.android.email.mail.Flag;
     22 import com.android.email.mail.Folder;
     23 import com.android.email.mail.Message;
     24 import com.android.email.mail.MessagingException;
     25 import com.android.email.mail.Part;
     26 import com.android.email.mail.Message.RecipientType;
     27 import com.android.email.mail.internet.MimeBodyPart;
     28 import com.android.email.mail.internet.MimeHeader;
     29 import com.android.email.mail.internet.MimeMessage;
     30 import com.android.email.mail.internet.MimeMultipart;
     31 import com.android.email.mail.internet.MimeUtility;
     32 import com.android.email.mail.internet.TextBody;
     33 import com.android.email.mail.store.LocalStore;
     34 import com.android.email.provider.AttachmentProvider;
     35 import com.android.email.provider.EmailContent;
     36 import com.android.email.provider.EmailContent.Attachment;
     37 import com.android.email.provider.EmailContent.AttachmentColumns;
     38 import com.android.email.provider.EmailContent.Mailbox;
     39 
     40 import org.apache.commons.io.IOUtils;
     41 
     42 import android.content.ContentUris;
     43 import android.content.ContentValues;
     44 import android.content.Context;
     45 import android.database.Cursor;
     46 import android.net.Uri;
     47 import android.provider.OpenableColumns;
     48 import android.util.Log;
     49 
     50 import java.io.File;
     51 import java.io.FileOutputStream;
     52 import java.io.IOException;
     53 import java.io.InputStream;
     54 import java.util.ArrayList;
     55 import java.util.Date;
     56 import java.util.HashMap;
     57 
     58 public class LegacyConversions {
     59 
     60     /** DO NOT CHECK IN "TRUE" */
     61     private static final boolean DEBUG_ATTACHMENTS = false;
     62 
     63     /** Used for mapping folder names to type codes (e.g. inbox, drafts, trash) */
     64     private static final HashMap<String, Integer>
     65             sServerMailboxNames = new HashMap<String, Integer>();
     66 
     67     /**
     68      * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
     69      */
     70     /* package */ static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
     71     /* package */ static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
     72     /* package */ static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
     73 
     74     /**
     75      * Standard columns for querying content providers
     76      */
     77     private static final String[] ATTACHMENT_META_COLUMNS_PROJECTION = {
     78         OpenableColumns.DISPLAY_NAME,
     79         OpenableColumns.SIZE
     80     };
     81     private static final int ATTACHMENT_META_COLUMNS_SIZE = 1;
     82 
     83     /**
     84      * Copy field-by-field from a "store" message to a "provider" message
     85      * @param message The message we've just downloaded (must be a MimeMessage)
     86      * @param localMessage The message we'd like to write into the DB
     87      * @result true if dirty (changes were made)
     88      */
     89     public static boolean updateMessageFields(EmailContent.Message localMessage, Message message,
     90                 long accountId, long mailboxId) throws MessagingException {
     91 
     92         Address[] from = message.getFrom();
     93         Address[] to = message.getRecipients(Message.RecipientType.TO);
     94         Address[] cc = message.getRecipients(Message.RecipientType.CC);
     95         Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
     96         Address[] replyTo = message.getReplyTo();
     97         String subject = message.getSubject();
     98         Date sentDate = message.getSentDate();
     99         Date internalDate = message.getInternalDate();
    100 
    101         if (from != null && from.length > 0) {
    102             localMessage.mDisplayName = from[0].toFriendly();
    103         }
    104         if (sentDate != null) {
    105             localMessage.mTimeStamp = sentDate.getTime();
    106         }
    107         if (subject != null) {
    108             localMessage.mSubject = subject;
    109         }
    110         localMessage.mFlagRead = message.isSet(Flag.SEEN);
    111 
    112         // Keep the message in the "unloaded" state until it has (at least) a display name.
    113         // This prevents early flickering of empty messages in POP download.
    114         if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
    115             if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
    116                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
    117             } else {
    118                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
    119             }
    120         }
    121         localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);
    122 //        public boolean mFlagAttachment = false;
    123 //        public int mFlags = 0;
    124 
    125         localMessage.mServerId = message.getUid();
    126         if (internalDate != null) {
    127             localMessage.mServerTimeStamp = internalDate.getTime();
    128         }
    129 //        public String mClientId;
    130 
    131         // Only replace the local message-id if a new one was found.  This is seen in some ISP's
    132         // which may deliver messages w/o a message-id header.
    133         String messageId = ((MimeMessage)message).getMessageId();
    134         if (messageId != null) {
    135             localMessage.mMessageId = messageId;
    136         }
    137 
    138 //        public long mBodyKey;
    139         localMessage.mMailboxKey = mailboxId;
    140         localMessage.mAccountKey = accountId;
    141 
    142         if (from != null && from.length > 0) {
    143             localMessage.mFrom = Address.pack(from);
    144         }
    145 
    146         localMessage.mTo = Address.pack(to);
    147         localMessage.mCc = Address.pack(cc);
    148         localMessage.mBcc = Address.pack(bcc);
    149         localMessage.mReplyTo = Address.pack(replyTo);
    150 
    151 //        public String mText;
    152 //        public String mHtml;
    153 //        public String mTextReply;
    154 //        public String mHtmlReply;
    155 
    156 //        // Can be used while building messages, but is NOT saved by the Provider
    157 //        transient public ArrayList<Attachment> mAttachments = null;
    158 
    159         return true;
    160     }
    161 
    162     /**
    163      * Copy body text (plain and/or HTML) from MimeMessage to provider Message
    164      */
    165     public static boolean updateBodyFields(EmailContent.Body body,
    166             EmailContent.Message localMessage, ArrayList<Part> viewables)
    167             throws MessagingException {
    168 
    169         body.mMessageKey = localMessage.mId;
    170 
    171         StringBuffer sbHtml = null;
    172         StringBuffer sbText = null;
    173         StringBuffer sbHtmlReply = null;
    174         StringBuffer sbTextReply = null;
    175         StringBuffer sbIntroText = null;
    176 
    177         for (Part viewable : viewables) {
    178             String text = MimeUtility.getTextFromPart(viewable);
    179             String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
    180             String replyTag = null;
    181             if (replyTags != null && replyTags.length > 0) {
    182                 replyTag = replyTags[0];
    183             }
    184             // Deploy text as marked by the various tags
    185             boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
    186 
    187             if (replyTag != null) {
    188                 boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
    189                 boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
    190                 boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
    191 
    192                 if (isQuotedReply || isQuotedForward) {
    193                     if (isHtml) {
    194                         sbHtmlReply = appendTextPart(sbHtmlReply, text);
    195                     } else {
    196                         sbTextReply = appendTextPart(sbTextReply, text);
    197                     }
    198                     // Set message flags as well
    199                     localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
    200                     localMessage.mFlags |= isQuotedReply
    201                             ? EmailContent.Message.FLAG_TYPE_REPLY
    202                             : EmailContent.Message.FLAG_TYPE_FORWARD;
    203                     continue;
    204                 }
    205                 if (isQuotedIntro) {
    206                     sbIntroText = appendTextPart(sbIntroText, text);
    207                     continue;
    208                 }
    209             }
    210 
    211             // Most of the time, just process regular body parts
    212             if (isHtml) {
    213                 sbHtml = appendTextPart(sbHtml, text);
    214             } else {
    215                 sbText = appendTextPart(sbText, text);
    216             }
    217         }
    218 
    219         // write the combined data to the body part
    220         if (sbText != null && sbText.length() != 0) {
    221             body.mTextContent = sbText.toString();
    222         }
    223         if (sbHtml != null && sbHtml.length() != 0) {
    224             body.mHtmlContent = sbHtml.toString();
    225         }
    226         if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
    227             body.mHtmlReply = sbHtmlReply.toString();
    228         }
    229         if (sbTextReply != null && sbTextReply.length() != 0) {
    230             body.mTextReply = sbTextReply.toString();
    231         }
    232         if (sbIntroText != null && sbIntroText.length() != 0) {
    233             body.mIntroText = sbIntroText.toString();
    234         }
    235         return true;
    236     }
    237 
    238     /**
    239      * Helper function to append text to a StringBuffer, creating it if necessary.
    240      * Optimization:  The majority of the time we are *not* appending - we should have a path
    241      * that deals with single strings.
    242      */
    243     private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
    244         if (newText == null) {
    245             return sb;
    246         }
    247         else if (sb == null) {
    248             sb = new StringBuffer(newText);
    249         } else {
    250             if (sb.length() > 0) {
    251                 sb.append('\n');
    252             }
    253             sb.append(newText);
    254         }
    255         return sb;
    256     }
    257 
    258     /**
    259      * Copy attachments from MimeMessage to provider Message.
    260      *
    261      * @param context a context for file operations
    262      * @param localMessage the attachments will be built against this message
    263      * @param attachments the attachments to add
    264      * @param upgrading if true, we are upgrading a local account - handle attachments differently
    265      * @throws IOException
    266      */
    267     public static void updateAttachments(Context context, EmailContent.Message localMessage,
    268             ArrayList<Part> attachments, boolean upgrading) throws MessagingException, IOException {
    269         localMessage.mAttachments = null;
    270         for (Part attachmentPart : attachments) {
    271             addOneAttachment(context, localMessage, attachmentPart, upgrading);
    272         }
    273     }
    274 
    275     /**
    276      * Add a single attachment part to the message
    277      *
    278      * This will skip adding attachments if they are already found in the attachments table.
    279      * The heuristic for this will fail (false-positive) if two identical attachments are
    280      * included in a single POP3 message.
    281      * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments
    282      * position within the list of multipart/mixed elements.  This would make every POP3 attachment
    283      * unique, and might also simplify the code (since we could just look at the positions, and
    284      * ignore the filename, etc.)
    285      *
    286      * TODO: Take a closer look at encoding and deal with it if necessary.
    287      *
    288      * @param context a context for file operations
    289      * @param localMessage the attachments will be built against this message
    290      * @param part a single attachment part from POP or IMAP
    291      * @param upgrading true if upgrading a local account - handle attachments differently
    292      * @throws IOException
    293      */
    294     private static void addOneAttachment(Context context, EmailContent.Message localMessage,
    295             Part part, boolean upgrading) throws MessagingException, IOException {
    296 
    297         Attachment localAttachment = new Attachment();
    298 
    299         // Transfer fields from mime format to provider format
    300         String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
    301         String name = MimeUtility.getHeaderParameter(contentType, "name");
    302         if (name == null) {
    303             String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
    304             name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
    305         }
    306 
    307         // Select the URI for the new attachments.  For attachments downloaded by the legacy
    308         // IMAP/POP code, this is not determined yet, so is null (it will be rewritten below,
    309         // or later, when the actual attachment file is created.)
    310         //
    311         // When upgrading older local accounts, the URI represents a local asset (e.g. a photo)
    312         // so we need to preserve the URI.
    313         // TODO This works for outgoing messages, where the URI does not change.  May need
    314         // additional logic to handle the case of rewriting URI for received attachments.
    315         Uri contentUri = null;
    316         String contentUriString = null;
    317         if (upgrading) {
    318             Body body = part.getBody();
    319             if (body instanceof LocalStore.LocalAttachmentBody) {
    320                 LocalStore.LocalAttachmentBody localBody = (LocalStore.LocalAttachmentBody) body;
    321                 contentUri = localBody.getContentUri();
    322                 if (contentUri != null) {
    323                     contentUriString = contentUri.toString();
    324                 }
    325             }
    326         }
    327 
    328         // Find size, if available, via a number of techniques:
    329         long size = 0;
    330         if (upgrading) {
    331             // If upgrading a legacy account, the size must be recaptured from the data source
    332             if (contentUri != null) {
    333                 Cursor metadataCursor = context.getContentResolver().query(contentUri,
    334                         ATTACHMENT_META_COLUMNS_PROJECTION, null, null, null);
    335                 if (metadataCursor != null) {
    336                     try {
    337                         if (metadataCursor.moveToFirst()) {
    338                             size = metadataCursor.getInt(ATTACHMENT_META_COLUMNS_SIZE);
    339                         }
    340                     } finally {
    341                         metadataCursor.close();
    342                     }
    343                 }
    344             }
    345             // TODO: a downloaded legacy attachment - see if the above code works
    346         } else {
    347             // Incoming attachment: Try to pull size from disposition (if not downloaded yet)
    348             String disposition = part.getDisposition();
    349             if (disposition != null) {
    350                 String s = MimeUtility.getHeaderParameter(disposition, "size");
    351                 if (s != null) {
    352                     size = Long.parseLong(s);
    353                 }
    354             }
    355         }
    356 
    357         // Get partId for unloaded IMAP attachments (if any)
    358         // This is only provided (and used) when we have structure but not the actual attachment
    359         String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
    360         String partId = partIds != null ? partIds[0] : null;
    361 
    362         localAttachment.mFileName = name;
    363         localAttachment.mMimeType = part.getMimeType();
    364         localAttachment.mSize = size;           // May be reset below if file handled
    365         localAttachment.mContentId = part.getContentId();
    366         localAttachment.mContentUri = contentUriString;
    367         localAttachment.mMessageKey = localMessage.mId;
    368         localAttachment.mLocation = partId;
    369         localAttachment.mEncoding = "B";        // TODO - convert other known encodings
    370 
    371         if (DEBUG_ATTACHMENTS) {
    372             Log.d(Email.LOG_TAG, "Add attachment " + localAttachment);
    373         }
    374 
    375         // To prevent duplication - do we already have a matching attachment?
    376         // The fields we'll check for equality are:
    377         //  mFileName, mMimeType, mContentId, mMessageKey, mLocation
    378         // NOTE:  This will false-positive if you attach the exact same file, twice, to a POP3
    379         // message.  We can live with that - you'll get one of the copies.
    380         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    381         Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
    382                 null, null, null);
    383         boolean attachmentFoundInDb = false;
    384         try {
    385             while (cursor.moveToNext()) {
    386                 Attachment dbAttachment = new Attachment().restore(cursor);
    387                 // We test each of the fields here (instead of in SQL) because they may be
    388                 // null, or may be strings.
    389                 if (stringNotEqual(dbAttachment.mFileName, localAttachment.mFileName)) continue;
    390                 if (stringNotEqual(dbAttachment.mMimeType, localAttachment.mMimeType)) continue;
    391                 if (stringNotEqual(dbAttachment.mContentId, localAttachment.mContentId)) continue;
    392                 if (stringNotEqual(dbAttachment.mLocation, localAttachment.mLocation)) continue;
    393                 // We found a match, so use the existing attachment id, and stop looking/looping
    394                 attachmentFoundInDb = true;
    395                 localAttachment.mId = dbAttachment.mId;
    396                 if (DEBUG_ATTACHMENTS) {
    397                     Log.d(Email.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
    398                 }
    399                 break;
    400             }
    401         } finally {
    402             cursor.close();
    403         }
    404 
    405         // Save the attachment (so far) in order to obtain an id
    406         if (!attachmentFoundInDb) {
    407             localAttachment.save(context);
    408         }
    409 
    410         // If an attachment body was actually provided, we need to write the file now
    411         if (!upgrading) {
    412             saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
    413         }
    414 
    415         if (localMessage.mAttachments == null) {
    416             localMessage.mAttachments = new ArrayList<Attachment>();
    417         }
    418         localMessage.mAttachments.add(localAttachment);
    419         localMessage.mFlagAttachment = true;
    420     }
    421 
    422     /**
    423      * Helper for addOneAttachment that compares two strings, deals with nulls, and treats
    424      * nulls and empty strings as equal.
    425      */
    426     /* package */ static boolean stringNotEqual(String a, String b) {
    427         if (a == null && b == null) return false;       // fast exit for two null strings
    428         if (a == null) a = "";
    429         if (b == null) b = "";
    430         return !a.equals(b);
    431     }
    432 
    433     /**
    434      * Save the body part of a single attachment, to a file in the attachments directory.
    435      */
    436     public static void saveAttachmentBody(Context context, Part part, Attachment localAttachment,
    437             long accountId) throws MessagingException, IOException {
    438         if (part.getBody() != null) {
    439             long attachmentId = localAttachment.mId;
    440 
    441             InputStream in = part.getBody().getInputStream();
    442 
    443             File saveIn = AttachmentProvider.getAttachmentDirectory(context, accountId);
    444             if (!saveIn.exists()) {
    445                 saveIn.mkdirs();
    446             }
    447             File saveAs = AttachmentProvider.getAttachmentFilename(context, accountId,
    448                     attachmentId);
    449             saveAs.createNewFile();
    450             FileOutputStream out = new FileOutputStream(saveAs);
    451             long copySize = IOUtils.copy(in, out);
    452             in.close();
    453             out.close();
    454 
    455             // update the attachment with the extra information we now know
    456             String contentUriString = AttachmentProvider.getAttachmentUri(
    457                     accountId, attachmentId).toString();
    458 
    459             localAttachment.mSize = copySize;
    460             localAttachment.mContentUri = contentUriString;
    461 
    462             // update the attachment in the database as well
    463             ContentValues cv = new ContentValues();
    464             cv.put(AttachmentColumns.SIZE, copySize);
    465             cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
    466             Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
    467             context.getContentResolver().update(uri, cv, null, null);
    468         }
    469     }
    470 
    471     /**
    472      * Read a complete Provider message into a legacy message (for IMAP upload).  This
    473      * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch().
    474      */
    475     public static Message makeMessage(Context context, EmailContent.Message localMessage)
    476             throws MessagingException {
    477         MimeMessage message = new MimeMessage();
    478 
    479         // LocalFolder.getMessages() equivalent:  Copy message fields
    480         message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject);
    481         Address[] from = Address.unpack(localMessage.mFrom);
    482         if (from.length > 0) {
    483             message.setFrom(from[0]);
    484         }
    485         message.setSentDate(new Date(localMessage.mTimeStamp));
    486         message.setUid(localMessage.mServerId);
    487         message.setFlag(Flag.DELETED,
    488                 localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED);
    489         message.setFlag(Flag.SEEN, localMessage.mFlagRead);
    490         message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite);
    491 //      message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey);
    492         message.setRecipients(RecipientType.TO, Address.unpack(localMessage.mTo));
    493         message.setRecipients(RecipientType.CC, Address.unpack(localMessage.mCc));
    494         message.setRecipients(RecipientType.BCC, Address.unpack(localMessage.mBcc));
    495         message.setReplyTo(Address.unpack(localMessage.mReplyTo));
    496         message.setInternalDate(new Date(localMessage.mServerTimeStamp));
    497         message.setMessageId(localMessage.mMessageId);
    498 
    499         // LocalFolder.fetch() equivalent: build body parts
    500         message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
    501         MimeMultipart mp = new MimeMultipart();
    502         mp.setSubType("mixed");
    503         message.setBody(mp);
    504 
    505         try {
    506             addTextBodyPart(mp, "text/html", null,
    507                     EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId));
    508         } catch (RuntimeException rte) {
    509             Log.d(Email.LOG_TAG, "Exception while reading html body " + rte.toString());
    510         }
    511 
    512         try {
    513             addTextBodyPart(mp, "text/plain", null,
    514                     EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId));
    515         } catch (RuntimeException rte) {
    516             Log.d(Email.LOG_TAG, "Exception while reading text body " + rte.toString());
    517         }
    518 
    519         boolean isReply = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_REPLY) != 0;
    520         boolean isForward = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0;
    521 
    522         // If there is a quoted part (forwarding or reply), add the intro first, and then the
    523         // rest of it.  If it is opened in some other viewer, it will (hopefully) be displayed in
    524         // the same order as we've just set up the blocks:  composed text, intro, replied text
    525         if (isReply || isForward) {
    526             try {
    527                 addTextBodyPart(mp, "text/plain", BODY_QUOTED_PART_INTRO,
    528                         EmailContent.Body.restoreIntroTextWithMessageId(context, localMessage.mId));
    529             } catch (RuntimeException rte) {
    530                 Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString());
    531             }
    532 
    533             String replyTag = isReply ? BODY_QUOTED_PART_REPLY : BODY_QUOTED_PART_FORWARD;
    534             try {
    535                 addTextBodyPart(mp, "text/html", replyTag,
    536                         EmailContent.Body.restoreReplyHtmlWithMessageId(context, localMessage.mId));
    537             } catch (RuntimeException rte) {
    538                 Log.d(Email.LOG_TAG, "Exception while reading html reply " + rte.toString());
    539             }
    540 
    541             try {
    542                 addTextBodyPart(mp, "text/plain", replyTag,
    543                         EmailContent.Body.restoreReplyTextWithMessageId(context, localMessage.mId));
    544             } catch (RuntimeException rte) {
    545                 Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString());
    546             }
    547         }
    548 
    549         // Attachments
    550         // TODO: Make sure we deal with these as structures and don't accidentally upload files
    551 //        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
    552 //        Cursor attachments = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
    553 //                null, null, null);
    554 //        try {
    555 //
    556 //        } finally {
    557 //            attachments.close();
    558 //        }
    559 
    560         return message;
    561     }
    562 
    563     /**
    564      * Helper method to add a body part for a given type of text, if found
    565      *
    566      * @param mp The text body part will be added to this multipart
    567      * @param contentType The content-type of the text being added
    568      * @param quotedPartTag If non-null, HEADER_ANDROID_BODY_QUOTED_PART will be set to this value
    569      * @param partText The text to add.  If null, nothing happens
    570      */
    571     private static void addTextBodyPart(MimeMultipart mp, String contentType, String quotedPartTag,
    572             String partText) throws MessagingException {
    573         if (partText == null) {
    574             return;
    575         }
    576         TextBody body = new TextBody(partText);
    577         MimeBodyPart bp = new MimeBodyPart(body, contentType);
    578         if (quotedPartTag != null) {
    579             bp.addHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART, quotedPartTag);
    580         }
    581         mp.addBodyPart(bp);
    582     }
    583 
    584     /**
    585      * Conversion from provider account to legacy account
    586      *
    587      * Used for backup/restore.
    588      *
    589      * @param context application context
    590      * @param fromAccount the provider account to be backed up (including transient hostauth's)
    591      * @return a legacy Account object ready to be committed to preferences
    592      */
    593     /* package */ static Account makeLegacyAccount(Context context,
    594             EmailContent.Account fromAccount) {
    595         Account result = new Account(context);
    596 
    597         result.setDescription(fromAccount.getDisplayName());
    598         result.setEmail(fromAccount.getEmailAddress());
    599         // fromAccount.mSyncKey - assume lost if restoring
    600         result.setSyncWindow(fromAccount.getSyncLookback());
    601         result.setAutomaticCheckIntervalMinutes(fromAccount.getSyncInterval());
    602         // fromAccount.mHostAuthKeyRecv - id not saved; will be reassigned when restoring
    603         // fromAccount.mHostAuthKeySend - id not saved; will be reassigned when restoring
    604 
    605         // Provider Account flags, and how they are mapped.
    606         //  FLAGS_NOTIFY_NEW_MAIL       -> mNotifyNewMail
    607         //  FLAGS_VIBRATE_ALWAYS        -> mVibrate
    608         //  FLAGS_VIBRATE_WHEN_SILENT   -> mVibrateWhenSilent
    609         //  DELETE_POLICY_NEVER         -> mDeletePolicy
    610         //  DELETE_POLICY_7DAYS
    611         //  DELETE_POLICY_ON_DELETE
    612         result.setNotifyNewMail(0 !=
    613             (fromAccount.getFlags() & EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL));
    614         result.setVibrate(0 !=
    615             (fromAccount.getFlags() & EmailContent.Account.FLAGS_VIBRATE_ALWAYS));
    616         result.setVibrateWhenSilent(0 !=
    617             (fromAccount.getFlags() & EmailContent.Account.FLAGS_VIBRATE_WHEN_SILENT));
    618         result.setDeletePolicy(fromAccount.getDeletePolicy());
    619 
    620         result.mUuid = fromAccount.getUuid();
    621         result.setName(fromAccount.mSenderName);
    622         result.setRingtone(fromAccount.mRingtoneUri);
    623         result.mProtocolVersion = fromAccount.mProtocolVersion;
    624         // int fromAccount.mNewMessageCount = will be reset on next sync
    625         result.mSecurityFlags = fromAccount.mSecurityFlags;
    626         result.mSignature = fromAccount.mSignature;
    627 
    628         // Use the existing conversions from HostAuth <-> Uri
    629         result.setStoreUri(fromAccount.getStoreUri(context));
    630         result.setSenderUri(fromAccount.getSenderUri(context));
    631 
    632         return result;
    633     }
    634 
    635     /**
    636      * Conversion from legacy account to provider account
    637      *
    638      * Used for backup/restore and for account migration.
    639      *
    640      * @param context application context
    641      * @param fromAccount the legacy account to convert to modern format
    642      * @return an Account ready to be committed to provider
    643      */
    644     public static EmailContent.Account makeAccount(Context context, Account fromAccount) {
    645 
    646         EmailContent.Account result = new EmailContent.Account();
    647 
    648         result.setDisplayName(fromAccount.getDescription());
    649         result.setEmailAddress(fromAccount.getEmail());
    650         result.mSyncKey = null;
    651         result.setSyncLookback(fromAccount.getSyncWindow());
    652         result.setSyncInterval(fromAccount.getAutomaticCheckIntervalMinutes());
    653         // result.mHostAuthKeyRecv;     -- will be set when object is saved
    654         // result.mHostAuthKeySend;     -- will be set when object is saved
    655         int flags = 0;
    656         if (fromAccount.isNotifyNewMail())  flags |= EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL;
    657         if (fromAccount.isVibrate())        flags |= EmailContent.Account.FLAGS_VIBRATE_ALWAYS;
    658         if (fromAccount.isVibrateWhenSilent())
    659             flags |= EmailContent.Account.FLAGS_VIBRATE_WHEN_SILENT;
    660         result.setFlags(flags);
    661         result.setDeletePolicy(fromAccount.getDeletePolicy());
    662         // result.setDefaultAccount();  -- will be set by caller, if neededf
    663         result.mCompatibilityUuid = fromAccount.getUuid();
    664         result.setSenderName(fromAccount.getName());
    665         result.setRingtone(fromAccount.getRingtone());
    666         result.mProtocolVersion = fromAccount.mProtocolVersion;
    667         result.mNewMessageCount = 0;
    668         result.mSecurityFlags = fromAccount.mSecurityFlags;
    669         result.mSecuritySyncKey = null;
    670         result.mSignature = fromAccount.mSignature;
    671 
    672         result.setStoreUri(context, fromAccount.getStoreUri());
    673         result.setSenderUri(context, fromAccount.getSenderUri());
    674 
    675         return result;
    676     }
    677 
    678     /**
    679      * Conversion from legacy folder to provider mailbox.  Used for account migration.
    680      * Note: Many mailbox fields are unused in IMAP & POP accounts.
    681      *
    682      * @param context application context
    683      * @param toAccount the provider account that this folder will be associated with
    684      * @param fromFolder the legacy folder to convert to modern format
    685      * @return an Account ready to be committed to provider
    686      */
    687     public static EmailContent.Mailbox makeMailbox(Context context, EmailContent.Account toAccount,
    688             Folder fromFolder) throws MessagingException {
    689         EmailContent.Mailbox result = new EmailContent.Mailbox();
    690 
    691         result.mDisplayName = fromFolder.getName();
    692         // result.mServerId
    693         // result.mParentServerId
    694         result.mAccountKey = toAccount.mId;
    695         result.mType = inferMailboxTypeFromName(context, fromFolder.getName());
    696         // result.mDelimiter
    697         // result.mSyncKey
    698         // result.mSyncLookback
    699         // result.mSyncInterval
    700         result.mSyncTime = 0;
    701         result.mUnreadCount = fromFolder.getUnreadMessageCount();
    702         result.mFlagVisible = true;
    703         result.mFlags = 0;
    704         result.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
    705         // result.mSyncStatus
    706 
    707         return result;
    708     }
    709 
    710     /**
    711      * Infer mailbox type from mailbox name.  Used by MessagingController (for live folder sync)
    712      * and for legacy account upgrades.
    713      */
    714     public static synchronized int inferMailboxTypeFromName(Context context, String mailboxName) {
    715         if (sServerMailboxNames.size() == 0) {
    716             // preload the hashmap, one time only
    717             sServerMailboxNames.put(
    718                     context.getString(R.string.mailbox_name_server_inbox).toLowerCase(),
    719                     Mailbox.TYPE_INBOX);
    720             sServerMailboxNames.put(
    721                     context.getString(R.string.mailbox_name_server_outbox).toLowerCase(),
    722                     Mailbox.TYPE_OUTBOX);
    723             sServerMailboxNames.put(
    724                     context.getString(R.string.mailbox_name_server_drafts).toLowerCase(),
    725                     Mailbox.TYPE_DRAFTS);
    726             sServerMailboxNames.put(
    727                     context.getString(R.string.mailbox_name_server_trash).toLowerCase(),
    728                     Mailbox.TYPE_TRASH);
    729             sServerMailboxNames.put(
    730                     context.getString(R.string.mailbox_name_server_sent).toLowerCase(),
    731                     Mailbox.TYPE_SENT);
    732             sServerMailboxNames.put(
    733                     context.getString(R.string.mailbox_name_server_junk).toLowerCase(),
    734                     Mailbox.TYPE_JUNK);
    735         }
    736         if (mailboxName == null || mailboxName.length() == 0) {
    737             return EmailContent.Mailbox.TYPE_MAIL;
    738         }
    739         String lowerCaseName = mailboxName.toLowerCase();
    740         Integer type = sServerMailboxNames.get(lowerCaseName);
    741         if (type != null) {
    742             return type;
    743         }
    744         return EmailContent.Mailbox.TYPE_MAIL;
    745     }
    746 }
    747