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