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