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