diff options
Diffstat (limited to 'src/com/android/email/service/ImapService.java')
-rw-r--r-- | src/com/android/email/service/ImapService.java | 1615 |
1 files changed, 0 insertions, 1615 deletions
diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java deleted file mode 100644 index f7f2517d0..000000000 --- a/src/com/android/email/service/ImapService.java +++ /dev/null @@ -1,1615 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.service; - -import android.app.Service; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.TrafficStats; -import android.net.Uri; -import android.os.IBinder; -import android.os.SystemClock; -import android.text.TextUtils; -import android.text.format.DateUtils; - -import com.android.email.DebugUtils; -import com.android.email.LegacyConversions; -import com.android.email.NotificationController; -import com.android.email.R; -import com.android.email.mail.Store; -import com.android.email.provider.Utilities; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TrafficFlags; -import com.android.emailcommon.internet.MimeUtility; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.FetchProfile; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Folder.FolderType; -import com.android.emailcommon.mail.Folder.MessageRetrievalListener; -import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; -import com.android.emailcommon.mail.Folder.OpenMode; -import com.android.emailcommon.mail.Message; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Part; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.emailcommon.provider.EmailContent.MessageColumns; -import com.android.emailcommon.provider.EmailContent.SyncColumns; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.EmailServiceStatus; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.service.SyncWindow; -import com.android.emailcommon.utility.AttachmentUtilities; -import com.android.mail.providers.UIProvider; -import com.android.mail.utils.LogUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - -public class ImapService extends Service { - // TODO get these from configurations or settings. - private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS; - private static final long FULL_SYNC_WINDOW_MILLIS = 7 * DateUtils.DAY_IN_MILLIS; - private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS; - - // The maximum number of messages to fetch in a single command. - private static final int MAX_MESSAGES_TO_FETCH = 500; - private static final int MINIMUM_MESSAGES_TO_SYNC = 10; - private static final int LOAD_MORE_MIN_INCREMENT = 10; - private static final int LOAD_MORE_MAX_INCREMENT = 20; - private static final long INITIAL_WINDOW_SIZE_INCREASE = 24 * 60 * 60 * 1000; - - private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; - private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; - private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; - - /** - * Simple cache for last search result mailbox by account and serverId, since the most common - * case will be repeated use of the same mailbox - */ - private static long mLastSearchAccountKey = Account.NO_ACCOUNT; - private static String mLastSearchServerId = null; - private static Mailbox mLastSearchRemoteMailbox = null; - - /** - * Cache search results by account; this allows for "load more" support without having to - * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory - * shouldn't be an issue - */ - private static final HashMap<Long, SortableMessage[]> sSearchResults = - new HashMap<Long, SortableMessage[]>(); - - /** - * We write this into the serverId field of messages that will never be upsynced. - */ - private static final String LOCAL_SERVERID_PREFIX = "Local-"; - - private static String sMessageDecodeErrorString; - - /** - * Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access - * to a Context object. - * @return Error string or empty string - */ - public static String getMessageDecodeErrorString() { - return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString; - } - - @Override - public void onCreate() { - super.onCreate(); - - sMessageDecodeErrorString = getString(R.string.message_decode_error); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - /** - * Create our EmailService implementation here. - */ - private final EmailServiceStub mBinder = new EmailServiceStub() { - @Override - public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { - try { - return searchMailboxImpl(getApplicationContext(), accountId, searchParams, - destMailboxId); - } catch (MessagingException e) { - // Ignore - } - return 0; - } - }; - - @Override - public IBinder onBind(Intent intent) { - mBinder.init(this); - return mBinder; - } - - /** - * Start foreground synchronization of the specified folder. This is called by - * synchronizeMailbox or checkMail. - * TODO this should use ID's instead of fully-restored objects - * @return The status code for whether this operation succeeded. - * @throws MessagingException - */ - public static synchronized int synchronizeMailboxSynchronous(Context context, - final Account account, final Mailbox folder, final boolean loadMore, - final boolean uiRefresh) throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - NotificationController nc = NotificationController.getInstance(context); - Store remoteStore = null; - try { - remoteStore = Store.getInstance(account, context); - processPendingActionsSynchronous(context, account, remoteStore, uiRefresh); - synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh); - // Clear authentication notification for this account - nc.cancelLoginFailedNotification(account.mId); - } catch (MessagingException e) { - if (Logging.LOGD) { - LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxSynchronous", e); - } - if (e instanceof AuthenticationFailedException) { - // Generate authentication notification - nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */); - } - throw e; - } finally { - if (remoteStore != null) { - remoteStore.closeConnections(); - } - } - // TODO: Rather than use exceptions as logic above, return the status and handle it - // correctly in caller. - return EmailServiceStatus.SUCCESS; - } - - /** - * Lightweight record for the first pass of message sync, where I'm just seeing if - * the local message requires sync. Later (for messages that need syncing) we'll do a full - * readout from the DB. - */ - private static class LocalMessageInfo { - private static final int COLUMN_ID = 0; - private static final int COLUMN_FLAG_READ = 1; - private static final int COLUMN_FLAG_FAVORITE = 2; - private static final int COLUMN_FLAG_LOADED = 3; - private static final int COLUMN_SERVER_ID = 4; - private static final int COLUMN_FLAGS = 5; - private static final int COLUMN_TIMESTAMP = 6; - private static final String[] PROJECTION = { - MessageColumns._ID, - MessageColumns.FLAG_READ, - MessageColumns.FLAG_FAVORITE, - MessageColumns.FLAG_LOADED, - SyncColumns.SERVER_ID, - MessageColumns.FLAGS, - MessageColumns.TIMESTAMP - }; - - final long mId; - final boolean mFlagRead; - final boolean mFlagFavorite; - final int mFlagLoaded; - final String mServerId; - final int mFlags; - final long mTimestamp; - - public LocalMessageInfo(Cursor c) { - mId = c.getLong(COLUMN_ID); - mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0; - mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0; - mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); - mServerId = c.getString(COLUMN_SERVER_ID); - mFlags = c.getInt(COLUMN_FLAGS); - mTimestamp = c.getLong(COLUMN_TIMESTAMP); - // Note: mailbox key and account key not needed - they are projected for the SELECT - } - } - - private static class OldestTimestampInfo { - private static final int COLUMN_OLDEST_TIMESTAMP = 0; - private static final String[] PROJECTION = new String[] { - "MIN(" + MessageColumns.TIMESTAMP + ")" - }; - } - - /** - * Load the structure and body of messages not yet synced - * @param account the account we're syncing - * @param remoteFolder the (open) Folder we're working on - * @param messages an array of Messages we've got headers for - * @param toMailbox the destination mailbox we're syncing - * @throws MessagingException - */ - static void loadUnsyncedMessages(final Context context, final Account account, - Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox) - throws MessagingException { - - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null); - Message [] oneMessageArray = new Message[1]; - for (Message message : messages) { - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList<Part> viewables = new ArrayList<Part>(); - ArrayList<Part> attachments = new ArrayList<Part>(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - oneMessageArray[0] = message; - for (Part part : viewables) { - fp.clear(); - fp.add(part); - remoteFolder.fetch(oneMessageArray, fp, null); - } - // Store the updated message locally and mark it fully loaded - Utilities.copyOneMessageToProvider(context, message, account, toMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - } - - public static void downloadFlagAndEnvelope(final Context context, final Account account, - final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, - HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages) - throws MessagingException { - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - final HashMap<String, LocalMessageInfo> localMapCopy; - if (localMessageMap != null) - localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap); - else { - localMapCopy = new HashMap<String, LocalMessageInfo>(); - } - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[unsyncedMessages.size()]), fp, - new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - // Determine if the new message was already known (e.g. partial) - // And create or reload the full message info - final LocalMessageInfo localMessageInfo = - localMapCopy.get(message.getUid()); - final boolean localExists = localMessageInfo != null; - - if (!localExists && message.isSet(Flag.DELETED)) { - // This is a deleted message that we don't have locally, so don't - // create it - return; - } - - final EmailContent.Message localMessage; - if (!localExists) { - localMessage = new EmailContent.Message(); - } else { - localMessage = EmailContent.Message.restoreMessageWithId( - context, localMessageInfo.mId); - } - - if (localMessage != null) { - try { - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Commit the message to the local store - Utilities.saveOrUpdate(localMessage, context); - // Track the "new" ness of the downloaded message - if (!message.isSet(Flag.SEEN) && unseenMessages != null) { - unseenMessages.add(localMessage.mId); - } - } catch (MessagingException me) { - LogUtils.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - } - } - catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, - "Error while storing downloaded message." + e.toString()); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - } - - /** - * Synchronizer for IMAP. - * - * TODO Break this method up into smaller chunks. - * - * @param account the account to sync - * @param mailbox the mailbox to sync - * @param loadMore whether we should be loading more older messages - * @param uiRefresh whether this request is in response to a user action - * @throws MessagingException - */ - private synchronized static void synchronizeMailboxGeneric(final Context context, - final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore, - final boolean uiRefresh) - throws MessagingException { - - LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxGeneric " + account + " " + mailbox + " " - + loadMore + " " + uiRefresh); - - final ArrayList<Long> unseenMessages = new ArrayList<Long>(); - - ContentResolver resolver = context.getContentResolver(); - - // 0. We do not ever sync DRAFTS or OUTBOX (down or up) - if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { - return; - } - - // 1. Figure out what our sync window should be. - long endDate; - - // We will do a full sync if the user has actively requested a sync, or if it has been - // too long since the last full sync. - // If we have rebooted since the last full sync, then we may get a negative - // timeSinceLastFullSync. In this case, we don't know how long it's been since the last - // full sync so we should perform the full sync. - final long timeSinceLastFullSync = SystemClock.elapsedRealtime() - - mailbox.mLastFullSyncTime; - final boolean fullSync = (uiRefresh || loadMore || - timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS || timeSinceLastFullSync < 0); - - if (account.mSyncLookback == SyncWindow.SYNC_WINDOW_ALL) { - // This is really for testing. There is no UI that allows setting the sync window for - // IMAP, but it can be set by sending a special intent to AccountSetupFinal activity. - endDate = 0; - } else if (fullSync) { - // Find the oldest message in the local store. We need our time window to include - // all messages that are currently present locally. - endDate = System.currentTimeMillis() - FULL_SYNC_WINDOW_MILLIS; - Cursor localOldestCursor = null; - try { - // b/11520812 Ignore message with timestamp = 0 (which includes NULL) - localOldestCursor = resolver.query(EmailContent.Message.CONTENT_URI, - OldestTimestampInfo.PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + " AND " + - MessageColumns.MAILBOX_KEY + "=? AND " + - MessageColumns.TIMESTAMP + "!=0", - new String[] {String.valueOf(account.mId), String.valueOf(mailbox.mId)}, - null); - if (localOldestCursor != null && localOldestCursor.moveToFirst()) { - long oldestLocalMessageDate = localOldestCursor.getLong( - OldestTimestampInfo.COLUMN_OLDEST_TIMESTAMP); - if (oldestLocalMessageDate > 0) { - endDate = Math.min(endDate, oldestLocalMessageDate); - LogUtils.d( - Logging.LOG_TAG, "oldest local message " + oldestLocalMessageDate); - } - } - } finally { - if (localOldestCursor != null) { - localOldestCursor.close(); - } - } - LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate); - } else { - // We are doing a frequent, quick sync. This only syncs a small time window, so that - // we wil get any new messages, but not spend a lot of bandwidth downloading - // messageIds that we most likely already have. - endDate = System.currentTimeMillis() - QUICK_SYNC_WINDOW_MILLIS; - LogUtils.d(Logging.LOG_TAG, "quick sync: original window: now - " + endDate); - } - - // 2. Open the remote folder and create the remote folder if necessary - // The account might have been deleted - if (remoteStore == null) { - LogUtils.d(Logging.LOG_TAG, "account is apparently deleted"); - return; - } - final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - - // If the folder is a "special" folder we need to see if it exists - // on the remote server. It if does not exist we'll try to create it. If we - // can't create we'll abort. This will happen on every single Pop3 folder as - // designed and on Imap folders during error conditions. This allows us - // to treat Pop3 and Imap the same in this code. - if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT) { - if (!remoteFolder.exists()) { - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - LogUtils.w(Logging.LOG_TAG, "could not create remote folder type %d", - mailbox.mType); - return; - } - } - } - remoteFolder.open(OpenMode.READ_WRITE); - - // 3. Trash any remote messages that are marked as trashed locally. - // TODO - this comment was here, but no code was here. - - // 4. Get the number of messages on the server. - // TODO: this value includes deleted but unpurged messages, and so slightly mismatches - // the contents of our DB since we drop deleted messages. Figure out what to do about this. - final int remoteMessageCount = remoteFolder.getMessageCount(); - - // 5. Save folder message count locally. - mailbox.updateMessageCount(context, remoteMessageCount); - - // 6. Get all message Ids in our sync window: - Message[] remoteMessages; - remoteMessages = remoteFolder.getMessages(0, endDate, null); - LogUtils.d(Logging.LOG_TAG, "received " + remoteMessages.length + " messages"); - - // 7. See if we need any additional messages beyond our date query range results. - // If we do, keep increasing the size of our query window until we have - // enough, or until we have all messages in the mailbox. - int totalCountNeeded; - if (loadMore) { - totalCountNeeded = remoteMessages.length + LOAD_MORE_MIN_INCREMENT; - } else { - totalCountNeeded = remoteMessages.length; - if (fullSync && totalCountNeeded < MINIMUM_MESSAGES_TO_SYNC) { - totalCountNeeded = MINIMUM_MESSAGES_TO_SYNC; - } - } - LogUtils.d(Logging.LOG_TAG, "need " + totalCountNeeded + " total"); - - final int additionalMessagesNeeded = totalCountNeeded - remoteMessages.length; - if (additionalMessagesNeeded > 0) { - LogUtils.d(Logging.LOG_TAG, "trying to get " + additionalMessagesNeeded + " more"); - long startDate = endDate - 1; - Message[] additionalMessages = new Message[0]; - long windowIncreaseSize = INITIAL_WINDOW_SIZE_INCREASE; - while (additionalMessages.length < additionalMessagesNeeded && endDate > 0) { - endDate = endDate - windowIncreaseSize; - if (endDate < 0) { - LogUtils.d(Logging.LOG_TAG, "window size too large, this is the last attempt"); - endDate = 0; - } - LogUtils.d(Logging.LOG_TAG, - "requesting additional messages from range " + startDate + " - " + endDate); - additionalMessages = remoteFolder.getMessages(startDate, endDate, null); - - // If don't get enough messages with the first window size expansion, - // we need to accelerate rate at which the window expands. Otherwise, - // if there were no messages for several weeks, we'd always end up - // performing dozens of queries. - windowIncreaseSize *= 2; - } - - LogUtils.d(Logging.LOG_TAG, "additionalMessages " + additionalMessages.length); - if (additionalMessages.length < additionalMessagesNeeded) { - // We have attempted to load a window that goes all the way back to time zero, - // but we still don't have as many messages as the server says are in the inbox. - // This is not expected to happen. - LogUtils.e(Logging.LOG_TAG, "expected to find " + additionalMessagesNeeded - + " more messages, only got " + additionalMessages.length); - } - int additionalToKeep = additionalMessages.length; - if (additionalMessages.length > LOAD_MORE_MAX_INCREMENT) { - // We have way more additional messages than intended, drop some of them. - // The last messages are the most recent, so those are the ones we need to keep. - additionalToKeep = LOAD_MORE_MAX_INCREMENT; - } - - // Copy the messages into one array. - Message[] allMessages = new Message[remoteMessages.length + additionalToKeep]; - System.arraycopy(remoteMessages, 0, allMessages, 0, remoteMessages.length); - // additionalMessages may have more than we need, only copy the last - // several. These are the most recent messages in that set because - // of the way IMAP server returns messages. - System.arraycopy(additionalMessages, additionalMessages.length - additionalToKeep, - allMessages, remoteMessages.length, additionalToKeep); - remoteMessages = allMessages; - } - - // 8. Get the all of the local messages within the sync window, and create - // an index of the uids. - // The IMAP query for messages ignores time, and only looks at the date part of the endDate. - // So if we query for messages since Aug 11 at 3:00 PM, we can get messages from any time - // on Aug 11. Our IMAP query results can include messages up to 24 hours older than endDate, - // or up to 25 hours older at a daylight savings transition. - // It is important that we have the Id of any local message that could potentially be - // returned by the IMAP query, or we will create duplicate copies of the same messages. - // So we will increase our local query range by this much. - // Note that this complicates deletion: It's not okay to delete anything that is in the - // localMessageMap but not in the remote result, because we know that we may be getting - // Ids of local messages that are outside the IMAP query window. - Cursor localUidCursor = null; - HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>(); - try { - // FLAG: There is a problem that causes us to store the wrong date on some messages, - // so messages get a date of zero. If we filter these messages out and don't put them - // in our localMessageMap, then we'll end up loading the same message again. - // See b/10508861 -// final long queryEndDate = endDate - DateUtils.DAY_IN_MILLIS - DateUtils.HOUR_IN_MILLIS; - final long queryEndDate = 0; - localUidCursor = resolver.query( - EmailContent.Message.CONTENT_URI, - LocalMessageInfo.PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" - + " AND " + MessageColumns.MAILBOX_KEY + "=?" - + " AND " + MessageColumns.TIMESTAMP + ">=?", - new String[] { - String.valueOf(account.mId), - String.valueOf(mailbox.mId), - String.valueOf(queryEndDate) }, - null); - while (localUidCursor.moveToNext()) { - LocalMessageInfo info = new LocalMessageInfo(localUidCursor); - // If the message has no server id, it's local only. This should only happen for - // mail created on the client that has failed to upsync. We want to ignore such - // mail during synchronization (i.e. leave it as-is and let the next sync try again - // to upsync). - if (!TextUtils.isEmpty(info.mServerId)) { - localMessageMap.put(info.mServerId, info); - } - } - } finally { - if (localUidCursor != null) { - localUidCursor.close(); - } - } - - // 9. Get a list of the messages that are in the remote list but not on the - // local store, or messages that are in the local store but failed to download - // on the last sync. These are the new messages that we will download. - // Note, we also skip syncing messages which are flagged as "deleted message" sentinels, - // because they are locally deleted and we don't need or want the old message from - // the server. - final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); - final HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); - // Process the messages in the reverse order we received them in. This means that - // we load the most recent one first, which gives a better user experience. - for (int i = remoteMessages.length - 1; i >= 0; i--) { - Message message = remoteMessages[i]; - LogUtils.d(Logging.LOG_TAG, "remote message " + message.getUid()); - remoteUidMap.put(message.getUid(), message); - - LocalMessageInfo localMessage = localMessageMap.get(message.getUid()); - - // localMessage == null -> message has never been created (not even headers) - // mFlagLoaded = UNLOADED -> message created, but none of body loaded - // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded - // mFlagLoaded = COMPLETE -> message body has been completely loaded - // mFlagLoaded = DELETED -> message has been deleted - // Only the first two of these are "unsynced", so let's retrieve them - if (localMessage == null || - (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) || - (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) { - unsyncedMessages.add(message); - } - } - - // 10. Download basic info about the new/unloaded messages (if any) - /* - * Fetch the flags and envelope only of the new messages. This is intended to get us - * critical data as fast as possible, and then we'll fill in the details. - */ - if (unsyncedMessages.size() > 0) { - downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages, - localMessageMap, unseenMessages); - } - - // 11. Refresh the flags for any messages in the local store that we didn't just download. - // TODO This is a bit wasteful because we're also updating any messages we already did get - // the flags and envelope for previously. - // TODO: the fetch() function, and others, should take List<>s of messages, not - // arrays of messages. - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - if (remoteMessages.length > MAX_MESSAGES_TO_FETCH) { - List<Message> remoteMessageList = Arrays.asList(remoteMessages); - for (int start = 0; start < remoteMessageList.size(); start += MAX_MESSAGES_TO_FETCH) { - int end = start + MAX_MESSAGES_TO_FETCH; - if (end >= remoteMessageList.size()) { - end = remoteMessageList.size() - 1; - } - List<Message> chunk = remoteMessageList.subList(start, end); - final Message[] partialArray = chunk.toArray(new Message[chunk.size()]); - // Fetch this one chunk of messages - remoteFolder.fetch(partialArray, fp, null); - } - } else { - remoteFolder.fetch(remoteMessages, fp, null); - } - boolean remoteSupportsSeen = false; - boolean remoteSupportsFlagged = false; - boolean remoteSupportsAnswered = false; - for (Flag flag : remoteFolder.getPermanentFlags()) { - if (flag == Flag.SEEN) { - remoteSupportsSeen = true; - } - if (flag == Flag.FLAGGED) { - remoteSupportsFlagged = true; - } - if (flag == Flag.ANSWERED) { - remoteSupportsAnswered = true; - } - } - - // 12. Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3) - if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) { - for (Message remoteMessage : remoteMessages) { - LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid()); - if (localMessageInfo == null) { - continue; - } - boolean localSeen = localMessageInfo.mFlagRead; - boolean remoteSeen = remoteMessage.isSet(Flag.SEEN); - boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen)); - boolean localFlagged = localMessageInfo.mFlagFavorite; - boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED); - boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged)); - int localFlags = localMessageInfo.mFlags; - boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0; - boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED); - boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered)); - if (newSeen || newFlagged || newAnswered) { - Uri uri = ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, localMessageInfo.mId); - ContentValues updateValues = new ContentValues(); - updateValues.put(MessageColumns.FLAG_READ, remoteSeen); - updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged); - if (remoteAnswered) { - localFlags |= EmailContent.Message.FLAG_REPLIED_TO; - } else { - localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO; - } - updateValues.put(MessageColumns.FLAGS, localFlags); - resolver.update(uri, updateValues, null, null); - } - } - } - - // 12.5 Remove messages that are marked as deleted so that we drop them from the DB in the - // next step - for (final Message remoteMessage : remoteMessages) { - if (remoteMessage.isSet(Flag.DELETED)) { - remoteUidMap.remove(remoteMessage.getUid()); - unsyncedMessages.remove(remoteMessage); - } - } - - // 13. Remove messages that are in the local store and in the current sync window, - // but no longer on the remote store. Note that localMessageMap can contain messages - // that are not actually in our sync window. We need to check the timestamp to ensure - // that it is before deleting. - for (final LocalMessageInfo info : localMessageMap.values()) { - // If this message is inside our sync window, and we cannot find it in our list - // of remote messages, then we know it's been deleted from the server. - if (info.mTimestamp >= endDate && !remoteUidMap.containsKey(info.mServerId)) { - // Delete associated data (attachment files) - // Attachment & Body records are auto-deleted when we delete the Message record - AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, info.mId); - - // Delete the message itself - final Uri uriToDelete = ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, info.mId); - resolver.delete(uriToDelete, null, null); - - // Delete extra rows (e.g. updated or deleted) - final Uri updateRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, info.mId); - resolver.delete(updateRowToDelete, null, null); - final Uri deleteRowToDelete = ContentUris.withAppendedId( - EmailContent.Message.DELETED_CONTENT_URI, info.mId); - resolver.delete(deleteRowToDelete, null, null); - } - } - - loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); - - if (fullSync) { - mailbox.updateLastFullSyncTime(context, SystemClock.elapsedRealtime()); - } - - // 14. Clean up and report results - remoteFolder.close(false); - } - - /** - * Find messages in the updated table that need to be written back to server. - * - * Handles: - * Read/Unread - * Flagged - * Append (upload) - * Move To Trash - * Empty trash - * TODO: - * Move - * - * @param account the account to scan for pending actions - * @throws MessagingException - */ - private static void processPendingActionsSynchronous(Context context, Account account, - Store remoteStore, boolean manualSync) - throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); - String[] accountIdArgs = new String[] { Long.toString(account.mId) }; - - // Handle deletes first, it's always better to get rid of things first - processPendingDeletesSynchronous(context, account, remoteStore, accountIdArgs); - - // Handle uploads (currently, only to sent messages) - processPendingUploadsSynchronous(context, account, remoteStore, accountIdArgs, manualSync); - - // Now handle updates / upsyncs - processPendingUpdatesSynchronous(context, account, remoteStore, accountIdArgs); - } - - /** - * Get the mailbox corresponding to the remote location of a message; this will normally be - * the mailbox whose _id is mailboxKey, except for search results, where we must look it up - * by serverId. - * - * @param message the message in question - * @return the mailbox in which the message resides on the server - */ - private static Mailbox getRemoteMailboxForMessage( - Context context, EmailContent.Message message) { - // If this is a search result, use the protocolSearchInfo field to get the server info - if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { - long accountKey = message.mAccountKey; - String protocolSearchInfo = message.mProtocolSearchInfo; - if (accountKey == mLastSearchAccountKey && - protocolSearchInfo.equals(mLastSearchServerId)) { - return mLastSearchRemoteMailbox; - } - Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, - Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION, - new String[] {protocolSearchInfo, Long.toString(accountKey) }, - null); - try { - if (c.moveToNext()) { - Mailbox mailbox = new Mailbox(); - mailbox.restore(c); - mLastSearchAccountKey = accountKey; - mLastSearchServerId = protocolSearchInfo; - mLastSearchRemoteMailbox = mailbox; - return mailbox; - } else { - return null; - } - } finally { - c.close(); - } - } else { - return Mailbox.restoreMailboxWithId(context, message.mMailboxKey); - } - } - - /** - * Scan for messages that are in the Message_Deletes table, look for differences that - * we can deal with, and do the work. - */ - private static void processPendingDeletesSynchronous(Context context, Account account, - Store remoteStore, String[] accountIdArgs) { - Cursor deletes = context.getContentResolver().query( - EmailContent.Message.DELETED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - try { - // loop through messages marked as deleted - while (deletes.moveToNext()) { - EmailContent.Message oldMessage = - EmailContent.getContent(context, deletes, EmailContent.Message.class); - - if (oldMessage != null) { - lastMessageId = oldMessage.mId; - - Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - final boolean deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH; - - // Dispatch here for specific change types - if (deleteFromTrash) { - // Move message to trash - processPendingDeleteFromTrash(remoteStore, mailbox, oldMessage); - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, - oldMessage.mId); - context.getContentResolver().delete(uri, null, null); - } - } - } 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 (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "Unable to process pending delete for id=" - + lastMessageId + ": " + me); - } - } finally { - deletes.close(); - } - } - - /** - * 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. - */ - private static void processPendingUploadsSynchronous(Context context, Account account, - Store remoteStore, String[] accountIdArgs, boolean manualSync) { - ContentResolver resolver = context.getContentResolver(); - // Find the Sent folder (since that's all we're uploading for now - // TODO: Upsync for all folders? (In case a user moves mail from Sent before it is - // handled. Also, this would generically solve allowing drafts to upload.) - 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 { - 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, - MessageColumns.MAILBOX_KEY + "=?" - + " and (" + MessageColumns.SERVER_ID + " is null" - + " or " + MessageColumns.SERVER_ID + "=''" + ")", - mailboxKeyArgs, - null); - try { - while (upsyncs1.moveToNext()) { - // Load the remote store if it will be needed - if (remoteStore == null) { - remoteStore = Store.getInstance(account, context); - } - // Load the mailbox if it will be needed - if (mailbox == null) { - mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - } - // upsync the message - long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); - lastMessageId = id; - processUploadMessage(context, remoteStore, mailbox, id, manualSync); - } - } finally { - if (upsyncs1 != null) { - upsyncs1.close(); - } - if (remoteStore != null) { - remoteStore.closeConnections(); - } - } - } - } 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 (DebugUtils.DEBUG) { - LogUtils.d(Logging.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. - */ - private static void processPendingUpdatesSynchronous(Context context, Account account, - Store remoteStore, String[] accountIdArgs) { - ContentResolver resolver = context.getContentResolver(); - Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - try { - // Demand load mailbox (note order-by to reduce thrashing here) - Mailbox mailbox = null; - // loop through messages marked as needing updates - while (updates.moveToNext()) { - boolean changeMoveToTrash = false; - boolean changeRead = false; - boolean changeFlagged = false; - boolean changeMailbox = false; - boolean changeAnswered = false; - - EmailContent.Message oldMessage = - EmailContent.getContent(context, updates, EmailContent.Message.class); - lastMessageId = oldMessage.mId; - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(context, oldMessage.mId); - if (newMessage != null) { - mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - if (oldMessage.mMailboxKey != newMessage.mMailboxKey) { - if (mailbox.mType == Mailbox.TYPE_TRASH) { - changeMoveToTrash = true; - } else { - changeMailbox = true; - } - } - changeRead = oldMessage.mFlagRead != newMessage.mFlagRead; - changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite; - changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO); - } - - // Load the remote store if it will be needed - if (remoteStore == null && - (changeMoveToTrash || changeRead || changeFlagged || changeMailbox || - changeAnswered)) { - remoteStore = Store.getInstance(account, context); - } - - // Dispatch here for specific change types - if (changeMoveToTrash) { - // Move message to trash - processPendingMoveToTrash(context, remoteStore, mailbox, oldMessage, - newMessage); - } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) { - processPendingDataChange(context, remoteStore, mailbox, changeRead, - changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage); - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, - oldMessage.mId); - resolver.delete(uri, null, null); - } - - } 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 (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "Unable to process pending update for id=" - + lastMessageId + ": " + me); - } - } finally { - updates.close(); - } - } - - /** - * 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 mailbox the actual mailbox - */ - private static void processUploadMessage(Context context, Store remoteStore, Mailbox mailbox, - long messageId, boolean manualSync) - throws MessagingException { - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(context, messageId); - final boolean deleteUpdate; - if (newMessage == null) { - deleteUpdate = true; - LogUtils.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_TRASH) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); - } else if (newMessage.mMailboxKey != mailbox.mId) { - deleteUpdate = false; - LogUtils.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); - } else { - LogUtils.d(Logging.LOG_TAG, "Upsync triggered for message id=" + messageId); - deleteUpdate = - processPendingAppend(context, remoteStore, mailbox, newMessage, manualSync); - } - if (deleteUpdate) { - // Finally, delete the update (if any) - Uri uri = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, messageId); - context.getContentResolver().delete(uri, null, null); - } - } - - /** - * Upsync changes to read, flagged, or mailbox - * - * @param remoteStore the remote store for this mailbox - * @param mailbox the mailbox the message is stored in - * @param changeRead whether the message's read state has changed - * @param changeFlagged whether the message's flagged state has changed - * @param changeMailbox whether the message's mailbox has changed - * @param oldMessage the message in it's pre-change state - * @param newMessage the current version of the message - */ - private static void processPendingDataChange(final Context context, Store remoteStore, - Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, - boolean changeAnswered, EmailContent.Message oldMessage, - final EmailContent.Message newMessage) throws MessagingException { - // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't - // being moved - Mailbox newMailbox = mailbox; - // Mailbox is the original remote mailbox (the one we're acting on) - mailbox = getRemoteMailboxForMessage(context, oldMessage); - - // 0. No remote update if the message is local-only - if (newMessage.mServerId == null || newMessage.mServerId.equals("") - || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) { - 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.mServerId); - if (!remoteFolder.exists()) { - return; - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return; - } - - // 3. Finally, apply the changes to the message - Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); - if (remoteMessage == null) { - return; - } - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, - "Update for msg id=" + newMessage.mId - + " read=" + newMessage.mFlagRead - + " flagged=" + newMessage.mFlagFavorite - + " answered=" - + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0) - + " new mailbox=" + newMessage.mMailboxKey); - } - Message[] messages = new Message[] { remoteMessage }; - if (changeRead) { - remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead); - } - if (changeFlagged) { - remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite); - } - if (changeAnswered) { - remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED, - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0); - } - if (changeMailbox) { - Folder toFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - // We may need the message id to search for the message in the destination folder - remoteMessage.setMessageId(newMessage.mMessageId); - // Copy the message to its new folder - remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - ContentValues cv = new ContentValues(); - cv.put(MessageColumns.SERVER_ID, newUid); - // We only have one message, so, any updates _must_ be for it. Otherwise, - // we'd have to cycle through to find the one with the same server ID. - context.getContentResolver().update(ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null); - } - - @Override - public void onMessageNotFound(Message message) { - } - }); - // Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - } - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param newMailbox The local trash mailbox - * @param oldMessage The message copy that was saved in the updates shadow table - * @param newMessage The message that was moved to the mailbox - */ - private static void processPendingMoveToTrash(final Context context, Store remoteStore, - 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 = getRemoteMailboxForMessage(context, oldMessage); - if (oldMailbox == null) { - // can't find old mailbox, it may have been deleted. just return. - return; - } - // 2. We don't support delete-from-trash here - if (oldMailbox.mType == Mailbox.TYPE_TRASH) { - return; - } - - // The rest of this method handles server-side deletion - - // 4. Find the remote mailbox (that we deleted from), and open it - Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - return; - } - - // 5. Find the remote original message - Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteFolder.close(false); - return; - } - - // 6. Find the remote trash folder, and create it if not found - Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - /* - * If the remote trash folder doesn't exist we try to create it. - */ - remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); - } - - // 7. Try to copy the message into the remote trash folder - // Note, this entire section will be skipped for POP3 because there's no remote trash - if (remoteTrashFolder.exists()) { - /* - * Because remoteTrashFolder may be new, we need to explicitly open it - */ - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - remoteTrashFolder.close(false); - return; - } - - remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, - new Folder.MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - // update the UID in the local trash folder, because some stores will - // have to change it when copying to remoteTrashFolder - ContentValues cv = new ContentValues(); - cv.put(MessageColumns.SERVER_ID, newUid); - context.getContentResolver().update(newMessage.getUri(), cv, null, null); - } - - /** - * This will be called if the deleted message doesn't exist and can't be - * deleted (e.g. it was already deleted from the server.) In this case, - * attempt to delete the local copy as well. - */ - @Override - public void onMessageNotFound(Message message) { - context.getContentResolver().delete(newMessage.getUri(), null, null); - } - }); - remoteTrashFolder.close(false); - } - - // 8. Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param oldMailbox The local trash mailbox - * @param oldMessage The message that was deleted from the trash - */ - private static void processPendingDeleteFromTrash(Store remoteStore, - Mailbox oldMailbox, EmailContent.Message oldMessage) - throws MessagingException { - - // 1. We only support delete-from-trash here - if (oldMailbox.mType != Mailbox.TYPE_TRASH) { - return; - } - - // 2. Find the remote trash folder (that we are deleting from), and open it - Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - return; - } - - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteTrashFolder.close(false); - return; - } - - // 3. Find the remote original message - Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteTrashFolder.close(false); - return; - } - - // 4. Delete the message from the remote trash folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteTrashFolder.expunge(); - remoteTrashFolder.close(false); - } - - /** - * 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. - * - * @param remoteStore the remote store we're working in - * @param mailbox The mailbox we're appending to - * @param message The message we're appending - * @param manualSync True if this is a manual sync (changes upsync behavior) - * @return true if successfully uploaded - */ - private static boolean processPendingAppend(Context context, Store remoteStore, Mailbox mailbox, - EmailContent.Message message, boolean manualSync) - throws MessagingException { - boolean updateInternalDate = false; - boolean updateMessage = false; - boolean deleteMessage = false; - - // 1. Find the remote folder that we're appending to and create and/or open it - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - if (!remoteFolder.exists()) { - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - // This is a (hopefully) transient error and we return false to try again later - return false; - } - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return false; - } - - // 2. If possible, load a remote message with the matching UID - Message remoteMessage = null; - 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) { - // TODO: - // if we have a serverId and remoteMessage is still null, then probably the message - // has been deleted and we should delete locally. - // 3a. Create a legacy message to upload - Message localMessage = LegacyConversions.makeMessage(context, message); - // 3b. Upload it - //FetchProfile fp = new FetchProfile(); - //fp.add(FetchProfile.Item.BODY); - // Note that this operation will assign the Uid to localMessage - remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */); - - // 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. - // TODO: - // I don't see a good reason we should be here. If the message already has a serverId, - // then we should be handling it in processPendingUpdates(), - // not processPendingUploads() - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); - Date localDate = new Date(message.mServerTimeStamp); - Date remoteDate = remoteMessage.getInternalDate(); - if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { - // 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 - // TODO: This strategy has a problem: This will create a second message, - // so that at least temporarily, we will have two messages for what the - // user would think of as one. - Message localMessage = LegacyConversions.makeMessage(context, message); - - // 4c. Upload it - fp.clear(); - fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */); - - // 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 && message.mServerId != null) { - try { - Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); - if (remoteMessage2 != null) { - FetchProfile fp2 = new FetchProfile(); - fp2.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); - final Date remoteDate = remoteMessage2.getInternalDate(); - if (remoteDate != 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 = context.getContentResolver(); - if (deleteMessage) { - resolver.delete(uri, null, null); - } else if (updateMessage) { - ContentValues cv = new ContentValues(); - cv.put(MessageColumns.SERVER_ID, message.mServerId); - cv.put(MessageColumns.SERVER_TIMESTAMP, message.mServerTimeStamp); - resolver.update(uri, cv, null, null); - } - } - - return true; - } - - /** - * A message and numeric uid that's easily sortable - */ - private static class SortableMessage { - private final Message mMessage; - private final long mUid; - - SortableMessage(Message message, long uid) { - mMessage = message; - mUid = uid; - } - } - - private static int searchMailboxImpl(final Context context, final long accountId, - final SearchParams searchParams, final long destMailboxId) throws MessagingException { - final Account account = Account.restoreAccountWithId(context, accountId); - final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId); - final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId); - if (account == null || mailbox == null || destMailbox == null) { - LogUtils.d(Logging.LOG_TAG, "Attempted search for %s " - + "but account or mailbox information was missing", searchParams); - return 0; - } - - // Tell UI that we're loading messages - final ContentValues statusValues = new ContentValues(2); - statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY); - destMailbox.update(context, statusValues); - - Store remoteStore = null; - int numSearchResults = 0; - try { - remoteStore = Store.getInstance(account, context); - final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - remoteFolder.open(OpenMode.READ_WRITE); - - SortableMessage[] sortableMessages = new SortableMessage[0]; - if (searchParams.mOffset == 0) { - // Get the "bare" messages (basically uid) - final Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); - final int remoteCount = remoteMessages.length; - if (remoteCount > 0) { - sortableMessages = new SortableMessage[remoteCount]; - int i = 0; - for (Message msg : remoteMessages) { - sortableMessages[i++] = new SortableMessage(msg, - Long.parseLong(msg.getUid())); - } - // Sort the uid's, most recent first - // Note: Not all servers will be nice and return results in the order of - // request; those that do will see messages arrive from newest to oldest - Arrays.sort(sortableMessages, new Comparator<SortableMessage>() { - @Override - public int compare(SortableMessage lhs, SortableMessage rhs) { - return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0; - } - }); - sSearchResults.put(accountId, sortableMessages); - } - } else { - // It seems odd for this to happen, but if the previous query returned zero results, - // but the UI somehow still attempted to load more, then sSearchResults will have - // a null value for this account. We need to handle this below. - sortableMessages = sSearchResults.get(accountId); - } - - numSearchResults = (sortableMessages != null ? sortableMessages.length : 0); - final int numToLoad = - Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); - destMailbox.updateMessageCount(context, numSearchResults); - if (numToLoad <= 0) { - return 0; - } - - final ArrayList<Message> messageList = new ArrayList<>(); - for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) { - messageList.add(sortableMessages[i].mMessage); - } - // First fetch FLAGS and ENVELOPE. In a second pass, we'll fetch STRUCTURE and - // the first body part. - final FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - Message[] messageArray = messageList.toArray(new Message[messageList.size()]); - - // TODO: We are purposely processing messages with a MessageRetrievalListener here, - // rather than just walking the messageArray after the operation completes. This is so - // that we can immediately update the database so the user can see something useful - // happening, even if the message body has not yet been fetched. - // There are some issues with this approach: - // 1. It means that we have a single thread doing both network and database operations, - // and either can block the other. The database updates could slow down the network - // reads, keeping our network connection open longer than is really necessary. - // 2. We still load all of this data into messageArray, even though it's not used. - // It would be nicer if we had one thread doing the network operation, and a separate - // thread consuming that data and performing the appropriate database work, then - // discarding the data as soon as it is no longer needed. This would reduce our memory - // footprint and potentially allow our network operation to complete faster. - remoteFolder.fetch(messageArray, fp, new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - EmailContent.Message localMessage = new EmailContent.Message(); - - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Save off the mailbox that this message *really* belongs in. - // We need this information if we need to do more lookups - // (like loading attachments) for this message. See b/11294681 - localMessage.mMainMailboxKey = localMessage.mMailboxKey; - localMessage.mMailboxKey = destMailboxId; - // We load 50k or so; maybe it's complete, maybe not... - int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; - // We store the serverId of the source mailbox into protocolSearchInfo - // This will be used by loadMessageForView, etc. to use the proper remote - // folder - localMessage.mProtocolSearchInfo = mailbox.mServerId; - // Commit the message to the local store - Utilities.saveOrUpdate(localMessage, context); - } catch (MessagingException me) { - LogUtils.e(Logging.LOG_TAG, me, - "Error while copying downloaded message."); - } catch (Exception e) { - LogUtils.e(Logging.LOG_TAG, e, - "Error while storing downloaded message."); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - // Now load the structure for all of the messages: - fp.clear(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(messageArray, fp, null); - - // Finally, load the first body part (i.e. message text). - // This means attachment contents are not yet loaded, but that's okay, - // we'll load them as needed, same as in synced messages. - Message[] oneMessageArray = new Message[1]; - for (Message message : messageArray) { - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList<Part> viewables = new ArrayList<>(); - ArrayList<Part> attachments = new ArrayList<>(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - oneMessageArray[0] = message; - for (Part part : viewables) { - fp.clear(); - fp.add(part); - remoteFolder.fetch(oneMessageArray, fp, null); - } - // Store the updated message locally and mark it fully loaded - Utilities.copyOneMessageToProvider(context, message, account, destMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - - } finally { - if (remoteStore != null) { - remoteStore.closeConnections(); - } - // Tell UI that we're done loading messages - statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); - statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); - destMailbox.update(context, statusValues); - } - - return numSearchResults; - } -} |