• 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 com.android.email.mail.Address;
20 import com.android.email.mail.Flag;
21 import com.android.email.mail.Message;
22 import com.android.email.mail.MessagingException;
23 import com.android.email.mail.Part;
24 import com.android.email.mail.Message.RecipientType;
25 import com.android.email.mail.internet.MimeBodyPart;
26 import com.android.email.mail.internet.MimeHeader;
27 import com.android.email.mail.internet.MimeMessage;
28 import com.android.email.mail.internet.MimeMultipart;
29 import com.android.email.mail.internet.MimeUtility;
30 import com.android.email.mail.internet.TextBody;
31 import com.android.email.provider.AttachmentProvider;
32 import com.android.email.provider.EmailContent;
33 import com.android.email.provider.EmailContent.Attachment;
34 import com.android.email.provider.EmailContent.AttachmentColumns;
35 
36 import org.apache.commons.io.IOUtils;
37 
38 import android.content.ContentUris;
39 import android.content.ContentValues;
40 import android.content.Context;
41 import android.database.Cursor;
42 import android.net.Uri;
43 import android.util.Log;
44 
45 import java.io.File;
46 import java.io.FileOutputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.util.ArrayList;
50 import java.util.Date;
51 
52 public class LegacyConversions {
53 
54     /** DO NOT CHECK IN "TRUE" */
55     private static final boolean DEBUG_ATTACHMENTS = false;
56 
57     /**
58      * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
59      */
60     /* package */ static final String BODY_QUOTED_PART_REPLY = "quoted-reply";
61     /* package */ static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
62     /* package */ static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
63 
64     /**
65      * Copy field-by-field from a "store" message to a "provider" message
66      * @param message The message we've just downloaded (must be a MimeMessage)
67      * @param localMessage The message we'd like to write into the DB
68      * @result true if dirty (changes were made)
69      */
updateMessageFields(EmailContent.Message localMessage, Message message, long accountId, long mailboxId)70     public static boolean updateMessageFields(EmailContent.Message localMessage, Message message,
71                 long accountId, long mailboxId) throws MessagingException {
72 
73         Address[] from = message.getFrom();
74         Address[] to = message.getRecipients(Message.RecipientType.TO);
75         Address[] cc = message.getRecipients(Message.RecipientType.CC);
76         Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
77         Address[] replyTo = message.getReplyTo();
78         String subject = message.getSubject();
79         Date sentDate = message.getSentDate();
80         Date internalDate = message.getInternalDate();
81 
82         if (from != null && from.length > 0) {
83             localMessage.mDisplayName = from[0].toFriendly();
84         }
85         if (sentDate != null) {
86             localMessage.mTimeStamp = sentDate.getTime();
87         }
88         if (subject != null) {
89             localMessage.mSubject = subject;
90         }
91         localMessage.mFlagRead = message.isSet(Flag.SEEN);
92 
93         // Keep the message in the "unloaded" state until it has (at least) a display name.
94         // This prevents early flickering of empty messages in POP download.
95         if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
96             if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
97                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
98             } else {
99                 localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
100             }
101         }
102         localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);
103 //        public boolean mFlagAttachment = false;
104 //        public int mFlags = 0;
105 
106         localMessage.mServerId = message.getUid();
107         if (internalDate != null) {
108             localMessage.mServerTimeStamp = internalDate.getTime();
109         }
110 //        public String mClientId;
111 
112         // Absorb a MessagingException here in the case of messages that were delivered without
113         // a proper message-id.  This is seen in some ISP's but it is non-fatal -- (we'll just use
114         // the locally-generated message-id.)
115         try {
116             localMessage.mMessageId = ((MimeMessage)message).getMessageId();
117         } catch (MessagingException me)  {
118             if (Email.DEBUG) {
119                 Log.d(Email.LOG_TAG, "Missing message-id for UID=" + localMessage.mServerId);
120             }
121         }
122 
123 //        public long mBodyKey;
124         localMessage.mMailboxKey = mailboxId;
125         localMessage.mAccountKey = accountId;
126 
127         if (from != null && from.length > 0) {
128             localMessage.mFrom = Address.pack(from);
129         }
130 
131         localMessage.mTo = Address.pack(to);
132         localMessage.mCc = Address.pack(cc);
133         localMessage.mBcc = Address.pack(bcc);
134         localMessage.mReplyTo = Address.pack(replyTo);
135 
136 //        public String mText;
137 //        public String mHtml;
138 //        public String mTextReply;
139 //        public String mHtmlReply;
140 
141 //        // Can be used while building messages, but is NOT saved by the Provider
142 //        transient public ArrayList<Attachment> mAttachments = null;
143 
144         return true;
145     }
146 
147     /**
148      * Copy body text (plain and/or HTML) from MimeMessage to provider Message
149      */
updateBodyFields(EmailContent.Body body, EmailContent.Message localMessage, ArrayList<Part> viewables)150     public static boolean updateBodyFields(EmailContent.Body body,
151             EmailContent.Message localMessage, ArrayList<Part> viewables)
152             throws MessagingException {
153 
154         body.mMessageKey = localMessage.mId;
155 
156         StringBuffer sbHtml = null;
157         StringBuffer sbText = null;
158         StringBuffer sbHtmlReply = null;
159         StringBuffer sbTextReply = null;
160         StringBuffer sbIntroText = null;
161 
162         for (Part viewable : viewables) {
163             String text = MimeUtility.getTextFromPart(viewable);
164             String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART);
165             String replyTag = null;
166             if (replyTags != null && replyTags.length > 0) {
167                 replyTag = replyTags[0];
168             }
169             // Deploy text as marked by the various tags
170             boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType());
171 
172             if (replyTag != null) {
173                 boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag);
174                 boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag);
175                 boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag);
176 
177                 if (isQuotedReply || isQuotedForward) {
178                     if (isHtml) {
179                         sbHtmlReply = appendTextPart(sbHtmlReply, text);
180                     } else {
181                         sbTextReply = appendTextPart(sbTextReply, text);
182                     }
183                     // Set message flags as well
184                     localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
185                     localMessage.mFlags |= isQuotedReply
186                             ? EmailContent.Message.FLAG_TYPE_REPLY
187                             : EmailContent.Message.FLAG_TYPE_FORWARD;
188                     continue;
189                 }
190                 if (isQuotedIntro) {
191                     sbIntroText = appendTextPart(sbIntroText, text);
192                     continue;
193                 }
194             }
195 
196             // Most of the time, just process regular body parts
197             if (isHtml) {
198                 sbHtml = appendTextPart(sbHtml, text);
199             } else {
200                 sbText = appendTextPart(sbText, text);
201             }
202         }
203 
204         // write the combined data to the body part
205         if (sbText != null && sbText.length() != 0) {
206             body.mTextContent = sbText.toString();
207         }
208         if (sbHtml != null && sbHtml.length() != 0) {
209             body.mHtmlContent = sbHtml.toString();
210         }
211         if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
212             body.mHtmlReply = sbHtmlReply.toString();
213         }
214         if (sbTextReply != null && sbTextReply.length() != 0) {
215             body.mTextReply = sbTextReply.toString();
216         }
217         if (sbIntroText != null && sbIntroText.length() != 0) {
218             body.mIntroText = sbIntroText.toString();
219         }
220         return true;
221     }
222 
223     /**
224      * Helper function to append text to a StringBuffer, creating it if necessary.
225      * Optimization:  The majority of the time we are *not* appending - we should have a path
226      * that deals with single strings.
227      */
appendTextPart(StringBuffer sb, String newText)228     private static StringBuffer appendTextPart(StringBuffer sb, String newText) {
229         if (newText == null) {
230             return sb;
231         }
232         else if (sb == null) {
233             sb = new StringBuffer(newText);
234         } else {
235             if (sb.length() > 0) {
236                 sb.append('\n');
237             }
238             sb.append(newText);
239         }
240         return sb;
241     }
242 
243     /**
244      * Copy attachments from MimeMessage to provider Message.
245      *
246      * @param context a context for file operations
247      * @param localMessage the attachments will be built against this message
248      * @param attachments the attachments to add
249      * @throws IOException
250      */
updateAttachments(Context context, EmailContent.Message localMessage, ArrayList<Part> attachments)251     public static void updateAttachments(Context context, EmailContent.Message localMessage,
252             ArrayList<Part> attachments) throws MessagingException, IOException {
253         localMessage.mAttachments = null;
254         for (Part attachmentPart : attachments) {
255             addOneAttachment(context, localMessage, attachmentPart);
256         }
257     }
258 
259     /**
260      * Add a single attachment part to the message
261      *
262      * This will skip adding attachments if they are already found in the attachments table.
263      * The heuristic for this will fail (false-positive) if two identical attachments are
264      * included in a single POP3 message.
265      * TODO: Fix that, by (elsewhere) simulating an mLocation value based on the attachments
266      * position within the list of multipart/mixed elements.  This would make every POP3 attachment
267      * unique, and might also simplify the code (since we could just look at the positions, and
268      * ignore the filename, etc.)
269      *
270      * TODO: Take a closer look at encoding and deal with it if necessary.
271      *
272      * @param context a context for file operations
273      * @param localMessage the attachments will be built against this message
274      * @param part a single attachment part from POP or IMAP
275      * @throws IOException
276      */
addOneAttachment(Context context, EmailContent.Message localMessage, Part part)277     private static void addOneAttachment(Context context, EmailContent.Message localMessage,
278             Part part) throws MessagingException, IOException {
279 
280         Attachment localAttachment = new Attachment();
281 
282         // Transfer fields from mime format to provider format
283         String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
284         String name = MimeUtility.getHeaderParameter(contentType, "name");
285         if (name == null) {
286             String contentDisposition = MimeUtility.unfoldAndDecode(part.getContentType());
287             name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
288         }
289 
290         // Try to pull size from disposition (if not downloaded)
291         long size = 0;
292         String disposition = part.getDisposition();
293         if (disposition != null) {
294             String s = MimeUtility.getHeaderParameter(disposition, "size");
295             if (s != null) {
296                 size = Long.parseLong(s);
297             }
298         }
299 
300         // Get partId for unloaded IMAP attachments (if any)
301         // This is only provided (and used) when we have structure but not the actual attachment
302         String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
303         String partId = partIds != null ? partIds[0] : null;
304 
305         localAttachment.mFileName = MimeUtility.getHeaderParameter(contentType, "name");
306         localAttachment.mMimeType = part.getMimeType();
307         localAttachment.mSize = size;           // May be reset below if file handled
308         localAttachment.mContentId = part.getContentId();
309         localAttachment.mContentUri = null;     // Will be set when file is saved
310         localAttachment.mMessageKey = localMessage.mId;
311         localAttachment.mLocation = partId;
312         localAttachment.mEncoding = "B";        // TODO - convert other known encodings
313 
314         if (DEBUG_ATTACHMENTS) {
315             Log.d(Email.LOG_TAG, "Add attachment " + localAttachment);
316         }
317 
318         // To prevent duplication - do we already have a matching attachment?
319         // The fields we'll check for equality are:
320         //  mFileName, mMimeType, mContentId, mMessageKey, mLocation
321         // NOTE:  This will false-positive if you attach the exact same file, twice, to a POP3
322         // message.  We can live with that - you'll get one of the copies.
323         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
324         Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
325                 null, null, null);
326         boolean attachmentFoundInDb = false;
327         try {
328             while (cursor.moveToNext()) {
329                 Attachment dbAttachment = new Attachment().restore(cursor);
330                 // We test each of the fields here (instead of in SQL) because they may be
331                 // null, or may be strings.
332                 if (stringNotEqual(dbAttachment.mFileName, localAttachment.mFileName)) continue;
333                 if (stringNotEqual(dbAttachment.mMimeType, localAttachment.mMimeType)) continue;
334                 if (stringNotEqual(dbAttachment.mContentId, localAttachment.mContentId)) continue;
335                 if (stringNotEqual(dbAttachment.mLocation, localAttachment.mLocation)) continue;
336                 // We found a match, so use the existing attachment id, and stop looking/looping
337                 attachmentFoundInDb = true;
338                 localAttachment.mId = dbAttachment.mId;
339                 if (DEBUG_ATTACHMENTS) {
340                     Log.d(Email.LOG_TAG, "Skipped, found db attachment " + dbAttachment);
341                 }
342                 break;
343             }
344         } finally {
345             cursor.close();
346         }
347 
348         // Save the attachment (so far) in order to obtain an id
349         if (!attachmentFoundInDb) {
350             localAttachment.save(context);
351         }
352 
353         // If an attachment body was actually provided, we need to write the file now
354         saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
355 
356         if (localMessage.mAttachments == null) {
357             localMessage.mAttachments = new ArrayList<Attachment>();
358         }
359         localMessage.mAttachments.add(localAttachment);
360         localMessage.mFlagAttachment = true;
361     }
362 
363     /**
364      * Helper for addOneAttachment that compares two strings, deals with nulls, and treats
365      * nulls and empty strings as equal.
366      */
stringNotEqual(String a, String b)367     /* package */ static boolean stringNotEqual(String a, String b) {
368         if (a == null && b == null) return false;       // fast exit for two null strings
369         if (a == null) a = "";
370         if (b == null) b = "";
371         return !a.equals(b);
372     }
373 
374     /**
375      * Save the body part of a single attachment, to a file in the attachments directory.
376      */
saveAttachmentBody(Context context, Part part, Attachment localAttachment, long accountId)377     public static void saveAttachmentBody(Context context, Part part, Attachment localAttachment,
378             long accountId) throws MessagingException, IOException {
379         if (part.getBody() != null) {
380             long attachmentId = localAttachment.mId;
381 
382             InputStream in = part.getBody().getInputStream();
383 
384             File saveIn = AttachmentProvider.getAttachmentDirectory(context, accountId);
385             if (!saveIn.exists()) {
386                 saveIn.mkdirs();
387             }
388             File saveAs = AttachmentProvider.getAttachmentFilename(context, accountId,
389                     attachmentId);
390             saveAs.createNewFile();
391             FileOutputStream out = new FileOutputStream(saveAs);
392             long copySize = IOUtils.copy(in, out);
393             in.close();
394             out.close();
395 
396             // update the attachment with the extra information we now know
397             String contentUriString = AttachmentProvider.getAttachmentUri(
398                     accountId, attachmentId).toString();
399 
400             localAttachment.mSize = copySize;
401             localAttachment.mContentUri = contentUriString;
402 
403             // update the attachment in the database as well
404             ContentValues cv = new ContentValues();
405             cv.put(AttachmentColumns.SIZE, copySize);
406             cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
407             Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
408             context.getContentResolver().update(uri, cv, null, null);
409         }
410     }
411 
412     /**
413      * Read a complete Provider message into a legacy message (for IMAP upload).  This
414      * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch().
415      */
makeMessage(Context context, EmailContent.Message localMessage)416     public static Message makeMessage(Context context, EmailContent.Message localMessage)
417             throws MessagingException {
418         MimeMessage message = new MimeMessage();
419 
420         // LocalFolder.getMessages() equivalent:  Copy message fields
421         message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject);
422         Address[] from = Address.unpack(localMessage.mFrom);
423         if (from.length > 0) {
424             message.setFrom(from[0]);
425         }
426         message.setSentDate(new Date(localMessage.mTimeStamp));
427         message.setUid(localMessage.mServerId);
428         message.setFlag(Flag.DELETED,
429                 localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED);
430         message.setFlag(Flag.SEEN, localMessage.mFlagRead);
431         message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite);
432 //      message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey);
433         message.setRecipients(RecipientType.TO, Address.unpack(localMessage.mTo));
434         message.setRecipients(RecipientType.CC, Address.unpack(localMessage.mCc));
435         message.setRecipients(RecipientType.BCC, Address.unpack(localMessage.mBcc));
436         message.setReplyTo(Address.unpack(localMessage.mReplyTo));
437         message.setInternalDate(new Date(localMessage.mServerTimeStamp));
438         message.setMessageId(localMessage.mMessageId);
439 
440         // LocalFolder.fetch() equivalent: build body parts
441         message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
442         MimeMultipart mp = new MimeMultipart();
443         mp.setSubType("mixed");
444         message.setBody(mp);
445 
446         try {
447             addTextBodyPart(mp, "text/html", null,
448                     EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId));
449         } catch (RuntimeException rte) {
450             Log.d(Email.LOG_TAG, "Exception while reading html body " + rte.toString());
451         }
452 
453         try {
454             addTextBodyPart(mp, "text/plain", null,
455                     EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId));
456         } catch (RuntimeException rte) {
457             Log.d(Email.LOG_TAG, "Exception while reading text body " + rte.toString());
458         }
459 
460         boolean isReply = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_REPLY) != 0;
461         boolean isForward = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0;
462 
463         // If there is a quoted part (forwarding or reply), add the intro first, and then the
464         // rest of it.  If it is opened in some other viewer, it will (hopefully) be displayed in
465         // the same order as we've just set up the blocks:  composed text, intro, replied text
466         if (isReply || isForward) {
467             try {
468                 addTextBodyPart(mp, "text/plain", BODY_QUOTED_PART_INTRO,
469                         EmailContent.Body.restoreIntroTextWithMessageId(context, localMessage.mId));
470             } catch (RuntimeException rte) {
471                 Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString());
472             }
473 
474             String replyTag = isReply ? BODY_QUOTED_PART_REPLY : BODY_QUOTED_PART_FORWARD;
475             try {
476                 addTextBodyPart(mp, "text/html", replyTag,
477                         EmailContent.Body.restoreReplyHtmlWithMessageId(context, localMessage.mId));
478             } catch (RuntimeException rte) {
479                 Log.d(Email.LOG_TAG, "Exception while reading html reply " + rte.toString());
480             }
481 
482             try {
483                 addTextBodyPart(mp, "text/plain", replyTag,
484                         EmailContent.Body.restoreReplyTextWithMessageId(context, localMessage.mId));
485             } catch (RuntimeException rte) {
486                 Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString());
487             }
488         }
489 
490         // Attachments
491         // TODO: Make sure we deal with these as structures and don't accidentally upload files
492 //        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
493 //        Cursor attachments = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
494 //                null, null, null);
495 //        try {
496 //
497 //        } finally {
498 //            attachments.close();
499 //        }
500 
501         return message;
502     }
503 
504     /**
505      * Helper method to add a body part for a given type of text, if found
506      *
507      * @param mp The text body part will be added to this multipart
508      * @param contentType The content-type of the text being added
509      * @param quotedPartTag If non-null, HEADER_ANDROID_BODY_QUOTED_PART will be set to this value
510      * @param partText The text to add.  If null, nothing happens
511      */
addTextBodyPart(MimeMultipart mp, String contentType, String quotedPartTag, String partText)512     private static void addTextBodyPart(MimeMultipart mp, String contentType, String quotedPartTag,
513             String partText) throws MessagingException {
514         if (partText == null) {
515             return;
516         }
517         TextBody body = new TextBody(partText);
518         MimeBodyPart bp = new MimeBodyPart(body, contentType);
519         if (quotedPartTag != null) {
520             bp.addHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART, quotedPartTag);
521         }
522         mp.addBodyPart(bp);
523     }
524 }
525