• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
updateMessageFields(final EmailContent.Message localMessage, final Message message, final long accountId, final long mailboxId)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      */
updateAttachments(final Context context, final EmailContent.Message localMessage, final ArrayList<Part> attachments)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 
updateInlineAttachments(final Context context, final EmailContent.Message localMessage, final ArrayList<Part> inlineAttachments)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
mimePartToAttachment(final Part part)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      */
addOneAttachment(final Context context, final EmailContent.Message localMessage, final Part part)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      */
saveAttachmentBody(final Context context, final Part part, final Attachment localAttachment, long accountId)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      */
makeMessage(final Context context, final EmailContent.Message localMessage)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      */
addTextBodyPart(final MimeMultipart mp, final String contentType, final String partText)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
addAttachmentPart(final Multipart mp, final String contentType, final Long contentSize, final String filename, final String contentId, final InputStream content)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
inferMailboxTypeFromName(Context context, String mailboxName)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