Enable message upload

* Create logic to detect upsyncable messages in Sent
* Note:  Drafts is now local only for IMAP - no sync, either way
* Rewrite MessageController.processPendingAppend for Provider world
* Write provider message -> legacy message converter
* Fixed bug in IMAP APPEND (it was not picking the right UID for the
    uploaded message.)
* Better handling of server internaldate
* Add constants for new X-Android-Body-Quoted-Part header
* Add EmailContent routines to get each of the 5 parts of the body
* Remove "Load more" from unsynced message lists
* Add toString to MimeHeader for debug support

Bug # 2097471

TODO (next CL): Upload attachments records too

Change-Id: I209182f5adc6b6696919f559e3cbbdd58b3eed3a
This commit is contained in:
Andrew Stadler 2009-09-25 14:54:32 -07:00
parent 1086473056
commit c41c47fa07
16 changed files with 780 additions and 235 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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()) {

View File

@ -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;

View File

@ -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

View File

@ -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<EmailProvider> {
* 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<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
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());
}
}

View File

@ -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;
}

View File

@ -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));
}
/**

View File

@ -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());

View File

@ -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();

View File

@ -479,6 +479,28 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
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)