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