diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java index 02e473280..6e2931bb0 100644 --- a/src/com/android/email/LegacyConversions.java +++ b/src/com/android/email/LegacyConversions.java @@ -21,8 +21,13 @@ import com.android.email.mail.Flag; import com.android.email.mail.Message; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; +import com.android.email.mail.Message.RecipientType; +import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; +import com.android.email.mail.internet.MimeMessage; +import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.MimeUtility; +import com.android.email.mail.internet.TextBody; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Attachment; @@ -34,6 +39,7 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.net.Uri; +import android.util.Log; import java.io.File; import java.io.FileOutputStream; @@ -44,9 +50,16 @@ import java.util.Date; public class LegacyConversions { + /** + * Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts + */ + /* package */ static final String BODY_QUOTED_PART_REPLY = "quoted-reply"; + /* package */ static final String BODY_QUOTED_PART_FORWARD = "quoted-forward"; + /* package */ static final String BODY_QUOTED_PART_INTRO = "quoted-intro"; + /** * Copy field-by-field from a "store" message to a "provider" message - * @param message The message we've just downloaded + * @param message The message we've just downloaded (must be a MimeMessage) * @param localMessage The message we'd like to write into the DB * @result true if dirty (changes were made) */ @@ -91,7 +104,7 @@ public class LegacyConversions { localMessage.mServerTimeStamp = internalDate.getTime(); } // public String mClientId; -// public String mMessageId; + localMessage.mMessageId = ((MimeMessage)message).getMessageId(); // public long mBodyKey; localMessage.mMailboxKey = mailboxId; @@ -126,33 +139,90 @@ public class LegacyConversions { body.mMessageKey = localMessage.mId; - StringBuffer sbHtml = new StringBuffer(); - StringBuffer sbText = new StringBuffer(); + StringBuffer sbHtml = null; + StringBuffer sbText = null; + StringBuffer sbHtmlReply = null; + StringBuffer sbTextReply = null; + StringBuffer sbIntroText = null; + for (Part viewable : viewables) { String text = MimeUtility.getTextFromPart(viewable); - if ("text/html".equalsIgnoreCase(viewable.getMimeType())) { - if (sbHtml.length() > 0) { - sbHtml.append('\n'); + String[] replyTags = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART); + String replyTag = null; + if (replyTags != null && replyTags.length > 0) { + replyTag = replyTags[0]; + } + // Deploy text as marked by the various tags + boolean isHtml = "text/html".equalsIgnoreCase(viewable.getMimeType()); + + if (replyTag != null) { + boolean isQuotedReply = BODY_QUOTED_PART_REPLY.equalsIgnoreCase(replyTag); + boolean isQuotedForward = BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(replyTag); + boolean isQuotedIntro = BODY_QUOTED_PART_INTRO.equalsIgnoreCase(replyTag); + + if (isQuotedReply || isQuotedForward) { + if (isHtml) { + sbHtmlReply = appendTextPart(sbHtmlReply, text); + } else { + sbTextReply = appendTextPart(sbTextReply, text); + } + // Set message flags as well + localMessage.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; + localMessage.mFlags |= isQuotedReply + ? EmailContent.Message.FLAG_TYPE_REPLY + : EmailContent.Message.FLAG_TYPE_FORWARD; + continue; } - sbHtml.append(text); + if (isQuotedIntro) { + sbIntroText = appendTextPart(sbIntroText, text); + continue; + } + } + + // Most of the time, just process regular body parts + if (isHtml) { + sbHtml = appendTextPart(sbHtml, text); } else { - if (sbText.length() > 0) { - sbText.append('\n'); - } - sbText.append(text); + sbText = appendTextPart(sbText, text); } } // write the combined data to the body part - if (sbText.length() != 0) { + if (sbText != null && sbText.length() != 0) { body.mTextContent = sbText.toString(); } - if (sbHtml.length() != 0) { + if (sbHtml != null && sbHtml.length() != 0) { body.mHtmlContent = sbHtml.toString(); } + if (sbHtmlReply != null && sbHtmlReply.length() != 0) { + body.mHtmlReply = sbHtmlReply.toString(); + } + if (sbTextReply != null && sbTextReply.length() != 0) { + body.mTextReply = sbTextReply.toString(); + } + if (sbIntroText != null && sbIntroText.length() != 0) { + body.mIntroText = sbIntroText.toString(); + } return true; } + /** + * Helper function to append text to a StringBuffer, creating it if necessary. + * Optimization: The majority of the time we are *not* appending - we should have a path + * that deals with single strings. + */ + private static StringBuffer appendTextPart(StringBuffer sb, String newText) { + if (sb == null) { + sb = new StringBuffer(newText); + } else { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(newText); + } + return sb; + } + /** * Copy attachments from MimeMessage to provider Message. * @@ -269,4 +339,116 @@ public class LegacyConversions { } } + /** + * Read a complete Provider message into a legacy message (for IMAP upload). This + * is basically the equivalent of LocalFolder.getMessages() + LocalFolder.fetch(). + */ + public static Message makeMessage(Context context, EmailContent.Message localMessage) + throws MessagingException { + MimeMessage message = new MimeMessage(); + + // LocalFolder.getMessages() equivalent: Copy message fields + message.setSubject(localMessage.mSubject == null ? "" : localMessage.mSubject); + Address[] from = Address.unpack(localMessage.mFrom); + if (from.length > 0) { + message.setFrom(from[0]); + } + message.setSentDate(new Date(localMessage.mTimeStamp)); + message.setUid(localMessage.mServerId); + message.setFlag(Flag.DELETED, + localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_DELETED); + message.setFlag(Flag.SEEN, localMessage.mFlagRead); + message.setFlag(Flag.FLAGGED, localMessage.mFlagFavorite); +// message.setFlag(Flag.DRAFT, localMessage.mMailboxKey == draftMailboxKey); + message.setRecipients(RecipientType.TO, Address.unpack(localMessage.mTo)); + message.setRecipients(RecipientType.CC, Address.unpack(localMessage.mCc)); + message.setRecipients(RecipientType.BCC, Address.unpack(localMessage.mBcc)); + message.setReplyTo(Address.unpack(localMessage.mReplyTo)); + message.setInternalDate(new Date(localMessage.mServerTimeStamp)); + message.setMessageId(localMessage.mMessageId); + + // LocalFolder.fetch() equivalent: build body parts + message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); + MimeMultipart mp = new MimeMultipart(); + mp.setSubType("mixed"); + message.setBody(mp); + + try { + addTextBodyPart(mp, "text/html", null, + EmailContent.Body.restoreBodyHtmlWithMessageId(context, localMessage.mId)); + } catch (RuntimeException rte) { + Log.d(Email.LOG_TAG, "Exception while reading html body " + rte.toString()); + } + + try { + addTextBodyPart(mp, "text/plain", null, + EmailContent.Body.restoreBodyTextWithMessageId(context, localMessage.mId)); + } catch (RuntimeException rte) { + Log.d(Email.LOG_TAG, "Exception while reading text body " + rte.toString()); + } + + boolean isReply = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_REPLY) != 0; + boolean isForward = (localMessage.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0; + + // If there is a quoted part (forwarding or reply), add the intro first, and then the + // rest of it. If it is opened in some other viewer, it will (hopefully) be displayed in + // the same order as we've just set up the blocks: composed text, intro, replied text + if (isReply || isForward) { + try { + addTextBodyPart(mp, "text/plain", BODY_QUOTED_PART_INTRO, + EmailContent.Body.restoreIntroTextWithMessageId(context, localMessage.mId)); + } catch (RuntimeException rte) { + Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString()); + } + + String replyTag = isReply ? BODY_QUOTED_PART_REPLY : BODY_QUOTED_PART_FORWARD; + try { + addTextBodyPart(mp, "text/html", replyTag, + EmailContent.Body.restoreReplyHtmlWithMessageId(context, localMessage.mId)); + } catch (RuntimeException rte) { + Log.d(Email.LOG_TAG, "Exception while reading html reply " + rte.toString()); + } + + try { + addTextBodyPart(mp, "text/plain", replyTag, + EmailContent.Body.restoreReplyTextWithMessageId(context, localMessage.mId)); + } catch (RuntimeException rte) { + Log.d(Email.LOG_TAG, "Exception while reading text reply " + rte.toString()); + } + } + + // Attachments + // TODO: Make sure we deal with these as structures and don't accidentally upload files +// Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); +// Cursor attachments = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, +// null, null, null); +// try { +// +// } finally { +// attachments.close(); +// } + + return message; + } + + /** + * Helper method to add a body part for a given type of text, if found + * + * @param mp The text body part will be added to this multipart + * @param contentType The content-type of the text being added + * @param quotedPartTag If non-null, HEADER_ANDROID_BODY_QUOTED_PART will be set to this value + * @param partText The text to add. If null, nothing happens + */ + private static void addTextBodyPart(MimeMultipart mp, String contentType, String quotedPartTag, + String partText) throws MessagingException { + if (partText == null) { + return; + } + TextBody body = new TextBody(partText); + MimeBodyPart bp = new MimeBodyPart(body, contentType); + if (quotedPartTag != null) { + bp.addHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART, quotedPartTag); + } + mp.addBodyPart(bp); + } } diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 74d82f95c..0d21ed161 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -33,10 +33,6 @@ import com.android.email.mail.internet.MimeBodyPart; import com.android.email.mail.internet.MimeHeader; import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.MimeUtility; -import com.android.email.mail.store.LocalStore; -import com.android.email.mail.store.LocalStore.LocalFolder; -import com.android.email.mail.store.LocalStore.LocalMessage; -import com.android.email.mail.store.LocalStore.PendingCommand; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Attachment; @@ -77,6 +73,7 @@ import java.util.concurrent.LinkedBlockingQueue; * removed from the queue once the activity is no longer active. */ public class MessagingController implements Runnable { + /** * The maximum message size that we'll consider to be "small". A small message is downloaded * in full immediately instead of in pieces. Anything over this size will be downloaded in @@ -100,6 +97,11 @@ public class MessagingController implements Runnable { private static Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; private static Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; + /** + * We write this into the serverId field of messages that will never be upsynced. + */ + private static final String LOCAL_SERVERID_PREFIX = "Local-"; + /** * Projections & CVs used by pruneCachedAttachments */ @@ -415,10 +417,7 @@ public class MessagingController implements Runnable { StoreSynchronizer.SyncResults results; // Select generic sync or store-specific sync - final LocalStore localStore = - (LocalStore) Store.getInstance(account.getLocalStoreUri(mContext), mContext, null); - Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, - localStore.getPersistentCallbacks()); + Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, null); StoreSynchronizer customSync = remoteStore.getMessageSynchronizer(); if (customSync == null) { results = synchronizeMailboxGeneric(account, folder); @@ -497,6 +496,12 @@ public class MessagingController implements Runnable { Log.d(Email.LOG_TAG, "*** synchronizeMailboxGeneric ***"); ContentResolver resolver = mContext.getContentResolver(); + // 0. We do not ever sync DRAFTS or OUTBOX (down or up) + if (folder.mType == Mailbox.TYPE_DRAFTS || folder.mType == Mailbox.TYPE_OUTBOX) { + int totalMessages = EmailContent.count(mContext, folder.getUri(), null, null); + return new StoreSynchronizer.SyncResults(totalMessages, 0); + } + // 1. Get the message list from the local store and create an index of the uids Cursor localUidCursor = null; @@ -1069,10 +1074,10 @@ public class MessagingController implements Runnable { * Handles: * Read/Unread * Flagged + * Append (upload) * Move To Trash * Empty trash * TODO: - * Append * Move * * @param account the account to scan for pending actions @@ -1086,6 +1091,9 @@ public class MessagingController implements Runnable { // Handle deletes first, it's always better to get rid of things first processPendingDeletesSynchronous(account, resolver, accountIdArgs); + // Handle uploads (currently, only to sent messages) + processPendingUploadsSynchronous(account, resolver, accountIdArgs); + // Now handle updates / upsyncs processPendingUpdatesSynchronous(account, resolver, accountIdArgs); } @@ -1154,6 +1162,109 @@ public class MessagingController implements Runnable { } } + /** + * Scan for messages that are in Sent, and are in need of upload, + * and send them to the server. "In need of upload" is defined as: + * serverId == null (no UID has been assigned) + * or + * message is in the updated list + * + * Note we also look for messages that are moving from drafts->outbox->sent. They never + * go through "drafts" or "outbox" on the server, so we hang onto these until they can be + * uploaded directly to the Sent folder. + * + * @param account + * @param resolver + * @param accountIdArgs + */ + private void processPendingUploadsSynchronous(EmailContent.Account account, + ContentResolver resolver, String[] accountIdArgs) throws MessagingException { + // Find the Sent folder (since that's all we're uploading for now + Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, + MailboxColumns.ACCOUNT_KEY + "=?" + + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT, + accountIdArgs, null); + long lastMessageId = -1; + try { + // Defer setting up the store until we know we need to access it + Store remoteStore = null; + while (mailboxes.moveToNext()) { + long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN); + String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) }; + // Demand load mailbox + Mailbox mailbox = null; + + // First handle the "new" messages (serverId == null) + Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI, + EmailContent.Message.ID_PROJECTION, + EmailContent.Message.MAILBOX_KEY + "=?" + + " and (" + EmailContent.Message.SERVER_ID + " is null" + + " or " + EmailContent.Message.SERVER_ID + "=''" + ")", + mailboxKeyArgs, + null); + try { + while (upsyncs1.moveToNext()) { + // Load the remote store if it will be needed + if (remoteStore == null) { + remoteStore = + Store.getInstance(account.getStoreUri(mContext), mContext, null); + } + // Load the mailbox if it will be needed + if (mailbox == null) { + mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); + } + // upsync the message + long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); + lastMessageId = id; + processUploadMessage(resolver, remoteStore, account, mailbox, id); + } + } finally { + if (upsyncs1 != null) { + upsyncs1.close(); + } + } + + // Next, handle any updates (e.g. edited in place, although this shouldn't happen) + Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, + EmailContent.Message.ID_PROJECTION, + EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs, + null); + try { + while (upsyncs2.moveToNext()) { + // Load the remote store if it will be needed + if (remoteStore == null) { + remoteStore = + Store.getInstance(account.getStoreUri(mContext), mContext, null); + } + // Load the mailbox if it will be needed + if (mailbox == null) { + mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); + } + // upsync the message + long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); + lastMessageId = id; + processUploadMessage(resolver, remoteStore, account, mailbox, id); + } + } finally { + if (upsyncs2 != null) { + upsyncs2.close(); + } + } + } + } catch (MessagingException me) { + // Presumably an error here is an account connection failure, so there is + // no point in continuing through the rest of the pending updates. + if (Email.DEBUG) { + Log.d(Email.LOG_TAG, "Unable to process pending upsync for id=" + + lastMessageId + ": " + me); + } + } finally { + if (mailboxes != null) { + mailboxes.close(); + } + } + } + /** * Scan for messages that are in the Message_Updates table, look for differences that * we can deal with, and do the work. @@ -1228,6 +1339,53 @@ public class MessagingController implements Runnable { } } + /** + * Upsync an entire message. This must also unwind whatever triggered it (either by + * updating the serverId, or by deleting the update record, or it's going to keep happening + * over and over again. + * + * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload. + * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select + * only the Drafts and Sent folders, this can happen when the update record and the current + * record mismatch. In this case, we let the update record remain, because the filters + * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it) + * appropriately. + * + * @param resolver + * @param remoteStore + * @param account + * @param mailbox the actual mailbox + * @param messageId + */ + private void processUploadMessage(ContentResolver resolver, Store remoteStore, + EmailContent.Account account, Mailbox mailbox, long messageId) + throws MessagingException { + EmailContent.Message message = + EmailContent.Message.restoreMessageWithId(mContext, messageId); + boolean deleteUpdate = false; + if (message == null) { + deleteUpdate = true; + Log.d(Email.LOG_TAG, "Upsync failed for null message, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { + deleteUpdate = false; + Log.d(Email.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { + deleteUpdate = false; + Log.d(Email.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_TRASH) { + deleteUpdate = false; + Log.d(Email.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); + } else { + Log.d(Email.LOG_TAG, "Upsyc triggered for message id=" + messageId); + deleteUpdate = processPendingAppend(remoteStore, account, mailbox, message); + } + if (deleteUpdate) { + // Finally, delete the update (if any) + Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, messageId); + resolver.delete(uri, null, null); + } + } + /** * Upsync changes to read or flagged * @@ -1239,6 +1397,19 @@ public class MessagingController implements Runnable { */ private void processPendingFlagChange(Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, EmailContent.Message newMessage) throws MessagingException { + + // 0. No remote update if the message is local-only + if (newMessage.mServerId == null || newMessage.mServerId.equals("") + || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { + return; + } + + // 1. No remote update for DRAFTS or OUTBOX + if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { + return; + } + + // 2. Open the remote store & folder Folder remoteFolder = remoteStore.getFolder(mailbox.mDisplayName); if (!remoteFolder.exists()) { return; @@ -1247,7 +1418,8 @@ public class MessagingController implements Runnable { if (remoteFolder.getMode() != OpenMode.READ_WRITE) { return; } - // Finally, apply the changes to the message + + // 3. Finally, apply the changes to the message Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); if (remoteMessage == null) { return; @@ -1280,6 +1452,12 @@ public class MessagingController implements Runnable { EmailContent.Account account, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage) throws MessagingException { + // 0. No remote move if the message is local-only + if (newMessage.mServerId == null || newMessage.mServerId.equals("") + || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { + return; + } + // 1. Escape early if we can't find the local mailbox // TODO smaller projection here Mailbox oldMailbox = Mailbox.restoreMailboxWithId(mContext, oldMessage.mMailboxKey); @@ -1427,96 +1605,131 @@ public class MessagingController implements Runnable { /** * Process a pending append message command. This command uploads a local message to the * server, first checking to be sure that the server message is not newer than - * the local message. Once the local message is successfully processed it is deleted so - * that the server message will be synchronized down without an additional copy being - * created. - * TODO update the local message UID instead of deleteing it + * the local message. * - * @param command arguments = (String folder, String uid) - * @param account - * @throws MessagingException + * @param remoteStore the remote store we're working in + * @param account The account in which we are working + * @param newMailbox The mailbox we're appending to + * @param message The message we're appending + * @return true if successfully uploaded */ - private void processPendingAppend(PendingCommand command, EmailContent.Account account) + private boolean processPendingAppend(Store remoteStore, EmailContent.Account account, + Mailbox newMailbox, EmailContent.Message message) throws MessagingException { - String folder = command.arguments[0]; - String uid = command.arguments[1]; - LocalStore localStore = (LocalStore) Store.getInstance( - account.getLocalStoreUri(mContext), mContext, null); - LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); - LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); + boolean updateInternalDate = false; + boolean updateMessage = false; + boolean deleteMessage = false; - if (localMessage == null) { - return; - } - - Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, - localStore.getPersistentCallbacks()); - Folder remoteFolder = remoteStore.getFolder(folder); + // 1. Find the remote folder that we're appending to and create and/or open it + Folder remoteFolder = remoteStore.getFolder(newMailbox.mDisplayName); if (!remoteFolder.exists()) { + if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) { + // This is POP3, we cannot actually upload. Instead, we'll update the message + // locally with a fake serverId (so we don't keep trying here) and return. + if (message.mServerId == null || message.mServerId.length() == 0) { + message.mServerId = LOCAL_SERVERID_PREFIX + message.mId; + Uri uri = + ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); + ContentValues cv = new ContentValues(); + cv.put(EmailContent.Message.SERVER_ID, message.mServerId); + mContext.getContentResolver().update(uri, cv, null, null); + } + return true; + } if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - return; + // This is a (hopefully) transient error and we return false to try again later + return false; } } - remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); + remoteFolder.open(OpenMode.READ_WRITE, null); if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return; + return false; } + // 2. If possible, load a remote message with the matching UID Message remoteMessage = null; - if (!localMessage.getUid().startsWith("Local") - && !localMessage.getUid().contains("-")) { - remoteMessage = remoteFolder.getMessage(localMessage.getUid()); + if (message.mServerId != null && message.mServerId.length() > 0) { + remoteMessage = remoteFolder.getMessage(message.mServerId); } + // 3. If a remote message could not be found, upload our local message if (remoteMessage == null) { - /* - * If the message does not exist remotely we just upload it and then - * update our local copy with the new uid. - */ + // 3a. Create a legacy message to upload + Message localMessage = LegacyConversions.makeMessage(mContext, message); + + // 3b. Upload it FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { localMessage }, fp, null); - String oldUid = localMessage.getUid(); remoteFolder.appendMessages(new Message[] { localMessage }); - localFolder.changeUid(localMessage); -// mListeners.messageUidChanged(account.mId, -1 folder.mId, oldUid, localMessage.getUid()); - } - else { - /* - * If the remote message exists we need to determine which copy to keep. - */ - /* - * See if the remote message is newer than ours. - */ + + // 3b. And record the UID from the server + message.mServerId = localMessage.getUid(); + updateInternalDate = true; + updateMessage = true; + } else { + // 4. If the remote message exists we need to determine which copy to keep. FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.ENVELOPE); remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); - Date localDate = localMessage.getInternalDate(); + Date localDate = new Date(message.mServerTimeStamp); Date remoteDate = remoteMessage.getInternalDate(); if (remoteDate.compareTo(localDate) > 0) { - /* - * If the remote message is newer than ours we'll just - * delete ours and move on. A sync will get the server message - * if we need to be able to see it. - */ - localMessage.setFlag(Flag.DELETED, true); - } - else { - /* - * Otherwise we'll upload our message and then delete the remote message. - */ + // 4a. If the remote message is newer than ours we'll just + // delete ours and move on. A sync will get the server message + // if we need to be able to see it. + deleteMessage = true; + } else { + // 4b. Otherwise we'll upload our message and then delete the remote message. + + // Create a legacy message to upload + Message localMessage = LegacyConversions.makeMessage(mContext, message); + + // 4c. Upload it fp.clear(); fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); - localFolder.fetch(new Message[] { localMessage }, fp, null); - String oldUid = localMessage.getUid(); remoteFolder.appendMessages(new Message[] { localMessage }); - localFolder.changeUid(localMessage); -// mListeners.messageUidChanged(account.mId, folder.mId, oldUid, localMessage.getUid()); + + // 4d. Record the UID and new internalDate from the server + message.mServerId = localMessage.getUid(); + updateInternalDate = true; + updateMessage = true; + + // 4e. And delete the old copy of the message from the server remoteMessage.setFlag(Flag.DELETED, true); } } + + // 5. If requested, Best-effort to capture new "internaldate" from the server + if (updateInternalDate) { + try { + Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); + FetchProfile fp2 = new FetchProfile(); + fp2.add(FetchProfile.Item.ENVELOPE); + remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); + message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime(); + updateMessage = true; + } catch (MessagingException me) { + // skip it - we can live without this + } + } + + // 6. Perform required edits to local copy of message + if (deleteMessage || updateMessage) { + Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); + ContentResolver resolver = mContext.getContentResolver(); + if (deleteMessage) { + resolver.delete(uri, null, null); + } else if (updateMessage) { + ContentValues cv = new ContentValues(); + cv.put(EmailContent.Message.SERVER_ID, message.mServerId); + cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp); + resolver.update(uri, cv, null, null); + } + } + + return true; } /** @@ -1791,13 +2004,16 @@ public class MessagingController implements Runnable { continue; } // 5. move to sent, or delete - Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); + Uri syncedUri = + ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); if (requireMoveMessageToSentFolder) { - resolver.update(uri, moveToSentValues, null, null); - // TODO: post for a pending upload + resolver.update(syncedUri, moveToSentValues, null, null); } else { AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, messageId); + Uri uri = + ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); resolver.delete(uri, null, null); + resolver.delete(syncedUri, null, null); } } // 6. report completion/success @@ -1859,34 +2075,6 @@ public class MessagingController implements Runnable { }); } - public void saveDraft(final EmailContent.Account account, final Message message) { - // TODO rewrite using provider upates - -// try { -// Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, -// null); -// LocalFolder localFolder = -// (LocalFolder) localStore.getFolder(account.getDraftsFolderName(mContext)); -// localFolder.open(OpenMode.READ_WRITE, null); -// localFolder.appendMessages(new Message[] { -// message -// }); -// Message localMessage = localFolder.getMessage(message.getUid()); -// localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); -// -// PendingCommand command = new PendingCommand(); -// command.command = PENDING_COMMAND_APPEND; -// command.arguments = new String[] { -// localFolder.getName(), -// localMessage.getUid() }; -// queuePendingCommand(account, command); -// processPendingCommands(account); -// } -// catch (MessagingException e) { -// Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); -// } - } - private static class Command { public Runnable runnable; diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 493ed5496..16d63e197 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -36,6 +36,7 @@ import com.android.email.provider.EmailContent.MessageColumns; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -529,9 +530,9 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus // the reply fields are only filled/used for Drafts. if (isEditDraft) { message.mHtmlReply = - Body.restoreHtmlReplyWithMessageId(MessageCompose.this, message.mId); + Body.restoreReplyHtmlWithMessageId(MessageCompose.this, message.mId); message.mTextReply = - Body.restoreTextReplyWithMessageId(MessageCompose.this, message.mId); + Body.restoreReplyTextWithMessageId(MessageCompose.this, message.mId); } else { message.mHtmlReply = null; message.mTextReply = null; @@ -749,7 +750,12 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus protected Void doInBackground(Void... params) { synchronized (mDraft) { if (mDraft.isSaved()) { - mDraft.update(MessageCompose.this, getUpdateContentValues(mDraft)); + // Update the message + Uri draftUri = + ContentUris.withAppendedId(mDraft.SYNCED_CONTENT_URI, mDraft.mId); + getContentResolver().update(draftUri, getUpdateContentValues(mDraft), + null, null); + // Update the body ContentValues values = new ContentValues(); values.put(BodyColumns.TEXT_CONTENT, mDraft.mText); values.put(BodyColumns.TEXT_REPLY, mDraft.mTextReply); diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index b2d70863c..2ba3d7c73 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -780,7 +780,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On * * Here are some rules (finish this list): * - * Any merged box (except send): refresh + * Any merged, synced box (except send): refresh * Any push-mode account: refresh * Any non-push-mode account: load more * Any outbox (send again): @@ -791,11 +791,14 @@ public class MessageList extends ListActivity implements OnItemClickListener, On // first, look for shortcuts that don't need us to spin up a DB access task if (mailboxId == Mailbox.QUERY_ALL_INBOXES || mailboxId == Mailbox.QUERY_ALL_UNREAD - || mailboxId == Mailbox.QUERY_ALL_FAVORITES - || mailboxId == Mailbox.QUERY_ALL_DRAFTS) { + || mailboxId == Mailbox.QUERY_ALL_FAVORITES) { finishFooterView(LIST_FOOTER_MODE_REFRESH); return; } + if (mailboxId == Mailbox.QUERY_ALL_DRAFTS || mailboxType == Mailbox.TYPE_DRAFTS) { + finishFooterView(LIST_FOOTER_MODE_NONE); + return; + } if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) { finishFooterView(LIST_FOOTER_MODE_SEND); return; @@ -839,8 +842,11 @@ public class MessageList extends ListActivity implements OnItemClickListener, On return LIST_FOOTER_MODE_NONE; } } - if (mailboxType == Mailbox.TYPE_OUTBOX) { - return LIST_FOOTER_MODE_SEND; + switch (mailboxType) { + case Mailbox.TYPE_OUTBOX: + return LIST_FOOTER_MODE_SEND; + case Mailbox.TYPE_DRAFTS: + return LIST_FOOTER_MODE_NONE; } if (accountId != -1) { // This is inefficient but the best fix is not here but in isMessagingController diff --git a/src/com/android/email/mail/Folder.java b/src/com/android/email/mail/Folder.java index 3ba33b975..a0bbbf455 100644 --- a/src/com/android/email/mail/Folder.java +++ b/src/com/android/email/mail/Folder.java @@ -74,6 +74,19 @@ public abstract class Folder { */ public abstract OpenMode getMode() throws MessagingException; + /** + * Reports if the Store is able to create folders of the given type. + * Does not actually attempt to create a folder. + * @param type + * @return true if can create, false if cannot create + */ + public abstract boolean canCreate(FolderType type); + + /** + * Attempt to create the given folder remotely using the given type. + * @param type + * @return true if created, false if cannot create (e.g. server side) + */ public abstract boolean create(FolderType type) throws MessagingException; public abstract boolean exists() throws MessagingException; diff --git a/src/com/android/email/mail/internet/MimeHeader.java b/src/com/android/email/mail/internet/MimeHeader.java index ab21faa4c..8278264ae 100644 --- a/src/com/android/email/mail/internet/MimeHeader.java +++ b/src/com/android/email/mail/internet/MimeHeader.java @@ -16,15 +16,14 @@ package com.android.email.mail.internet; +import com.android.email.Utility; +import com.android.email.mail.MessagingException; + import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; -import java.util.regex.Pattern; - -import com.android.email.Utility; -import com.android.email.mail.MessagingException; public class MimeHeader { /** @@ -35,6 +34,10 @@ public class MimeHeader { * into the MIME data by LocalStore.fetch. */ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData"; + /** + * Application specific header that is used to tag body parts for quoted/forwarded messages. + */ + public static final String HEADER_ANDROID_BODY_QUOTED_PART = "X-Android-Body-Quoted-Part"; public static final String HEADER_CONTENT_TYPE = "Content-Type"; public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; @@ -143,4 +146,9 @@ public class MimeHeader { return name + "=" + value; } } + + @Override + public String toString() { + return (mFields == null) ? null : mFields.toString(); + } } diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index cd966bf71..af6e7d128 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -447,6 +447,11 @@ public class ImapStore extends Store { } } + // IMAP supports folder creation + public boolean canCreate(FolderType type) { + return true; + } + public boolean create(FolderType type) throws MessagingException { /* * This method needs to operate in the unselected mode as well as the selected mode @@ -1008,7 +1013,8 @@ public class ImapStore extends Store { /* * Try to find the UID of the message we just appended using the - * Message-ID header. + * Message-ID header. If there are more than one response, take the + * last one, as it's most likely he newest (the one we just uploaded). */ String[] messageIdHeader = message.getHeader("Message-ID"); if (messageIdHeader == null || messageIdHeader.length == 0) { @@ -1021,7 +1027,7 @@ public class ImapStore extends Store { for (ImapResponse response1 : responses) { if (response1.mTag == null && response1.get(0).equals("SEARCH") && response1.size() > 1) { - message.setUid(response1.getString(1)); + message.setUid(response1.getString(response1.size()-1)); } } diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java index 5437db29a..b3fab9ef5 100644 --- a/src/com/android/email/mail/store/LocalStore.java +++ b/src/com/android/email/mail/store/LocalStore.java @@ -37,7 +37,6 @@ import com.android.email.mail.internet.MimeMessage; import com.android.email.mail.internet.MimeMultipart; import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.internet.TextBody; -import com.android.email.provider.AttachmentProvider; import org.apache.commons.io.IOUtils; @@ -46,7 +45,6 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -import android.util.Config; import android.util.Log; import java.io.ByteArrayInputStream; @@ -629,6 +627,12 @@ public class LocalStore extends Store implements PersistentDataCallbacks { return Utility.arrayContains(getPersonalNamespaces(), this); } + // LocalStore supports folder creation + @Override + public boolean canCreate(FolderType type) { + return true; + } + @Override public boolean create(FolderType type) throws MessagingException { if (exists()) { diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java index 36d09da3a..da3b26e2a 100644 --- a/src/com/android/email/mail/store/Pop3Store.java +++ b/src/com/android/email/mail/store/Pop3Store.java @@ -29,7 +29,6 @@ import com.android.email.mail.Store; import com.android.email.mail.Transport; import com.android.email.mail.Folder.OpenMode; import com.android.email.mail.internet.MimeMessage; -import com.android.email.mail.store.ImapStore.ImapMessage; import com.android.email.mail.transport.LoggingInputStream; import com.android.email.mail.transport.MailTransport; @@ -332,6 +331,11 @@ public class Pop3Store extends Store { return mName; } + // POP3 does not folder creation + public boolean canCreate(FolderType type) { + return false; + } + @Override public boolean create(FolderType type) throws MessagingException { return false; diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index a4752b889..41eef51b0 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -195,7 +195,6 @@ public abstract class EmailContent { public static final String TABLE_NAME = "Body"; public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body"); - public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_MESSAGE_KEY_COLUMN = 1; public static final int CONTENT_HTML_CONTENT_COLUMN = 2; @@ -210,23 +209,22 @@ public abstract class EmailContent { BodyColumns.INTRO_TEXT }; - public static final String[] TEXT_PROJECTION = new String[] { + public static final String[] COMMON_PROJECTION_TEXT = new String[] { RECORD_ID, BodyColumns.TEXT_CONTENT }; - - public static final String[] HTML_PROJECTION = new String[] { + public static final String[] COMMON_PROJECTION_HTML = new String[] { RECORD_ID, BodyColumns.HTML_CONTENT }; - - public static final String[] HTML_REPLY_PROJECTION = new String[] { - RECORD_ID, BodyColumns.HTML_REPLY - }; - - public static final String[] TEXT_REPLY_PROJECTION = new String[] { + public static final String[] COMMON_PROJECTION_REPLY_TEXT = new String[] { RECORD_ID, BodyColumns.TEXT_REPLY }; - - public static final int COMMON_TEXT_COLUMN = 1; + public static final String[] COMMON_PROJECTION_REPLY_HTML = new String[] { + RECORD_ID, BodyColumns.HTML_REPLY + }; + public static final String[] COMMON_PROJECTION_INTRO = new String[] { + RECORD_ID, BodyColumns.INTRO_TEXT + }; + public static final int COMMON_PROJECTION_COLUMN_TEXT = 1; public long mMessageKey; public String mHtmlContent; @@ -240,7 +238,7 @@ public abstract class EmailContent { mBaseUri = CONTENT_URI; } - @Override + @Override public ContentValues toContentValues() { ContentValues values = new ContentValues(); @@ -255,39 +253,39 @@ public abstract class EmailContent { return values; } - private static Body restoreBodyWithCursor(Cursor cursor) { - try { - if (cursor.moveToFirst()) { - return getContent(cursor, Body.class); - } else { - return null; - } - } finally { - cursor.close(); - } - } + private static Body restoreBodyWithCursor(Cursor cursor) { + try { + if (cursor.moveToFirst()) { + return getContent(cursor, Body.class); + } else { + return null; + } + } finally { + cursor.close(); + } + } - public static Body restoreBodyWithId(Context context, long id) { - Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id); - Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION, - null, null, null); - return restoreBodyWithCursor(c); - } + public static Body restoreBodyWithId(Context context, long id) { + Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id); + Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION, + null, null, null); + return restoreBodyWithCursor(c); + } - public static Body restoreBodyWithMessageId(Context context, long messageId) { - Cursor c = context.getContentResolver().query(Body.CONTENT_URI, - Body.CONTENT_PROJECTION, Body.MESSAGE_KEY + "=?", - new String[] {Long.toString(messageId)}, null); - return restoreBodyWithCursor(c); - } + public static Body restoreBodyWithMessageId(Context context, long messageId) { + Cursor c = context.getContentResolver().query(Body.CONTENT_URI, + Body.CONTENT_PROJECTION, Body.MESSAGE_KEY + "=?", + new String[] {Long.toString(messageId)}, null); + return restoreBodyWithCursor(c); + } /** * Returns the bodyId for the given messageId, or -1 if no body is found. */ public static long lookupBodyIdWithMessageId(ContentResolver resolver, long messageId) { Cursor c = resolver.query(Body.CONTENT_URI, ID_PROJECTION, - Body.MESSAGE_KEY + "=?", - new String[] {Long.toString(messageId)}, null); + Body.MESSAGE_KEY + "=?", + new String[] {Long.toString(messageId)}, null); try { return c.moveToFirst() ? c.getLong(ID_PROJECTION_COLUMN) : -1; } finally { @@ -313,13 +311,13 @@ public abstract class EmailContent { } } - private static String restoreTextWithMessageId(Context context, long messageId, - String[] projection) { + private static String restoreTextWithMessageId(Context context, long messageId, + String[] projection) { Cursor c = context.getContentResolver().query(Body.CONTENT_URI, projection, Body.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null); try { if (c.moveToFirst()) { - return c.getString(COMMON_TEXT_COLUMN); + return c.getString(COMMON_PROJECTION_COLUMN_TEXT); } else { return null; } @@ -329,19 +327,23 @@ public abstract class EmailContent { } public static String restoreBodyTextWithMessageId(Context context, long messageId) { - return restoreTextWithMessageId(context, messageId, Body.TEXT_PROJECTION); + return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_TEXT); } public static String restoreBodyHtmlWithMessageId(Context context, long messageId) { - return restoreTextWithMessageId(context, messageId, Body.HTML_PROJECTION); + return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_HTML); } - public static String restoreTextReplyWithMessageId(Context context, long messageId) { - return restoreTextWithMessageId(context, messageId, Body.TEXT_REPLY_PROJECTION); + public static String restoreReplyTextWithMessageId(Context context, long messageId) { + return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_TEXT); } - public static String restoreHtmlReplyWithMessageId(Context context, long messageId) { - return restoreTextWithMessageId(context, messageId, Body.HTML_REPLY_PROJECTION); + public static String restoreReplyHtmlWithMessageId(Context context, long messageId) { + return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_REPLY_HTML); + } + + public static String restoreIntroTextWithMessageId(Context context, long messageId) { + return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_INTRO); } @Override @@ -485,6 +487,12 @@ public abstract class EmailContent { RECORD_ID, SyncColumns.SERVER_ID }; + public static final int ID_MAILBOX_COLUMN_ID = 0; + public static final int ID_MAILBOX_COLUMN_MAILBOX_KEY = 1; + public static final String[] ID_MAILBOX_PROJECTION = new String[] { + RECORD_ID, MessageColumns.MAILBOX_KEY + }; + public static final String[] ID_COLUMN_PROJECTION = new String[] { RECORD_ID }; // _id field is in AbstractContent diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index b7b69abb9..e22343f3d 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -16,14 +16,18 @@ package com.android.email; +import com.android.email.mail.Address; import com.android.email.mail.BodyPart; +import com.android.email.mail.Flag; 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.Message.RecipientType; 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.MimeMessage; import com.android.email.mail.internet.MimeUtility; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailProvider; @@ -170,4 +174,117 @@ public class LegacyConversionsTests extends ProviderTestCase2 { * TODO: Sunny day test of adding attachments from a POP message. */ + /** + * Sunny day tests of converting an original message to a legacy message + */ + public void testMakeLegacyMessage() throws MessagingException { + // Set up and store a message in the provider + long account1Id = 1; + long mailbox1Id = 1; + + // Test message 1: No body + EmailContent.Message localMessage1 = ProviderTestUtils.setupMessage("make-legacy", + account1Id, mailbox1Id, false, true, mProviderContext); + Message getMessage1 = LegacyConversions.makeMessage(mProviderContext, localMessage1); + checkLegacyMessage("no body", localMessage1, getMessage1); + + // Test message 2: Simple body + EmailContent.Message localMessage2 = ProviderTestUtils.setupMessage("make-legacy", + account1Id, mailbox1Id, true, false, mProviderContext); + localMessage2.mTextReply = null; + localMessage2.mHtmlReply = null; + localMessage2.mIntroText = null; + localMessage2.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; + localMessage2.save(mProviderContext); + Message getMessage2 = LegacyConversions.makeMessage(mProviderContext, localMessage2); + checkLegacyMessage("simple body", localMessage2, getMessage2); + + // Test message 3: Body + replied-to text + EmailContent.Message localMessage3 = ProviderTestUtils.setupMessage("make-legacy", + account1Id, mailbox1Id, true, false, mProviderContext); + localMessage3.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; + localMessage3.mFlags |= EmailContent.Message.FLAG_TYPE_REPLY; + localMessage3.save(mProviderContext); + Message getMessage3 = LegacyConversions.makeMessage(mProviderContext, localMessage3); + checkLegacyMessage("reply-to", localMessage3, getMessage3); + + // Test message 4: Body + forwarded text + EmailContent.Message localMessage4 = ProviderTestUtils.setupMessage("make-legacy", + account1Id, mailbox1Id, true, false, mProviderContext); + localMessage4.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; + localMessage4.mFlags |= EmailContent.Message.FLAG_TYPE_FORWARD; + localMessage4.save(mProviderContext); + Message getMessage4 = LegacyConversions.makeMessage(mProviderContext, localMessage4); + checkLegacyMessage("forwarding", localMessage4, getMessage4); + } + + /** + * Check equality of a pair of converted message + */ + private void checkLegacyMessage(String tag, EmailContent.Message expect, Message actual) + throws MessagingException { + assertEquals(tag, expect.mServerId, actual.getUid()); + assertEquals(tag, expect.mSubject, actual.getSubject()); + assertEquals(tag, expect.mFrom, Address.pack(actual.getFrom())); + assertEquals(tag, expect.mTimeStamp, actual.getSentDate().getTime()); + assertEquals(tag, expect.mTo, Address.pack(actual.getRecipients(RecipientType.TO))); + assertEquals(tag, expect.mCc, Address.pack(actual.getRecipients(RecipientType.CC))); + assertEquals(tag, expect.mMessageId, ((MimeMessage)actual).getMessageId()); + // check flags + assertEquals(tag, expect.mFlagRead, actual.isSet(Flag.SEEN)); + assertEquals(tag, expect.mFlagFavorite, actual.isSet(Flag.FLAGGED)); + + // Check the body of the message + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(actual, viewables, attachments); + String get1Text = null; + String get1Html = null; + String get1TextReply = null; + String get1HtmlReply = null; + String get1TextIntro = null; + for (Part viewable : viewables) { + String text = MimeUtility.getTextFromPart(viewable); + boolean isHtml = viewable.getMimeType().equalsIgnoreCase("text/html"); + String[] headers = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART); + if (headers != null) { + String header = headers[0]; + boolean isReply = LegacyConversions.BODY_QUOTED_PART_REPLY.equalsIgnoreCase(header); + boolean isFwd = LegacyConversions.BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(header); + boolean isIntro = LegacyConversions.BODY_QUOTED_PART_INTRO.equalsIgnoreCase(header); + if (isReply || isFwd) { + if (isHtml) { + get1HtmlReply = text; + } else { + get1TextReply = text; + } + } else if (isIntro) { + get1TextIntro = text; + } + // Check flags + int replyTypeFlags = expect.mFlags & EmailContent.Message.FLAG_TYPE_MASK; + if (isReply) { + assertEquals(tag, EmailContent.Message.FLAG_TYPE_REPLY, replyTypeFlags); + } + if (isFwd) { + assertEquals(tag, EmailContent.Message.FLAG_TYPE_FORWARD, replyTypeFlags); + } + } else { + if (isHtml) { + get1Html = text; + } else { + get1Text = text; + } + } + } + assertEquals(tag, expect.mText, get1Text); + assertEquals(tag, expect.mHtml, get1Html); + assertEquals(tag, expect.mTextReply, get1TextReply); + assertEquals(tag, expect.mHtmlReply, get1HtmlReply); + assertEquals(tag, expect.mIntroText, get1TextIntro); + + // TODO Check the attachments + +// cv.put("attachment_count", attachments.size()); + } } diff --git a/tests/src/com/android/email/mail/MockFolder.java b/tests/src/com/android/email/mail/MockFolder.java index 4d915d36c..f933c4381 100644 --- a/tests/src/com/android/email/mail/MockFolder.java +++ b/tests/src/com/android/email/mail/MockFolder.java @@ -19,132 +19,106 @@ package com.android.email.mail; public class MockFolder extends Folder { @Override - public void appendMessages(Message[] messages) throws MessagingException { - // TODO Auto-generated method stub - + public void appendMessages(Message[] messages) { } @Override - public void close(boolean expunge) throws MessagingException { - // TODO Auto-generated method stub - + public void close(boolean expunge) { } @Override public void copyMessages(Message[] msgs, Folder folder, - MessageUpdateCallbacks callbacks) throws MessagingException { - // TODO Auto-generated method stub - + MessageUpdateCallbacks callbacks) { } @Override - public boolean create(FolderType type) throws MessagingException { - // TODO Auto-generated method stub + public boolean canCreate(FolderType type) { return false; } @Override - public void delete(boolean recurse) throws MessagingException { - // TODO Auto-generated method stub - - } - - @Override - public boolean exists() throws MessagingException { - // TODO Auto-generated method stub + public boolean create(FolderType type) { return false; } @Override - public Message[] expunge() throws MessagingException { - // TODO Auto-generated method stub + public void delete(boolean recurse) { + } + + @Override + public boolean exists() { + return false; + } + + @Override + public Message[] expunge() { return null; } @Override - public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) - throws MessagingException { - // TODO Auto-generated method stub - + public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) { } @Override - public Message getMessage(String uid) throws MessagingException { - // TODO Auto-generated method stub + public Message getMessage(String uid) { return null; } @Override - public int getMessageCount() throws MessagingException { - // TODO Auto-generated method stub + public int getMessageCount() { return 0; } @Override - public Message[] getMessages(int start, int end, MessageRetrievalListener listener) - throws MessagingException { - // TODO Auto-generated method stub + public Message[] getMessages(int start, int end, MessageRetrievalListener listener) { return null; } @Override - public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { - // TODO Auto-generated method stub + public Message[] getMessages(MessageRetrievalListener listener) { return null; } @Override - public Message[] getMessages(String[] uids, MessageRetrievalListener listener) - throws MessagingException { - // TODO Auto-generated method stub + public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { return null; } @Override - public OpenMode getMode() throws MessagingException { - // TODO Auto-generated method stub + public OpenMode getMode() { return null; } @Override public String getName() { - // TODO Auto-generated method stub return null; } @Override - public Flag[] getPermanentFlags() throws MessagingException { - // TODO Auto-generated method stub + public Flag[] getPermanentFlags() { return null; } @Override - public int getUnreadMessageCount() throws MessagingException { - // TODO Auto-generated method stub + public int getUnreadMessageCount() { return 0; } @Override public boolean isOpen() { - // TODO Auto-generated method stub return false; } @Override - public void open(OpenMode mode, PersistentDataCallbacks callbacks) throws MessagingException { - // TODO Auto-generated method stub - + public void open(OpenMode mode, PersistentDataCallbacks callbacks) { } @Override - public void setFlags(Message[] messages, Flag[] flags, boolean value) throws MessagingException { - // TODO Auto-generated method stub - + public void setFlags(Message[] messages, Flag[] flags, boolean value) { } @Override - public Message createMessage(String uid) throws MessagingException { - // TODO Auto-generated method stub + public Message createMessage(String uid) { return null; } diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java index 50657dd52..c274369f2 100644 --- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java @@ -20,6 +20,7 @@ import com.android.email.mail.Flag; import com.android.email.mail.Folder; import com.android.email.mail.MessagingException; import com.android.email.mail.Transport; +import com.android.email.mail.Folder.FolderType; import com.android.email.mail.Folder.OpenMode; import com.android.email.mail.internet.BinaryTempFileBody; import com.android.email.mail.transport.MockTransport; @@ -128,6 +129,10 @@ public class ImapStoreUnitTests extends AndroidTestCase { assertEquals(Flag.DELETED, flags[0]); assertEquals(Flag.SEEN, flags[1]); assertEquals(Flag.FLAGGED, flags[2]); + + // canCreate() returns true + assertTrue(mFolder.canCreate(FolderType.HOLDS_FOLDERS)); + assertTrue(mFolder.canCreate(FolderType.HOLDS_MESSAGES)); } /** diff --git a/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java b/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java index 47856f4b1..82e2967da 100644 --- a/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java @@ -236,11 +236,13 @@ public class Pop3StoreUnitTests extends AndroidTestCase { // getMode() returns OpenMode.READ_WRITE assertEquals(OpenMode.READ_WRITE, mFolder.getMode()); - - // create() return false + + // canCreate() && create() return false + assertFalse(mFolder.canCreate(FolderType.HOLDS_FOLDERS)); + assertFalse(mFolder.canCreate(FolderType.HOLDS_MESSAGES)); assertFalse(mFolder.create(FolderType.HOLDS_FOLDERS)); assertFalse(mFolder.create(FolderType.HOLDS_MESSAGES)); - + // getUnreadMessageCount() always returns -1 assertEquals(-1, mFolder.getUnreadMessageCount()); diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index c4b515518..676c2cfef 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -129,7 +129,7 @@ public class ProviderTestUtils extends Assert { message.mFlagLoaded = Message.FLAG_LOADED_UNLOADED; message.mFlagFavorite = true; message.mFlagAttachment = true; - message.mFlags = 2; + message.mFlags = 0; message.mServerId = "serverid " + name; message.mServerTimeStamp = 300 + name.length(); diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index cef5c9991..2fab60c39 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -479,6 +479,28 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(body2.mIntroText, introText); } + /** + * Test body retrieve methods + */ + public void testBodyRetrieve() { + // No account needed + // No mailbox needed + Message message1 = ProviderTestUtils.setupMessage("bodyretrieve", 1, 1, true, + true, mMockContext); + long messageId = message1.mId; + + assertEquals(message1.mText, + Body.restoreBodyTextWithMessageId(mMockContext, messageId)); + assertEquals(message1.mHtml, + Body.restoreBodyHtmlWithMessageId(mMockContext, messageId)); + assertEquals(message1.mTextReply, + Body.restoreReplyTextWithMessageId(mMockContext, messageId)); + assertEquals(message1.mHtmlReply, + Body.restoreReplyHtmlWithMessageId(mMockContext, messageId)); + assertEquals(message1.mIntroText, + Body.restoreIntroTextWithMessageId(mMockContext, messageId)); + } + /** * Test delete body. * 1. create message without body (message id 1)