From 3f1ac4da947f426775c9546f2e37206f58ce1a6e Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Tue, 11 Aug 2009 15:02:57 -0700 Subject: [PATCH] Add code to handle IMAP/POP attachments. IMAP messages up to about 25k will be downloaded properly. * Move Store->Provider message rewrite code to a separate utility. * Add code to descend a Store message and write provider attachments. * Unit test basic IMAP attachment handler TODO: * handle large IMAP messages. * unit test for POP * unit test for large IMAP messages --- src/com/android/email/Controller.java | 16 +- src/com/android/email/LegacyConversions.java | 275 ++++++++++++++++++ .../android/email/MessagingController.java | 207 ++++--------- .../email/provider/AttachmentProvider.java | 14 +- .../android/email/LegacyConversionsTests.java | 173 +++++++++++ 5 files changed, 530 insertions(+), 155 deletions(-) create mode 100644 src/com/android/email/LegacyConversions.java create mode 100644 tests/src/com/android/email/LegacyConversionsTests.java diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index a90fb0c55..c14960473 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -435,10 +435,22 @@ public class Controller { public void loadAttachment(long attachmentId, long messageId, long accountId, final Result callback) { - Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); - File saveToFile = AttachmentProvider.getAttachmentFilename(mContext, accountId, attachmentId); + if (saveToFile.exists()) { + // The attachment has already been downloaded, so we will just "pretend" to download it + synchronized (mListeners) { + for (Result listener : mListeners) { + listener.loadAttachmentCallback(null, messageId, attachmentId, 0); + } + for (Result listener : mListeners) { + listener.loadAttachmentCallback(null, messageId, attachmentId, 100); + } + } + return; + } + + Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); // Split here for target type (Service or MessagingController) IEmailService service = getServiceForMessage(messageId); diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java new file mode 100644 index 000000000..83f584ee1 --- /dev/null +++ b/src/com/android/email/LegacyConversions.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email; + +import com.android.email.mail.Address; +import com.android.email.mail.Message; +import com.android.email.mail.MessagingException; +import com.android.email.mail.Part; +import com.android.email.mail.internet.MimeHeader; +import com.android.email.mail.internet.MimeUtility; +import com.android.email.provider.AttachmentProvider; +import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailContent.Attachment; +import com.android.email.provider.EmailContent.AttachmentColumns; + +import org.apache.commons.io.IOUtils; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; + +public class LegacyConversions { + + /** + * Copy field-by-field from a "store" message to a "provider" message + * @param message The message we've just downloaded + * @param localMessage The message we'd like to write into the DB + * @result true if dirty (changes were made) + */ + public static boolean updateMessageFields(EmailContent.Message localMessage, Message message, + long accountId, long mailboxId) throws MessagingException { + + Address[] from = message.getFrom(); + Address[] to = message.getRecipients(Message.RecipientType.TO); + Address[] cc = message.getRecipients(Message.RecipientType.CC); + Address[] bcc = message.getRecipients(Message.RecipientType.BCC); + Address[] replyTo = message.getReplyTo(); + String subject = message.getSubject(); + Date sentDate = message.getSentDate(); + + if (from != null && from.length > 0) { + localMessage.mDisplayName = from[0].toFriendly(); + } + if (sentDate != null) { + localMessage.mTimeStamp = sentDate.getTime(); + } + if (subject != null) { + localMessage.mSubject = subject; + } +// public String mPreview; +// public boolean mFlagRead = false; + + // Keep the message in the "unloaded" state until it has (at least) a display name. + // This prevents early flickering of empty messages in POP download. + if (localMessage.mFlagLoaded != EmailContent.Message.LOADED) { + if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) { + localMessage.mFlagLoaded = EmailContent.Message.NOT_LOADED; + } else { + localMessage.mFlagLoaded = EmailContent.Message.PARTIALLY_LOADED; + } + } + // TODO handle flags, favorites, and read/unread +// public boolean mFlagFavorite = false; +// public boolean mFlagAttachment = false; +// public int mFlags = 0; +// +// public String mTextInfo; +// public String mHtmlInfo; +// + localMessage.mServerId = message.getUid(); +// public int mServerIntId; +// public String mClientId; +// public String mMessageId; +// public String mThreadId; +// +// public long mBodyKey; + localMessage.mMailboxKey = mailboxId; + localMessage.mAccountKey = accountId; +// public long mReferenceKey; +// +// public String mSender; + if (from != null && from.length > 0) { + localMessage.mFrom = Address.pack(from); + } + + localMessage.mTo = Address.pack(to); + localMessage.mCc = Address.pack(cc); + localMessage.mBcc = Address.pack(bcc); + localMessage.mReplyTo = Address.pack(replyTo); + +// public String mServerVersion; +// +// public String mText; +// public String mHtml; +// +// // Can be used while building messages, but is NOT saved by the Provider +// transient public ArrayList mAttachments = null; + + return true; + } + + /** + * Copy body text (plain and/or HTML) from MimeMessage to provider Message + * + * TODO: Take a closer look at textInfo and deal with it if necessary. + */ + public static boolean updateBodyFields(EmailContent.Body body, + EmailContent.Message localMessage, ArrayList viewables) + throws MessagingException { + + body.mMessageKey = localMessage.mId; + + StringBuffer sbHtml = new StringBuffer(); + StringBuffer sbText = new StringBuffer(); + for (Part viewable : viewables) { + String text = MimeUtility.getTextFromPart(viewable); + if ("text/html".equalsIgnoreCase(viewable.getMimeType())) { + if (sbHtml.length() > 0) { + sbHtml.append('\n'); + } + sbHtml.append(text); + } else { + if (sbText.length() > 0) { + sbText.append('\n'); + } + sbText.append(text); + } + } + + // write the combined data to the body part + if (sbText.length() != 0) { + localMessage.mTextInfo = "X;X;8;" + sbText.length()*2; + body.mTextContent = sbText.toString(); + } + if (sbHtml.length() != 0) { + localMessage.mHtmlInfo = "X;X;8;" + sbHtml.length()*2; + body.mHtmlContent = sbHtml.toString(); + } + return true; + } + + /** + * Copy attachments from MimeMessage to provider Message. + * + * @param context a context for file operations + * @param localMessage the attachments will be built against this message + * @param message the original message from POP or IMAP, that may have attachments + * @return true if it succeeded + * @throws IOException + */ + public static void updateAttachments(Context context, EmailContent.Message localMessage, + ArrayList attachments) throws MessagingException, IOException { + localMessage.mAttachments = null; + for (Part attachmentPart : attachments) { + addOneAttachment(context, localMessage, attachmentPart); + } + } + + /** + * Add a single attachment part to the message + * + * TODO: This will simply add to any existing attachments - could this ever happen? If so, + * change it to find existing attachments and delete/merge them. + * TODO: Take a closer look at encoding and deal with it if necessary. + * + * @param context a context for file operations + * @param localMessage the attachments will be built against this message + * @param part a single attachment part from POP or IMAP + * @return true if it succeeded + * @throws IOException + */ + private static void addOneAttachment(Context context, EmailContent.Message localMessage, + Part part) throws MessagingException, IOException { + + Attachment localAttachment = new Attachment(); + + // Transfer fields from mime format to provider format + String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); + String name = MimeUtility.getHeaderParameter(contentType, "name"); + if (name == null) { + String contentDisposition = MimeUtility.unfoldAndDecode(part.getContentType()); + name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); + } + + // Try to pull size from disposition (if not downloaded) + long size = 0; + String disposition = part.getDisposition(); + if (disposition != null) { + String s = MimeUtility.getHeaderParameter(disposition, "size"); + if (s != null) { + size = Long.parseLong(s); + } + } + + // Get partId for unloaded IMAP attachments (if any) + // This is only provided (and used) when we have structure but not the actual attachment + String[] partIds = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); + String partId = partIds != null ? partIds[0] : null;; + + localAttachment.mFileName = MimeUtility.getHeaderParameter(contentType, "name"); + localAttachment.mMimeType = part.getMimeType(); + localAttachment.mSize = size; // May be reset below if file handled + localAttachment.mContentId = part.getContentId(); + localAttachment.mContentUri = null; // Will be set when file is saved + localAttachment.mMessageKey = localMessage.mId; + localAttachment.mLocation = partId; + localAttachment.mEncoding = "B"; // TODO - convert other known encodings + + // Save the attachment (so far) in order to obtain an id + localAttachment.save(context); + + // If an attachment body was actually provided, we need to write the file now + // TODO this should be separated so it can be reused for attachment downloads + if (part.getBody() != null) { + long attachmentId = localAttachment.mId; + long accountId = localMessage.mAccountKey; + + InputStream in = part.getBody().getInputStream(); + + File saveIn = AttachmentProvider.getAttachmentDirectory(context, accountId); + if (!saveIn.exists()) { + saveIn.mkdirs(); + } + File saveAs = AttachmentProvider.getAttachmentFilename(context, accountId, + attachmentId); + saveAs.createNewFile(); + FileOutputStream out = new FileOutputStream(saveAs); + long copySize = IOUtils.copy(in, out); + in.close(); + out.close(); + + // update the attachment with the extra information we now know + String contentUriString = AttachmentProvider.getAttachmentUri( + accountId, attachmentId).toString(); + + localAttachment.mSize = copySize; + localAttachment.mContentUri = contentUriString; + + // update the attachment in the database as well + ContentValues cv = new ContentValues(); + cv.put(AttachmentColumns.SIZE, copySize); + cv.put(AttachmentColumns.CONTENT_URI, contentUriString); + Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); + context.getContentResolver().update(uri, cv, null, null); + } + + if (localMessage.mAttachments == null) { + localMessage.mAttachments = new ArrayList(); + } + localMessage.mAttachments.add(localAttachment); + localMessage.mFlagAttachment = true; + } +} diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 627143b45..b49c3a98d 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -16,7 +16,6 @@ package com.android.email; -import com.android.email.mail.Address; import com.android.email.mail.FetchProfile; import com.android.email.mail.Flag; import com.android.email.mail.Folder; @@ -29,7 +28,6 @@ import com.android.email.mail.Store; import com.android.email.mail.StoreSynchronizer; import com.android.email.mail.Folder.FolderType; import com.android.email.mail.Folder.OpenMode; -import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.store.LocalStore; import com.android.email.mail.store.LocalStore.LocalFolder; @@ -46,9 +44,9 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Process; -import android.util.Config; import android.util.Log; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -781,7 +779,7 @@ public class MessagingController implements Runnable { if (localMessage != null) { try { // Copy the fields that are available into the message - updateMessageFields(localMessage, + LegacyConversions.updateMessageFields(localMessage, message, account.mId, folder.mId); // Commit the message to the local store saveOrUpdate(localMessage); @@ -904,6 +902,7 @@ public class MessagingController implements Runnable { // this is going to be inefficient and duplicate work we've already done. 2. It's going // back to the DB for a local message that we already had (and discarded). + // For small messages, we specify "body", which returns everything (incl. attachments) fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, @@ -934,36 +933,63 @@ public class MessagingController implements Runnable { c.close(); } } - - if (localMessage != null) { - EmailContent.Body body = EmailContent.Body.restoreBodyWithId( - mContext, localMessage.mId); - if (body == null) { - body = new EmailContent.Body(); - } - try { - // Copy the fields that are available into the message - updateMessageFields(localMessage, - message, account.mId, folder.mId); - updateBodyFields(body, localMessage, message); - // TODO should updateMessageFields do this for us? - // localMessage.mFlagLoaded = EmailContent.Message.LOADED; - // Commit the message to the local store - saveOrUpdate(localMessage); - saveOrUpdate(body); - } catch (MessagingException me) { - Log.e(Email.LOG_TAG, - "Error while copying downloaded message." + me); - } - + if (localMessage == null) { + Log.d(Email.LOG_TAG, "Could not retrieve message from db, UUID=" + + message.getUid()); + return; } - } - catch (Exception e) { + + EmailContent.Body body = EmailContent.Body.restoreBodyWithId( + mContext, localMessage.mId); + if (body == null) { + body = new EmailContent.Body(); + } + try { + // Copy the fields that are available into the message object + LegacyConversions.updateMessageFields(localMessage, message, + account.mId, folder.mId); + + // Now process body parts & attachments + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + LegacyConversions.updateBodyFields(body, localMessage, viewables); + + // Commit the message & body to the local store immediately + saveOrUpdate(localMessage); + saveOrUpdate(body); + + // process (and save) attachments + LegacyConversions.updateAttachments(mContext, localMessage, + attachments); + + // One last update of message with two updated flags + localMessage.mFlagLoaded = EmailContent.Message.LOADED; + + ContentValues cv = new ContentValues(); + cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, + localMessage.mFlagAttachment); + cv.put(EmailContent.MessageColumns.FLAG_LOADED, + localMessage.mFlagLoaded); + Uri uri = ContentUris.withAppendedId( + EmailContent.Message.CONTENT_URI, localMessage.mId); + mContext.getContentResolver().update(uri, cv, null, null); + + } catch (MessagingException me) { + Log.e(Email.LOG_TAG, + "Error while copying downloaded message." + me); + } + + } catch (RuntimeException rte) { Log.e(Email.LOG_TAG, - "Error while storing downloaded message." + e.toString()); + "Error while storing downloaded message." + rte.toString()); + } catch (IOException ioe) { + Log.e(Email.LOG_TAG, + "Error while storing attachment." + ioe.toString()); } } - + public void messageStarted(String uid, int number, int ofTotal) { } }); @@ -1113,127 +1139,6 @@ public class MessagingController implements Runnable { } - /** - * Copy field-by-field from a "store" message to a "provider" message - * @param message The message we've just downloaded - * @param localMessage The message we'd like to write into the DB - * @result true if dirty (changes were made) - */ - /* package */ boolean updateMessageFields(EmailContent.Message localMessage, Message message, - long accountId, long mailboxId) throws MessagingException { - - Address[] from = message.getFrom(); - Address[] to = message.getRecipients(Message.RecipientType.TO); - Address[] cc = message.getRecipients(Message.RecipientType.CC); - Address[] bcc = message.getRecipients(Message.RecipientType.BCC); - Address[] replyTo = message.getReplyTo(); - String subject = message.getSubject(); - Date sentDate = message.getSentDate(); - - if (from != null && from.length > 0) { - localMessage.mDisplayName = from[0].toFriendly(); - } - if (sentDate != null) { - localMessage.mTimeStamp = sentDate.getTime(); - } - if (subject != null) { - localMessage.mSubject = subject; - } -// public String mPreview; -// public boolean mFlagRead = false; - - // Keep the message in the "unloaded" state until it has (at least) a display name. - // This prevents early flickering of empty messages in POP download. - if (localMessage.mFlagLoaded != EmailContent.Message.LOADED) { - if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) { - localMessage.mFlagLoaded = EmailContent.Message.NOT_LOADED; - } else { - localMessage.mFlagLoaded = EmailContent.Message.PARTIALLY_LOADED; - } - } -// public boolean mFlagFavorite = false; -// public boolean mFlagAttachment = false; -// public int mFlags = 0; -// -// public String mTextInfo; -// public String mHtmlInfo; -// - localMessage.mServerId = message.getUid(); -// public int mServerIntId; -// public String mClientId; -// public String mMessageId; -// public String mThreadId; -// -// public long mBodyKey; - localMessage.mMailboxKey = mailboxId; - localMessage.mAccountKey = accountId; -// public long mReferenceKey; -// -// public String mSender; - if (from != null && from.length > 0) { - localMessage.mFrom = Address.pack(from); - } - - if (to != null && to.length > 0) { - localMessage.mTo = Address.pack(to); - } - if (cc != null && cc.length > 0) { - localMessage.mCc = Address.pack(cc); - } - if (bcc != null && bcc.length > 0) { - localMessage.mBcc = Address.pack(bcc); - } - if (replyTo != null && replyTo.length > 0) { - localMessage.mReplyTo = Address.pack(replyTo); - } -// -// public String mServerVersion; -// -// public String mText; -// public String mHtml; -// -// // Can be used while building messages, but is NOT saved by the Provider -// transient public ArrayList mAttachments = null; -// -// public static final int UNREAD = 0; -// public static final int READ = 1; -// public static final int DELETED = 2; -// -// public static final int NOT_LOADED = 0; -// public static final int LOADED = 1; -// public static final int PARTIALLY_LOADED = 2; - - return true; - } - - /** - * Copy body text (plain and/or HTML) from MimeMessage to provider Message - */ - /* package */ boolean updateBodyFields(EmailContent.Body body, - EmailContent.Message localMessage, Message message) throws MessagingException { - - body.mMessageKey = localMessage.mId; - - Part htmlPart = MimeUtility.findFirstPartByMimeType(message, "text/html"); - Part textPart = MimeUtility.findFirstPartByMimeType(message, "text/plain"); - - if (textPart != null) { - String text = MimeUtility.getTextFromPart(textPart); - if (text != null) { - localMessage.mTextInfo = "X;X;8;" + text.length()*2; - body.mTextContent = text; - } - } - if (htmlPart != null) { - String html = MimeUtility.getTextFromPart(htmlPart); - if (html != null) { - localMessage.mHtmlInfo = "X;X;8;" + html.length()*2; - body.mHtmlContent = html; - } - } - return true; - } - private void queuePendingCommand(EmailContent.Account account, PendingCommand command) { try { LocalStore localStore = (LocalStore) Store.getInstance( diff --git a/src/com/android/email/provider/AttachmentProvider.java b/src/com/android/email/provider/AttachmentProvider.java index 5617162d1..39472f0ed 100644 --- a/src/com/android/email/provider/AttachmentProvider.java +++ b/src/com/android/email/provider/AttachmentProvider.java @@ -103,8 +103,18 @@ public class AttachmentProvider extends ContentProvider { * the filename that should be used. */ public static File getAttachmentFilename(Context context, long accountId, long attachmentId) { - return new File( - context.getDatabasePath(accountId + ".db_att"), Long.toString(attachmentId)); + return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId)); + } + + /** + * Return the directory for a given attachment. This should be used by any code that is + * going to *write* attachments. + * + * This does not create or write the directory. It simply builds the pathname that should be + * used. + */ + public static File getAttachmentDirectory(Context context, long accountId) { + return context.getDatabasePath(accountId + ".db_att"); } @Override diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java new file mode 100644 index 000000000..b7b69abb9 --- /dev/null +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email; + +import com.android.email.mail.BodyPart; +import com.android.email.mail.Message; +import com.android.email.mail.MessageTestUtils; +import com.android.email.mail.MessagingException; +import com.android.email.mail.Part; +import com.android.email.mail.MessageTestUtils.MessageBuilder; +import com.android.email.mail.MessageTestUtils.MultipartBuilder; +import com.android.email.mail.internet.MimeHeader; +import com.android.email.mail.internet.MimeUtility; +import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; +import com.android.email.provider.EmailContent.Attachment; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.test.ProviderTestCase2; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Tests of the Legacy Conversions code (used by MessagingController). + * + * NOTE: It would probably make sense to rewrite this using a MockProvider, instead of the + * ProviderTestCase (which is a real provider running on a temp database). This would be more of + * a true "unit test". + * + * You can run this entire test case with: + * runtest -c com.android.email.LegacyConversionsTests email + */ +public class LegacyConversionsTests extends ProviderTestCase2 { + + EmailProvider mProvider; + Context mProviderContext; + Context mContext; + + public LegacyConversionsTests() { + super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mProviderContext = getMockContext(); + mContext = getContext(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + /** + * TODO: basic Legacy -> Provider Message conversions + * TODO: basic Legacy -> Provider Body conversions + * TODO: rainy day tests of all kinds + */ + + /** + * Sunny day test of adding attachments from an IMAP message. + */ + public void testAddAttachments() throws MessagingException, IOException { + // Prepare a local message to add the attachments to + final long accountId = 1; + final long mailboxId = 1; + final EmailContent.Message localMessage = ProviderTestUtils.setupMessage( + "local-message", accountId, mailboxId, false, true, mProviderContext); + + // Prepare a legacy message with attachments + Part attachment1Part = MessageTestUtils.bodyPart("image/gif", null); + attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + "image/gif;\n name=\"attachment1\""); + attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + attachment1Part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + "attachment;\n filename=\"attachment1\";\n size=100"); + attachment1Part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "101"); + + Part attachment2Part = MessageTestUtils.bodyPart("image/jpg", null); + attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + "image/jpg;\n name=\"attachment2\""); + attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + attachment2Part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + "attachment;\n filename=\"attachment2\";\n size=200"); + attachment2Part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "102"); + + final Message legacyMessage = new MessageBuilder() + .setBody(new MultipartBuilder("multipart/mixed") + .addBodyPart(MessageTestUtils.bodyPart("text/html", null)) + .addBodyPart(new MultipartBuilder("multipart/mixed") + .addBodyPart((BodyPart)attachment1Part) + .addBodyPart((BodyPart)attachment2Part) + .buildBodyPart()) + .build()) + .build(); + + // Now, convert from legacy to provider and see what happens + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(legacyMessage, viewables, attachments); + LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments); + + // Read back all attachments for message and check field values + Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); + Cursor c = mProviderContext.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, + null, null, null); + try { + assertEquals(2, c.getCount()); + while (c.moveToNext()) { + Attachment attachment = Attachment.getContent(c, Attachment.class); + if ("101".equals(attachment.mLocation)) { + checkAttachment("attachment1Part", attachment1Part, attachment); + } else if ("102".equals(attachment.mLocation)) { + checkAttachment("attachment2Part", attachment2Part, attachment); + } else { + fail("Unexpected attachment with location " + attachment.mLocation); + } + } + } finally { + c.close(); + } + } + + /** + * Compare attachment that was converted from Part (expected) to Provider Attachment (actual) + * + * TODO content URI should only be set if we also saved a file + * TODO other data encodings + */ + private void checkAttachment(String tag, Part expected, EmailContent.Attachment actual) + throws MessagingException { + String contentType = MimeUtility.unfoldAndDecode(expected.getContentType()); + String expectedName = MimeUtility.getHeaderParameter(contentType, "name"); + assertEquals(tag, expectedName, actual.mFileName); + assertEquals(tag, expected.getMimeType(), actual.mMimeType); + String disposition = expected.getDisposition(); + String sizeString = MimeUtility.getHeaderParameter(disposition, "size"); + long expectedSize = Long.parseLong(sizeString); + assertEquals(tag, expectedSize, actual.mSize); + assertEquals(tag, expected.getContentId(), actual.mContentId); + assertNull(tag, actual.mContentUri); + assertTrue(tag, 0 != actual.mMessageKey); + String expectedPartId = + expected.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0]; + assertEquals(tag, expectedPartId, actual.mLocation); + assertEquals(tag, "B", actual.mEncoding); + } + + /** + * TODO: Sunny day test of adding attachments from a POP message. + */ + +}