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