1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.email.service;
18 
19 import android.app.Service;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.database.Cursor;
26 import android.net.TrafficStats;
27 import android.net.Uri;
28 import android.os.IBinder;
29 import android.os.SystemClock;
30 import android.text.TextUtils;
31 import android.text.format.DateUtils;
32 
33 import com.android.email.DebugUtils;
34 import com.android.email.LegacyConversions;
35 import com.android.email.NotificationController;
36 import com.android.email.NotificationControllerCreatorHolder;
37 import com.android.email.R;
38 import com.android.email.mail.Store;
39 import com.android.email.provider.Utilities;
40 import com.android.emailcommon.Logging;
41 import com.android.emailcommon.TrafficFlags;
42 import com.android.emailcommon.internet.MimeUtility;
43 import com.android.emailcommon.mail.AuthenticationFailedException;
44 import com.android.emailcommon.mail.FetchProfile;
45 import com.android.emailcommon.mail.Flag;
46 import com.android.emailcommon.mail.Folder;
47 import com.android.emailcommon.mail.Folder.FolderType;
48 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
49 import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
50 import com.android.emailcommon.mail.Folder.OpenMode;
51 import com.android.emailcommon.mail.Message;
52 import com.android.emailcommon.mail.MessagingException;
53 import com.android.emailcommon.mail.Part;
54 import com.android.emailcommon.provider.Account;
55 import com.android.emailcommon.provider.EmailContent;
56 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
57 import com.android.emailcommon.provider.EmailContent.MessageColumns;
58 import com.android.emailcommon.provider.EmailContent.SyncColumns;
59 import com.android.emailcommon.provider.Mailbox;
60 import com.android.emailcommon.service.EmailServiceStatus;
61 import com.android.emailcommon.service.SearchParams;
62 import com.android.emailcommon.service.SyncWindow;
63 import com.android.emailcommon.utility.AttachmentUtilities;
64 import com.android.mail.providers.UIProvider;
65 import com.android.mail.utils.LogUtils;
66 
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Comparator;
70 import java.util.Date;
71 import java.util.HashMap;
72 import java.util.List;
73 
74 public class ImapService extends Service {
75     // TODO get these from configurations or settings.
76     private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS;
77     private static final long FULL_SYNC_WINDOW_MILLIS = 7 * DateUtils.DAY_IN_MILLIS;
78     private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS;
79 
80     // The maximum number of messages to fetch in a single command.
81     private static final int MAX_MESSAGES_TO_FETCH = 500;
82     private static final int MINIMUM_MESSAGES_TO_SYNC = 10;
83     private static final int LOAD_MORE_MIN_INCREMENT = 10;
84     private static final int LOAD_MORE_MAX_INCREMENT = 20;
85     private static final long INITIAL_WINDOW_SIZE_INCREASE = 24 * 60 * 60 * 1000;
86 
87     private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
88     private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
89     private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
90 
91     /**
92      * Simple cache for last search result mailbox by account and serverId, since the most common
93      * case will be repeated use of the same mailbox
94      */
95     private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
96     private static String mLastSearchServerId = null;
97     private static Mailbox mLastSearchRemoteMailbox = null;
98 
99     /**
100      * Cache search results by account; this allows for "load more" support without having to
101      * redo the search (which can be quite slow).  SortableMessage is a smallish class, so memory
102      * shouldn't be an issue
103      */
104     private static final HashMap<Long, SortableMessage[]> sSearchResults =
105             new HashMap<Long, SortableMessage[]>();
106 
107     /**
108      * We write this into the serverId field of messages that will never be upsynced.
109      */
110     private static final String LOCAL_SERVERID_PREFIX = "Local-";
111 
112     private static String sMessageDecodeErrorString;
113 
114     /**
115      * Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access
116      * to a Context object.
117      * @return Error string or empty string
118      */
getMessageDecodeErrorString()119     public static String getMessageDecodeErrorString() {
120         return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString;
121     }
122 
123     @Override
onCreate()124     public void onCreate() {
125         super.onCreate();
126 
127         sMessageDecodeErrorString = getString(R.string.message_decode_error);
128     }
129 
130     @Override
onStartCommand(Intent intent, int flags, int startId)131     public int onStartCommand(Intent intent, int flags, int startId) {
132         return Service.START_STICKY;
133     }
134 
135     /**
136      * Create our EmailService implementation here.
137      */
138     private final EmailServiceStub mBinder = new EmailServiceStub() {
139         @Override
140         public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
141             try {
142                 return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
143                         destMailboxId);
144             } catch (MessagingException e) {
145                 // Ignore
146             }
147             return 0;
148         }
149     };
150 
151     @Override
onBind(Intent intent)152     public IBinder onBind(Intent intent) {
153         mBinder.init(this);
154         return mBinder;
155     }
156 
157     /**
158      * Start foreground synchronization of the specified folder. This is called by
159      * synchronizeMailbox or checkMail.
160      * TODO this should use ID's instead of fully-restored objects
161      * @return The status code for whether this operation succeeded.
162      * @throws MessagingException
163      */
synchronizeMailboxSynchronous(Context context, final Account account, final Mailbox folder, final boolean loadMore, final boolean uiRefresh)164     public static synchronized int synchronizeMailboxSynchronous(Context context,
165             final Account account, final Mailbox folder, final boolean loadMore,
166             final boolean uiRefresh) throws MessagingException {
167         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
168         final NotificationController nc =
169                 NotificationControllerCreatorHolder.getInstance(context);
170         Store remoteStore = null;
171         try {
172             remoteStore = Store.getInstance(account, context);
173             processPendingActionsSynchronous(context, account, remoteStore, uiRefresh);
174             synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh);
175             // Clear authentication notification for this account
176             nc.cancelLoginFailedNotification(account.mId);
177         } catch (MessagingException e) {
178             if (Logging.LOGD) {
179                 LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxSynchronous", e);
180             }
181             if (e instanceof AuthenticationFailedException) {
182                 // Generate authentication notification
183                 nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */);
184             }
185             throw e;
186         } finally {
187             if (remoteStore != null) {
188                 remoteStore.closeConnections();
189             }
190         }
191         // TODO: Rather than use exceptions as logic above, return the status and handle it
192         // correctly in caller.
193         return EmailServiceStatus.SUCCESS;
194     }
195 
196     /**
197      * Lightweight record for the first pass of message sync, where I'm just seeing if
198      * the local message requires sync.  Later (for messages that need syncing) we'll do a full
199      * readout from the DB.
200      */
201     private static class LocalMessageInfo {
202         private static final int COLUMN_ID = 0;
203         private static final int COLUMN_FLAG_READ = 1;
204         private static final int COLUMN_FLAG_FAVORITE = 2;
205         private static final int COLUMN_FLAG_LOADED = 3;
206         private static final int COLUMN_SERVER_ID = 4;
207         private static final int COLUMN_FLAGS =  5;
208         private static final int COLUMN_TIMESTAMP =  6;
209         private static final String[] PROJECTION = {
210                 MessageColumns._ID,
211                 MessageColumns.FLAG_READ,
212                 MessageColumns.FLAG_FAVORITE,
213                 MessageColumns.FLAG_LOADED,
214                 SyncColumns.SERVER_ID,
215                 MessageColumns.FLAGS,
216                 MessageColumns.TIMESTAMP
217         };
218 
219         final long mId;
220         final boolean mFlagRead;
221         final boolean mFlagFavorite;
222         final int mFlagLoaded;
223         final String mServerId;
224         final int mFlags;
225         final long mTimestamp;
226 
LocalMessageInfo(Cursor c)227         public LocalMessageInfo(Cursor c) {
228             mId = c.getLong(COLUMN_ID);
229             mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
230             mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
231             mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
232             mServerId = c.getString(COLUMN_SERVER_ID);
233             mFlags = c.getInt(COLUMN_FLAGS);
234             mTimestamp = c.getLong(COLUMN_TIMESTAMP);
235             // Note: mailbox key and account key not needed - they are projected for the SELECT
236         }
237     }
238 
239     private static class OldestTimestampInfo {
240         private static final int COLUMN_OLDEST_TIMESTAMP = 0;
241         private static final String[] PROJECTION = new String[] {
242             "MIN(" + MessageColumns.TIMESTAMP + ")"
243         };
244     }
245 
246     /**
247      * Load the structure and body of messages not yet synced
248      * @param account the account we're syncing
249      * @param remoteFolder the (open) Folder we're working on
250      * @param messages an array of Messages we've got headers for
251      * @param toMailbox the destination mailbox we're syncing
252      * @throws MessagingException
253      */
loadUnsyncedMessages(final Context context, final Account account, Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)254     static void loadUnsyncedMessages(final Context context, final Account account,
255             Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)
256             throws MessagingException {
257 
258         FetchProfile fp = new FetchProfile();
259         fp.add(FetchProfile.Item.STRUCTURE);
260         remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
261         Message [] oneMessageArray = new Message[1];
262         for (Message message : messages) {
263             // Build a list of parts we are interested in. Text parts will be downloaded
264             // right now, attachments will be left for later.
265             ArrayList<Part> viewables = new ArrayList<Part>();
266             ArrayList<Part> attachments = new ArrayList<Part>();
267             MimeUtility.collectParts(message, viewables, attachments);
268             // Download the viewables immediately
269             oneMessageArray[0] = message;
270             for (Part part : viewables) {
271                 fp.clear();
272                 fp.add(part);
273                 remoteFolder.fetch(oneMessageArray, fp, null);
274             }
275             // Store the updated message locally and mark it fully loaded
276             Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
277                     EmailContent.Message.FLAG_LOADED_COMPLETE);
278         }
279     }
280 
downloadFlagAndEnvelope(final Context context, final Account account, final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)281     public static void downloadFlagAndEnvelope(final Context context, final Account account,
282             final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
283             HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
284             throws MessagingException {
285         FetchProfile fp = new FetchProfile();
286         fp.add(FetchProfile.Item.FLAGS);
287         fp.add(FetchProfile.Item.ENVELOPE);
288 
289         final HashMap<String, LocalMessageInfo> localMapCopy;
290         if (localMessageMap != null)
291             localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
292         else {
293             localMapCopy = new HashMap<String, LocalMessageInfo>();
294         }
295 
296         remoteFolder.fetch(unsyncedMessages.toArray(new Message[unsyncedMessages.size()]), fp,
297                 new MessageRetrievalListener() {
298                     @Override
299                     public void messageRetrieved(Message message) {
300                         try {
301                             // Determine if the new message was already known (e.g. partial)
302                             // And create or reload the full message info
303                             final LocalMessageInfo localMessageInfo =
304                                     localMapCopy.get(message.getUid());
305                             final boolean localExists = localMessageInfo != null;
306 
307                             if (!localExists && message.isSet(Flag.DELETED)) {
308                                 // This is a deleted message that we don't have locally, so don't
309                                 // create it
310                                 return;
311                             }
312 
313                             final EmailContent.Message localMessage;
314                             if (!localExists) {
315                                 localMessage = new EmailContent.Message();
316                             } else {
317                                 localMessage = EmailContent.Message.restoreMessageWithId(
318                                         context, localMessageInfo.mId);
319                             }
320 
321                             if (localMessage != null) {
322                                 try {
323                                     // Copy the fields that are available into the message
324                                     LegacyConversions.updateMessageFields(localMessage,
325                                             message, account.mId, mailbox.mId);
326                                     // Commit the message to the local store
327                                     Utilities.saveOrUpdate(localMessage, context);
328                                     // Track the "new" ness of the downloaded message
329                                     if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
330                                         unseenMessages.add(localMessage.mId);
331                                     }
332                                 } catch (MessagingException me) {
333                                     LogUtils.e(Logging.LOG_TAG,
334                                             "Error while copying downloaded message." + me);
335                                 }
336                             }
337                         }
338                         catch (Exception e) {
339                             LogUtils.e(Logging.LOG_TAG,
340                                     "Error while storing downloaded message." + e.toString());
341                         }
342                     }
343 
344                     @Override
345                     public void loadAttachmentProgress(int progress) {
346                     }
347                 });
348 
349     }
350 
351     /**
352      * Synchronizer for IMAP.
353      *
354      * TODO Break this method up into smaller chunks.
355      *
356      * @param account the account to sync
357      * @param mailbox the mailbox to sync
358      * @param loadMore whether we should be loading more older messages
359      * @param uiRefresh whether this request is in response to a user action
360      * @throws MessagingException
361      */
synchronizeMailboxGeneric(final Context context, final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore, final boolean uiRefresh)362     private synchronized static void synchronizeMailboxGeneric(final Context context,
363             final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore,
364             final boolean uiRefresh)
365             throws MessagingException {
366 
367         LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxGeneric " + account + " " + mailbox + " "
368                 + loadMore + " " + uiRefresh);
369 
370         final ArrayList<Long> unseenMessages = new ArrayList<Long>();
371 
372         ContentResolver resolver = context.getContentResolver();
373 
374         // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
375         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
376             return;
377         }
378 
379         // 1. Figure out what our sync window should be.
380         long endDate;
381 
382         // We will do a full sync if the user has actively requested a sync, or if it has been
383         // too long since the last full sync.
384         // If we have rebooted since the last full sync, then we may get a negative
385         // timeSinceLastFullSync. In this case, we don't know how long it's been since the last
386         // full sync so we should perform the full sync.
387         final long timeSinceLastFullSync = SystemClock.elapsedRealtime() -
388                 mailbox.mLastFullSyncTime;
389         final boolean fullSync = (uiRefresh || loadMore ||
390                 timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS || timeSinceLastFullSync < 0);
391 
392         if (account.mSyncLookback == SyncWindow.SYNC_WINDOW_ALL) {
393             // This is really for testing. There is no UI that allows setting the sync window for
394             // IMAP, but it can be set by sending a special intent to AccountSetupFinal activity.
395             endDate = 0;
396         } else if (fullSync) {
397             // Find the oldest message in the local store. We need our time window to include
398             // all messages that are currently present locally.
399             endDate = System.currentTimeMillis() - FULL_SYNC_WINDOW_MILLIS;
400             Cursor localOldestCursor = null;
401             try {
402                 // b/11520812 Ignore message with timestamp = 0 (which includes NULL)
403                 localOldestCursor = resolver.query(EmailContent.Message.CONTENT_URI,
404                         OldestTimestampInfo.PROJECTION,
405                         EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + " AND " +
406                                 MessageColumns.MAILBOX_KEY + "=? AND " +
407                                 MessageColumns.TIMESTAMP + "!=0",
408                         new String[] {String.valueOf(account.mId), String.valueOf(mailbox.mId)},
409                         null);
410                 if (localOldestCursor != null && localOldestCursor.moveToFirst()) {
411                     long oldestLocalMessageDate = localOldestCursor.getLong(
412                             OldestTimestampInfo.COLUMN_OLDEST_TIMESTAMP);
413                     if (oldestLocalMessageDate > 0) {
414                         endDate = Math.min(endDate, oldestLocalMessageDate);
415                         LogUtils.d(
416                                 Logging.LOG_TAG, "oldest local message " + oldestLocalMessageDate);
417                     }
418                 }
419             } finally {
420                 if (localOldestCursor != null) {
421                     localOldestCursor.close();
422                 }
423             }
424             LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate);
425         } else {
426             // We are doing a frequent, quick sync. This only syncs a small time window, so that
427             // we wil get any new messages, but not spend a lot of bandwidth downloading
428             // messageIds that we most likely already have.
429             endDate = System.currentTimeMillis() - QUICK_SYNC_WINDOW_MILLIS;
430             LogUtils.d(Logging.LOG_TAG, "quick sync: original window: now - " + endDate);
431         }
432 
433         // 2. Open the remote folder and create the remote folder if necessary
434         // The account might have been deleted
435         if (remoteStore == null) {
436             LogUtils.d(Logging.LOG_TAG, "account is apparently deleted");
437             return;
438         }
439         final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
440 
441         // If the folder is a "special" folder we need to see if it exists
442         // on the remote server. It if does not exist we'll try to create it. If we
443         // can't create we'll abort. This will happen on every single Pop3 folder as
444         // designed and on Imap folders during error conditions. This allows us
445         // to treat Pop3 and Imap the same in this code.
446         if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT) {
447             if (!remoteFolder.exists()) {
448                 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
449                     LogUtils.w(Logging.LOG_TAG, "could not create remote folder type %d",
450                         mailbox.mType);
451                     return;
452                 }
453             }
454         }
455         remoteFolder.open(OpenMode.READ_WRITE);
456 
457         // 3. Trash any remote messages that are marked as trashed locally.
458         // TODO - this comment was here, but no code was here.
459 
460         // 4. Get the number of messages on the server.
461         // TODO: this value includes deleted but unpurged messages, and so slightly mismatches
462         // the contents of our DB since we drop deleted messages. Figure out what to do about this.
463         final int remoteMessageCount = remoteFolder.getMessageCount();
464 
465         // 5. Save folder message count locally.
466         mailbox.updateMessageCount(context, remoteMessageCount);
467 
468         // 6. Get all message Ids in our sync window:
469         Message[] remoteMessages;
470         remoteMessages = remoteFolder.getMessages(0, endDate, null);
471         LogUtils.d(Logging.LOG_TAG, "received " + remoteMessages.length + " messages");
472 
473         // 7. See if we need any additional messages beyond our date query range results.
474         // If we do, keep increasing the size of our query window until we have
475         // enough, or until we have all messages in the mailbox.
476         int totalCountNeeded;
477         if (loadMore) {
478             totalCountNeeded = remoteMessages.length + LOAD_MORE_MIN_INCREMENT;
479         } else {
480             totalCountNeeded = remoteMessages.length;
481             if (fullSync && totalCountNeeded < MINIMUM_MESSAGES_TO_SYNC) {
482                 totalCountNeeded = MINIMUM_MESSAGES_TO_SYNC;
483             }
484         }
485         LogUtils.d(Logging.LOG_TAG, "need " + totalCountNeeded + " total");
486 
487         final int additionalMessagesNeeded = totalCountNeeded - remoteMessages.length;
488         if (additionalMessagesNeeded > 0) {
489             LogUtils.d(Logging.LOG_TAG, "trying to get " + additionalMessagesNeeded + " more");
490             long startDate = endDate - 1;
491             Message[] additionalMessages = new Message[0];
492             long windowIncreaseSize = INITIAL_WINDOW_SIZE_INCREASE;
493             while (additionalMessages.length < additionalMessagesNeeded && endDate > 0) {
494                 endDate = endDate - windowIncreaseSize;
495                 if (endDate < 0) {
496                     LogUtils.d(Logging.LOG_TAG, "window size too large, this is the last attempt");
497                     endDate = 0;
498                 }
499                 LogUtils.d(Logging.LOG_TAG,
500                         "requesting additional messages from range " + startDate + " - " + endDate);
501                 additionalMessages = remoteFolder.getMessages(startDate, endDate, null);
502 
503                 // If don't get enough messages with the first window size expansion,
504                 // we need to accelerate rate at which the window expands. Otherwise,
505                 // if there were no messages for several weeks, we'd always end up
506                 // performing dozens of queries.
507                 windowIncreaseSize *= 2;
508             }
509 
510             LogUtils.d(Logging.LOG_TAG, "additionalMessages " + additionalMessages.length);
511             if (additionalMessages.length < additionalMessagesNeeded) {
512                 // We have attempted to load a window that goes all the way back to time zero,
513                 // but we still don't have as many messages as the server says are in the inbox.
514                 // This is not expected to happen.
515                 LogUtils.e(Logging.LOG_TAG, "expected to find " + additionalMessagesNeeded
516                         + " more messages, only got " + additionalMessages.length);
517             }
518             int additionalToKeep = additionalMessages.length;
519             if (additionalMessages.length > LOAD_MORE_MAX_INCREMENT) {
520                 // We have way more additional messages than intended, drop some of them.
521                 // The last messages are the most recent, so those are the ones we need to keep.
522                 additionalToKeep = LOAD_MORE_MAX_INCREMENT;
523             }
524 
525             // Copy the messages into one array.
526             Message[] allMessages = new Message[remoteMessages.length + additionalToKeep];
527             System.arraycopy(remoteMessages, 0, allMessages, 0, remoteMessages.length);
528             // additionalMessages may have more than we need, only copy the last
529             // several. These are the most recent messages in that set because
530             // of the way IMAP server returns messages.
531             System.arraycopy(additionalMessages, additionalMessages.length - additionalToKeep,
532                     allMessages, remoteMessages.length, additionalToKeep);
533             remoteMessages = allMessages;
534         }
535 
536         // 8. Get the all of the local messages within the sync window, and create
537         // an index of the uids.
538         // The IMAP query for messages ignores time, and only looks at the date part of the endDate.
539         // So if we query for messages since Aug 11 at 3:00 PM, we can get messages from any time
540         // on Aug 11. Our IMAP query results can include messages up to 24 hours older than endDate,
541         // or up to 25 hours older at a daylight savings transition.
542         // It is important that we have the Id of any local message that could potentially be
543         // returned by the IMAP query, or we will create duplicate copies of the same messages.
544         // So we will increase our local query range by this much.
545         // Note that this complicates deletion: It's not okay to delete anything that is in the
546         // localMessageMap but not in the remote result, because we know that we may be getting
547         // Ids of local messages that are outside the IMAP query window.
548         Cursor localUidCursor = null;
549         HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
550         try {
551             // FLAG: There is a problem that causes us to store the wrong date on some messages,
552             // so messages get a date of zero. If we filter these messages out and don't put them
553             // in our localMessageMap, then we'll end up loading the same message again.
554             // See b/10508861
555 //            final long queryEndDate = endDate - DateUtils.DAY_IN_MILLIS - DateUtils.HOUR_IN_MILLIS;
556             final long queryEndDate = 0;
557             localUidCursor = resolver.query(
558                     EmailContent.Message.CONTENT_URI,
559                     LocalMessageInfo.PROJECTION,
560                     EmailContent.MessageColumns.ACCOUNT_KEY + "=?"
561                             + " AND " + MessageColumns.MAILBOX_KEY + "=?"
562                             + " AND " + MessageColumns.TIMESTAMP + ">=?",
563                     new String[] {
564                             String.valueOf(account.mId),
565                             String.valueOf(mailbox.mId),
566                             String.valueOf(queryEndDate) },
567                     null);
568             while (localUidCursor.moveToNext()) {
569                 LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
570                 // If the message has no server id, it's local only. This should only happen for
571                 // mail created on the client that has failed to upsync. We want to ignore such
572                 // mail during synchronization (i.e. leave it as-is and let the next sync try again
573                 // to upsync).
574                 if (!TextUtils.isEmpty(info.mServerId)) {
575                     localMessageMap.put(info.mServerId, info);
576                 }
577             }
578         } finally {
579             if (localUidCursor != null) {
580                 localUidCursor.close();
581             }
582         }
583 
584         // 9. Get a list of the messages that are in the remote list but not on the
585         // local store, or messages that are in the local store but failed to download
586         // on the last sync. These are the new messages that we will download.
587         // Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
588         // because they are locally deleted and we don't need or want the old message from
589         // the server.
590         final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
591         final HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
592         // Process the messages in the reverse order we received them in. This means that
593         // we load the most recent one first, which gives a better user experience.
594         for (int i = remoteMessages.length - 1; i >= 0; i--) {
595             Message message = remoteMessages[i];
596             LogUtils.d(Logging.LOG_TAG, "remote message " + message.getUid());
597             remoteUidMap.put(message.getUid(), message);
598 
599             LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
600 
601             // localMessage == null -> message has never been created (not even headers)
602             // mFlagLoaded = UNLOADED -> message created, but none of body loaded
603             // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
604             // mFlagLoaded = COMPLETE -> message body has been completely loaded
605             // mFlagLoaded = DELETED -> message has been deleted
606             // Only the first two of these are "unsynced", so let's retrieve them
607             if (localMessage == null ||
608                     (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
609                     (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
610                 unsyncedMessages.add(message);
611             }
612         }
613 
614         // 10. Download basic info about the new/unloaded messages (if any)
615         /*
616          * Fetch the flags and envelope only of the new messages. This is intended to get us
617          * critical data as fast as possible, and then we'll fill in the details.
618          */
619         if (unsyncedMessages.size() > 0) {
620             downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
621                     localMessageMap, unseenMessages);
622         }
623 
624         // 11. Refresh the flags for any messages in the local store that we didn't just download.
625         // TODO This is a bit wasteful because we're also updating any messages we already did get
626         // the flags and envelope for previously.
627         // TODO: the fetch() function, and others, should take List<>s of messages, not
628         // arrays of messages.
629         FetchProfile fp = new FetchProfile();
630         fp.add(FetchProfile.Item.FLAGS);
631         if (remoteMessages.length > MAX_MESSAGES_TO_FETCH) {
632             List<Message> remoteMessageList = Arrays.asList(remoteMessages);
633             for (int start = 0; start < remoteMessageList.size(); start += MAX_MESSAGES_TO_FETCH) {
634                 int end = start + MAX_MESSAGES_TO_FETCH;
635                 if (end >= remoteMessageList.size()) {
636                     end = remoteMessageList.size() - 1;
637                 }
638                 List<Message> chunk = remoteMessageList.subList(start, end);
639                 final Message[] partialArray = chunk.toArray(new Message[chunk.size()]);
640                 // Fetch this one chunk of messages
641                 remoteFolder.fetch(partialArray, fp, null);
642             }
643         } else {
644             remoteFolder.fetch(remoteMessages, fp, null);
645         }
646         boolean remoteSupportsSeen = false;
647         boolean remoteSupportsFlagged = false;
648         boolean remoteSupportsAnswered = false;
649         for (Flag flag : remoteFolder.getPermanentFlags()) {
650             if (flag == Flag.SEEN) {
651                 remoteSupportsSeen = true;
652             }
653             if (flag == Flag.FLAGGED) {
654                 remoteSupportsFlagged = true;
655             }
656             if (flag == Flag.ANSWERED) {
657                 remoteSupportsAnswered = true;
658             }
659         }
660 
661         // 12. Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
662         if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
663             for (Message remoteMessage : remoteMessages) {
664                 LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
665                 if (localMessageInfo == null) {
666                     continue;
667                 }
668                 boolean localSeen = localMessageInfo.mFlagRead;
669                 boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
670                 boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
671                 boolean localFlagged = localMessageInfo.mFlagFavorite;
672                 boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
673                 boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
674                 int localFlags = localMessageInfo.mFlags;
675                 boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
676                 boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
677                 boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
678                 if (newSeen || newFlagged || newAnswered) {
679                     Uri uri = ContentUris.withAppendedId(
680                             EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
681                     ContentValues updateValues = new ContentValues();
682                     updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
683                     updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
684                     if (remoteAnswered) {
685                         localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
686                     } else {
687                         localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
688                     }
689                     updateValues.put(MessageColumns.FLAGS, localFlags);
690                     resolver.update(uri, updateValues, null, null);
691                 }
692             }
693         }
694 
695         // 12.5 Remove messages that are marked as deleted so that we drop them from the DB in the
696         // next step
697         for (final Message remoteMessage : remoteMessages) {
698             if (remoteMessage.isSet(Flag.DELETED)) {
699                 remoteUidMap.remove(remoteMessage.getUid());
700                 unsyncedMessages.remove(remoteMessage);
701             }
702         }
703 
704         // 13. Remove messages that are in the local store and in the current sync window,
705         // but no longer on the remote store. Note that localMessageMap can contain messages
706         // that are not actually in our sync window. We need to check the timestamp to ensure
707         // that it is before deleting.
708         for (final LocalMessageInfo info : localMessageMap.values()) {
709             // If this message is inside our sync window, and we cannot find it in our list
710             // of remote messages, then we know it's been deleted from the server.
711             if (info.mTimestamp >= endDate && !remoteUidMap.containsKey(info.mServerId)) {
712                 // Delete associated data (attachment files)
713                 // Attachment & Body records are auto-deleted when we delete the Message record
714                 AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, info.mId);
715 
716                 // Delete the message itself
717                 final Uri uriToDelete = ContentUris.withAppendedId(
718                         EmailContent.Message.CONTENT_URI, info.mId);
719                 resolver.delete(uriToDelete, null, null);
720 
721                 // Delete extra rows (e.g. updated or deleted)
722                 final Uri updateRowToDelete = ContentUris.withAppendedId(
723                         EmailContent.Message.UPDATED_CONTENT_URI, info.mId);
724                 resolver.delete(updateRowToDelete, null, null);
725                 final Uri deleteRowToDelete = ContentUris.withAppendedId(
726                         EmailContent.Message.DELETED_CONTENT_URI, info.mId);
727                 resolver.delete(deleteRowToDelete, null, null);
728             }
729         }
730 
731         loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
732 
733         if (fullSync) {
734             mailbox.updateLastFullSyncTime(context, SystemClock.elapsedRealtime());
735         }
736 
737         // 14. Clean up and report results
738         remoteFolder.close(false);
739     }
740 
741     /**
742      * Find messages in the updated table that need to be written back to server.
743      *
744      * Handles:
745      *   Read/Unread
746      *   Flagged
747      *   Append (upload)
748      *   Move To Trash
749      *   Empty trash
750      * TODO:
751      *   Move
752      *
753      * @param account the account to scan for pending actions
754      * @throws MessagingException
755      */
processPendingActionsSynchronous(Context context, Account account, Store remoteStore, boolean manualSync)756     private static void processPendingActionsSynchronous(Context context, Account account,
757             Store remoteStore, boolean manualSync)
758             throws MessagingException {
759         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
760         String[] accountIdArgs = new String[] { Long.toString(account.mId) };
761 
762         // Handle deletes first, it's always better to get rid of things first
763         processPendingDeletesSynchronous(context, account, remoteStore, accountIdArgs);
764 
765         // Handle uploads (currently, only to sent messages)
766         processPendingUploadsSynchronous(context, account, remoteStore, accountIdArgs, manualSync);
767 
768         // Now handle updates / upsyncs
769         processPendingUpdatesSynchronous(context, account, remoteStore, accountIdArgs);
770     }
771 
772     /**
773      * Get the mailbox corresponding to the remote location of a message; this will normally be
774      * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
775      * by serverId.
776      *
777      * @param message the message in question
778      * @return the mailbox in which the message resides on the server
779      */
getRemoteMailboxForMessage( Context context, EmailContent.Message message)780     private static Mailbox getRemoteMailboxForMessage(
781             Context context, EmailContent.Message message) {
782         // If this is a search result, use the protocolSearchInfo field to get the server info
783         if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
784             long accountKey = message.mAccountKey;
785             String protocolSearchInfo = message.mProtocolSearchInfo;
786             if (accountKey == mLastSearchAccountKey &&
787                     protocolSearchInfo.equals(mLastSearchServerId)) {
788                 return mLastSearchRemoteMailbox;
789             }
790             Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
791                     Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
792                     new String[] {protocolSearchInfo, Long.toString(accountKey) },
793                     null);
794             try {
795                 if (c.moveToNext()) {
796                     Mailbox mailbox = new Mailbox();
797                     mailbox.restore(c);
798                     mLastSearchAccountKey = accountKey;
799                     mLastSearchServerId = protocolSearchInfo;
800                     mLastSearchRemoteMailbox = mailbox;
801                     return mailbox;
802                 } else {
803                     return null;
804                 }
805             } finally {
806                 c.close();
807             }
808         } else {
809             return Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
810         }
811     }
812 
813     /**
814      * Scan for messages that are in the Message_Deletes table, look for differences that
815      * we can deal with, and do the work.
816      */
processPendingDeletesSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs)817     private static void processPendingDeletesSynchronous(Context context, Account account,
818             Store remoteStore, String[] accountIdArgs) {
819         Cursor deletes = context.getContentResolver().query(
820                 EmailContent.Message.DELETED_CONTENT_URI,
821                 EmailContent.Message.CONTENT_PROJECTION,
822                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
823                 EmailContent.MessageColumns.MAILBOX_KEY);
824         long lastMessageId = -1;
825         try {
826             // loop through messages marked as deleted
827             while (deletes.moveToNext()) {
828                 EmailContent.Message oldMessage =
829                         EmailContent.getContent(context, deletes, EmailContent.Message.class);
830 
831                 if (oldMessage != null) {
832                     lastMessageId = oldMessage.mId;
833 
834                     Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage);
835                     if (mailbox == null) {
836                         continue; // Mailbox removed. Move to the next message.
837                     }
838                     final boolean deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
839 
840                     // Dispatch here for specific change types
841                     if (deleteFromTrash) {
842                         // Move message to trash
843                         processPendingDeleteFromTrash(remoteStore, mailbox, oldMessage);
844                     }
845 
846                     // Finally, delete the update
847                     Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
848                             oldMessage.mId);
849                     context.getContentResolver().delete(uri, null, null);
850                 }
851             }
852         } catch (MessagingException me) {
853             // Presumably an error here is an account connection failure, so there is
854             // no point in continuing through the rest of the pending updates.
855             if (DebugUtils.DEBUG) {
856                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending delete for id="
857                         + lastMessageId + ": " + me);
858             }
859         } finally {
860             deletes.close();
861         }
862     }
863 
864     /**
865      * Scan for messages that are in Sent, and are in need of upload,
866      * and send them to the server. "In need of upload" is defined as:
867      *  serverId == null (no UID has been assigned)
868      * or
869      *  message is in the updated list
870      *
871      * Note we also look for messages that are moving from drafts->outbox->sent. They never
872      * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
873      * uploaded directly to the Sent folder.
874      */
processPendingUploadsSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs, boolean manualSync)875     private static void processPendingUploadsSynchronous(Context context, Account account,
876             Store remoteStore, String[] accountIdArgs, boolean manualSync) {
877         ContentResolver resolver = context.getContentResolver();
878         // Find the Sent folder (since that's all we're uploading for now
879         // TODO: Upsync for all folders? (In case a user moves mail from Sent before it is
880         // handled. Also, this would generically solve allowing drafts to upload.)
881         Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
882                 MailboxColumns.ACCOUNT_KEY + "=?"
883                 + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
884                 accountIdArgs, null);
885         long lastMessageId = -1;
886         try {
887             while (mailboxes.moveToNext()) {
888                 long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
889                 String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
890                 // Demand load mailbox
891                 Mailbox mailbox = null;
892 
893                 // First handle the "new" messages (serverId == null)
894                 Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
895                         EmailContent.Message.ID_PROJECTION,
896                         MessageColumns.MAILBOX_KEY + "=?"
897                         + " and (" + MessageColumns.SERVER_ID + " is null"
898                         + " or " + MessageColumns.SERVER_ID + "=''" + ")",
899                         mailboxKeyArgs,
900                         null);
901                 try {
902                     while (upsyncs1.moveToNext()) {
903                         // Load the remote store if it will be needed
904                         if (remoteStore == null) {
905                             remoteStore = Store.getInstance(account, context);
906                         }
907                         // Load the mailbox if it will be needed
908                         if (mailbox == null) {
909                             mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
910                             if (mailbox == null) {
911                                 continue; // Mailbox removed. Move to the next message.
912                             }
913                         }
914                         // upsync the message
915                         long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
916                         lastMessageId = id;
917                         processUploadMessage(context, remoteStore, mailbox, id, manualSync);
918                     }
919                 } finally {
920                     if (upsyncs1 != null) {
921                         upsyncs1.close();
922                     }
923                     if (remoteStore != null) {
924                         remoteStore.closeConnections();
925                     }
926                 }
927             }
928         } catch (MessagingException me) {
929             // Presumably an error here is an account connection failure, so there is
930             // no point in continuing through the rest of the pending updates.
931             if (DebugUtils.DEBUG) {
932                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
933                         + lastMessageId + ": " + me);
934             }
935         } finally {
936             if (mailboxes != null) {
937                 mailboxes.close();
938             }
939         }
940     }
941 
942     /**
943      * Scan for messages that are in the Message_Updates table, look for differences that
944      * we can deal with, and do the work.
945      */
processPendingUpdatesSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs)946     private static void processPendingUpdatesSynchronous(Context context, Account account,
947             Store remoteStore, String[] accountIdArgs) {
948         ContentResolver resolver = context.getContentResolver();
949         Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
950                 EmailContent.Message.CONTENT_PROJECTION,
951                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
952                 EmailContent.MessageColumns.MAILBOX_KEY);
953         long lastMessageId = -1;
954         try {
955             // Demand load mailbox (note order-by to reduce thrashing here)
956             Mailbox mailbox = null;
957             // loop through messages marked as needing updates
958             while (updates.moveToNext()) {
959                 boolean changeMoveToTrash = false;
960                 boolean changeRead = false;
961                 boolean changeFlagged = false;
962                 boolean changeMailbox = false;
963                 boolean changeAnswered = false;
964 
965                 EmailContent.Message oldMessage =
966                         EmailContent.getContent(context, updates, EmailContent.Message.class);
967                 lastMessageId = oldMessage.mId;
968                 EmailContent.Message newMessage =
969                         EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
970                 if (newMessage != null) {
971                     mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey);
972                     if (mailbox == null) {
973                         continue; // Mailbox removed. Move to the next message.
974                     }
975                     if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
976                         if (mailbox.mType == Mailbox.TYPE_TRASH) {
977                             changeMoveToTrash = true;
978                         } else {
979                             changeMailbox = true;
980                         }
981                     }
982                     changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
983                     changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
984                     changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
985                             (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
986                 }
987 
988                 // Load the remote store if it will be needed
989                 if (remoteStore == null &&
990                         (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
991                                 changeAnswered)) {
992                     remoteStore = Store.getInstance(account, context);
993                 }
994 
995                 // Dispatch here for specific change types
996                 if (changeMoveToTrash) {
997                     // Move message to trash
998                     processPendingMoveToTrash(context, remoteStore, mailbox, oldMessage,
999                             newMessage);
1000                 } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
1001                     processPendingDataChange(context, remoteStore, mailbox, changeRead,
1002                             changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage);
1003                 }
1004 
1005                 // Finally, delete the update
1006                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
1007                         oldMessage.mId);
1008                 resolver.delete(uri, null, null);
1009             }
1010 
1011         } catch (MessagingException me) {
1012             // Presumably an error here is an account connection failure, so there is
1013             // no point in continuing through the rest of the pending updates.
1014             if (DebugUtils.DEBUG) {
1015                 LogUtils.d(Logging.LOG_TAG, "Unable to process pending update for id="
1016                         + lastMessageId + ": " + me);
1017             }
1018         } finally {
1019             updates.close();
1020         }
1021     }
1022 
1023     /**
1024      * Upsync an entire message. This must also unwind whatever triggered it (either by
1025      * updating the serverId, or by deleting the update record, or it's going to keep happening
1026      * over and over again.
1027      *
1028      * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload.
1029      * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select
1030      * only the Drafts and Sent folders, this can happen when the update record and the current
1031      * record mismatch. In this case, we let the update record remain, because the filters
1032      * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
1033      * appropriately.
1034      *
1035      * @param mailbox the actual mailbox
1036      */
processUploadMessage(Context context, Store remoteStore, Mailbox mailbox, long messageId, boolean manualSync)1037     private static void processUploadMessage(Context context, Store remoteStore, Mailbox mailbox,
1038             long messageId, boolean manualSync)
1039             throws MessagingException {
1040         EmailContent.Message newMessage =
1041                 EmailContent.Message.restoreMessageWithId(context, messageId);
1042         final boolean deleteUpdate;
1043         if (newMessage == null) {
1044             deleteUpdate = true;
1045             LogUtils.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
1046         } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
1047             deleteUpdate = false;
1048             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
1049         } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
1050             deleteUpdate = false;
1051             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
1052         } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
1053             deleteUpdate = false;
1054             LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
1055         } else if (newMessage.mMailboxKey != mailbox.mId) {
1056             deleteUpdate = false;
1057             LogUtils.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
1058         } else {
1059             LogUtils.d(Logging.LOG_TAG, "Upsync triggered for message id=" + messageId);
1060             deleteUpdate =
1061                     processPendingAppend(context, remoteStore, mailbox, newMessage, manualSync);
1062         }
1063         if (deleteUpdate) {
1064             // Finally, delete the update (if any)
1065             Uri uri = ContentUris.withAppendedId(
1066                     EmailContent.Message.UPDATED_CONTENT_URI, messageId);
1067             context.getContentResolver().delete(uri, null, null);
1068         }
1069     }
1070 
1071     /**
1072      * Upsync changes to read, flagged, or mailbox
1073      *
1074      * @param remoteStore the remote store for this mailbox
1075      * @param mailbox the mailbox the message is stored in
1076      * @param changeRead whether the message's read state has changed
1077      * @param changeFlagged whether the message's flagged state has changed
1078      * @param changeMailbox whether the message's mailbox has changed
1079      * @param oldMessage the message in it's pre-change state
1080      * @param newMessage the current version of the message
1081      */
processPendingDataChange(final Context context, Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1082     private static void processPendingDataChange(final Context context, Store remoteStore,
1083             Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox,
1084             boolean changeAnswered, EmailContent.Message oldMessage,
1085             final EmailContent.Message newMessage) throws MessagingException {
1086         // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
1087         // being moved
1088         Mailbox newMailbox = mailbox;
1089         // Mailbox is the original remote mailbox (the one we're acting on)
1090         mailbox = getRemoteMailboxForMessage(context, oldMessage);
1091 
1092         // 0. No remote update if the message is local-only
1093         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1094                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
1095             return;
1096         }
1097 
1098         // 1. No remote update for DRAFTS or OUTBOX
1099         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
1100             return;
1101         }
1102 
1103         // 2. Open the remote store & folder
1104         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1105         if (!remoteFolder.exists()) {
1106             return;
1107         }
1108         remoteFolder.open(OpenMode.READ_WRITE);
1109         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1110             return;
1111         }
1112 
1113         // 3. Finally, apply the changes to the message
1114         Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
1115         if (remoteMessage == null) {
1116             return;
1117         }
1118         if (DebugUtils.DEBUG) {
1119             LogUtils.d(Logging.LOG_TAG,
1120                     "Update for msg id=" + newMessage.mId
1121                     + " read=" + newMessage.mFlagRead
1122                     + " flagged=" + newMessage.mFlagFavorite
1123                     + " answered="
1124                     + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
1125                     + " new mailbox=" + newMessage.mMailboxKey);
1126         }
1127         Message[] messages = new Message[] { remoteMessage };
1128         if (changeRead) {
1129             remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
1130         }
1131         if (changeFlagged) {
1132             remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
1133         }
1134         if (changeAnswered) {
1135             remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
1136                     (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
1137         }
1138         if (changeMailbox) {
1139             Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
1140             if (!remoteFolder.exists()) {
1141                 return;
1142             }
1143             // We may need the message id to search for the message in the destination folder
1144             remoteMessage.setMessageId(newMessage.mMessageId);
1145             // Copy the message to its new folder
1146             remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
1147                 @Override
1148                 public void onMessageUidChange(Message message, String newUid) {
1149                     ContentValues cv = new ContentValues();
1150                     cv.put(MessageColumns.SERVER_ID, newUid);
1151                     // We only have one message, so, any updates _must_ be for it. Otherwise,
1152                     // we'd have to cycle through to find the one with the same server ID.
1153                     context.getContentResolver().update(ContentUris.withAppendedId(
1154                             EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
1155                 }
1156 
1157                 @Override
1158                 public void onMessageNotFound(Message message) {
1159                 }
1160             });
1161             // Delete the message from the remote source folder
1162             remoteMessage.setFlag(Flag.DELETED, true);
1163             remoteFolder.expunge();
1164         }
1165         remoteFolder.close(false);
1166     }
1167 
1168     /**
1169      * Process a pending trash message command.
1170      *
1171      * @param remoteStore the remote store we're working in
1172      * @param newMailbox The local trash mailbox
1173      * @param oldMessage The message copy that was saved in the updates shadow table
1174      * @param newMessage The message that was moved to the mailbox
1175      */
processPendingMoveToTrash(final Context context, Store remoteStore, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1176     private static void processPendingMoveToTrash(final Context context, Store remoteStore,
1177             Mailbox newMailbox, EmailContent.Message oldMessage,
1178             final EmailContent.Message newMessage) throws MessagingException {
1179 
1180         // 0. No remote move if the message is local-only
1181         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1182                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
1183             return;
1184         }
1185 
1186         // 1. Escape early if we can't find the local mailbox
1187         // TODO smaller projection here
1188         Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage);
1189         if (oldMailbox == null) {
1190             // can't find old mailbox, it may have been deleted.  just return.
1191             return;
1192         }
1193         // 2. We don't support delete-from-trash here
1194         if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
1195             return;
1196         }
1197 
1198         // The rest of this method handles server-side deletion
1199 
1200         // 4.  Find the remote mailbox (that we deleted from), and open it
1201         Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
1202         if (!remoteFolder.exists()) {
1203             return;
1204         }
1205 
1206         remoteFolder.open(OpenMode.READ_WRITE);
1207         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1208             remoteFolder.close(false);
1209             return;
1210         }
1211 
1212         // 5. Find the remote original message
1213         Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
1214         if (remoteMessage == null) {
1215             remoteFolder.close(false);
1216             return;
1217         }
1218 
1219         // 6. Find the remote trash folder, and create it if not found
1220         Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
1221         if (!remoteTrashFolder.exists()) {
1222             /*
1223              * If the remote trash folder doesn't exist we try to create it.
1224              */
1225             remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
1226         }
1227 
1228         // 7. Try to copy the message into the remote trash folder
1229         // Note, this entire section will be skipped for POP3 because there's no remote trash
1230         if (remoteTrashFolder.exists()) {
1231             /*
1232              * Because remoteTrashFolder may be new, we need to explicitly open it
1233              */
1234             remoteTrashFolder.open(OpenMode.READ_WRITE);
1235             if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1236                 remoteFolder.close(false);
1237                 remoteTrashFolder.close(false);
1238                 return;
1239             }
1240 
1241             remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
1242                     new Folder.MessageUpdateCallbacks() {
1243                 @Override
1244                 public void onMessageUidChange(Message message, String newUid) {
1245                     // update the UID in the local trash folder, because some stores will
1246                     // have to change it when copying to remoteTrashFolder
1247                     ContentValues cv = new ContentValues();
1248                     cv.put(MessageColumns.SERVER_ID, newUid);
1249                     context.getContentResolver().update(newMessage.getUri(), cv, null, null);
1250                 }
1251 
1252                 /**
1253                  * This will be called if the deleted message doesn't exist and can't be
1254                  * deleted (e.g. it was already deleted from the server.)  In this case,
1255                  * attempt to delete the local copy as well.
1256                  */
1257                 @Override
1258                 public void onMessageNotFound(Message message) {
1259                     context.getContentResolver().delete(newMessage.getUri(), null, null);
1260                 }
1261             });
1262             remoteTrashFolder.close(false);
1263         }
1264 
1265         // 8. Delete the message from the remote source folder
1266         remoteMessage.setFlag(Flag.DELETED, true);
1267         remoteFolder.expunge();
1268         remoteFolder.close(false);
1269     }
1270 
1271     /**
1272      * Process a pending trash message command.
1273      *
1274      * @param remoteStore the remote store we're working in
1275      * @param oldMailbox The local trash mailbox
1276      * @param oldMessage The message that was deleted from the trash
1277      */
processPendingDeleteFromTrash(Store remoteStore, Mailbox oldMailbox, EmailContent.Message oldMessage)1278     private static void processPendingDeleteFromTrash(Store remoteStore,
1279             Mailbox oldMailbox, EmailContent.Message oldMessage)
1280             throws MessagingException {
1281 
1282         // 1. We only support delete-from-trash here
1283         if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
1284             return;
1285         }
1286 
1287         // 2.  Find the remote trash folder (that we are deleting from), and open it
1288         Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
1289         if (!remoteTrashFolder.exists()) {
1290             return;
1291         }
1292 
1293         remoteTrashFolder.open(OpenMode.READ_WRITE);
1294         if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1295             remoteTrashFolder.close(false);
1296             return;
1297         }
1298 
1299         // 3. Find the remote original message
1300         Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
1301         if (remoteMessage == null) {
1302             remoteTrashFolder.close(false);
1303             return;
1304         }
1305 
1306         // 4. Delete the message from the remote trash folder
1307         remoteMessage.setFlag(Flag.DELETED, true);
1308         remoteTrashFolder.expunge();
1309         remoteTrashFolder.close(false);
1310     }
1311 
1312     /**
1313      * Process a pending append message command. This command uploads a local message to the
1314      * server, first checking to be sure that the server message is not newer than
1315      * the local message.
1316      *
1317      * @param remoteStore the remote store we're working in
1318      * @param mailbox The mailbox we're appending to
1319      * @param message The message we're appending
1320      * @param manualSync True if this is a manual sync (changes upsync behavior)
1321      * @return true if successfully uploaded
1322      */
processPendingAppend(Context context, Store remoteStore, Mailbox mailbox, EmailContent.Message message, boolean manualSync)1323     private static boolean processPendingAppend(Context context, Store remoteStore, Mailbox mailbox,
1324             EmailContent.Message message, boolean manualSync)
1325             throws MessagingException {
1326         boolean updateInternalDate = false;
1327         boolean updateMessage = false;
1328         boolean deleteMessage = false;
1329 
1330         // 1. Find the remote folder that we're appending to and create and/or open it
1331         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1332         if (!remoteFolder.exists()) {
1333             if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
1334                 // This is a (hopefully) transient error and we return false to try again later
1335                 return false;
1336             }
1337         }
1338         remoteFolder.open(OpenMode.READ_WRITE);
1339         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1340             return false;
1341         }
1342 
1343         // 2. If possible, load a remote message with the matching UID
1344         Message remoteMessage = null;
1345         if (message.mServerId != null && message.mServerId.length() > 0) {
1346             remoteMessage = remoteFolder.getMessage(message.mServerId);
1347         }
1348 
1349         // 3. If a remote message could not be found, upload our local message
1350         if (remoteMessage == null) {
1351             // TODO:
1352             // if we have a serverId and remoteMessage is still null, then probably the message
1353             // has been deleted and we should delete locally.
1354             // 3a. Create a legacy message to upload
1355             Message localMessage = LegacyConversions.makeMessage(context, message);
1356             // 3b. Upload it
1357             //FetchProfile fp = new FetchProfile();
1358             //fp.add(FetchProfile.Item.BODY);
1359             // Note that this operation will assign the Uid to localMessage
1360             remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
1361 
1362             // 3b. And record the UID from the server
1363             message.mServerId = localMessage.getUid();
1364             updateInternalDate = true;
1365             updateMessage = true;
1366         } else {
1367             // 4. If the remote message exists we need to determine which copy to keep.
1368             // TODO:
1369             // I don't see a good reason we should be here. If the message already has a serverId,
1370             // then we should be handling it in processPendingUpdates(),
1371             // not processPendingUploads()
1372             FetchProfile fp = new FetchProfile();
1373             fp.add(FetchProfile.Item.ENVELOPE);
1374             remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1375             Date localDate = new Date(message.mServerTimeStamp);
1376             Date remoteDate = remoteMessage.getInternalDate();
1377             if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
1378                 // 4a. If the remote message is newer than ours we'll just
1379                 // delete ours and move on. A sync will get the server message
1380                 // if we need to be able to see it.
1381                 deleteMessage = true;
1382             } else {
1383                 // 4b. Otherwise we'll upload our message and then delete the remote message.
1384 
1385                 // Create a legacy message to upload
1386                 // TODO: This strategy has a problem: This will create a second message,
1387                 // so that at least temporarily, we will have two messages for what the
1388                 // user would think of as one.
1389                 Message localMessage = LegacyConversions.makeMessage(context, message);
1390 
1391                 // 4c. Upload it
1392                 fp.clear();
1393                 fp = new FetchProfile();
1394                 fp.add(FetchProfile.Item.BODY);
1395                 remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
1396 
1397                 // 4d. Record the UID and new internalDate from the server
1398                 message.mServerId = localMessage.getUid();
1399                 updateInternalDate = true;
1400                 updateMessage = true;
1401 
1402                 // 4e. And delete the old copy of the message from the server.
1403                 remoteMessage.setFlag(Flag.DELETED, true);
1404             }
1405         }
1406 
1407         // 5. If requested, Best-effort to capture new "internaldate" from the server
1408         if (updateInternalDate && message.mServerId != null) {
1409             try {
1410                 Message remoteMessage2 = remoteFolder.getMessage(message.mServerId);
1411                 if (remoteMessage2 != null) {
1412                     FetchProfile fp2 = new FetchProfile();
1413                     fp2.add(FetchProfile.Item.ENVELOPE);
1414                     remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null);
1415                     final Date remoteDate = remoteMessage2.getInternalDate();
1416                     if (remoteDate != null) {
1417                         message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime();
1418                         updateMessage = true;
1419                     }
1420                 }
1421             } catch (MessagingException me) {
1422                 // skip it - we can live without this
1423             }
1424         }
1425 
1426         // 6. Perform required edits to local copy of message
1427         if (deleteMessage || updateMessage) {
1428             Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
1429             ContentResolver resolver = context.getContentResolver();
1430             if (deleteMessage) {
1431                 resolver.delete(uri, null, null);
1432             } else if (updateMessage) {
1433                 ContentValues cv = new ContentValues();
1434                 cv.put(MessageColumns.SERVER_ID, message.mServerId);
1435                 cv.put(MessageColumns.SERVER_TIMESTAMP, message.mServerTimeStamp);
1436                 resolver.update(uri, cv, null, null);
1437             }
1438         }
1439 
1440         return true;
1441     }
1442 
1443     /**
1444      * A message and numeric uid that's easily sortable
1445      */
1446     private static class SortableMessage {
1447         private final Message mMessage;
1448         private final long mUid;
1449 
SortableMessage(Message message, long uid)1450         SortableMessage(Message message, long uid) {
1451             mMessage = message;
1452             mUid = uid;
1453         }
1454     }
1455 
searchMailboxImpl(final Context context, final long accountId, final SearchParams searchParams, final long destMailboxId)1456     private static int searchMailboxImpl(final Context context, final long accountId,
1457             final SearchParams searchParams, final long destMailboxId) throws MessagingException {
1458         final Account account = Account.restoreAccountWithId(context, accountId);
1459         final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId);
1460         final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
1461         if (account == null || mailbox == null || destMailbox == null) {
1462             LogUtils.d(Logging.LOG_TAG, "Attempted search for %s "
1463                     + "but account or mailbox information was missing", searchParams);
1464             return 0;
1465         }
1466 
1467         // Tell UI that we're loading messages
1468         final ContentValues statusValues = new ContentValues(2);
1469         statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY);
1470         destMailbox.update(context, statusValues);
1471 
1472         Store remoteStore = null;
1473         int numSearchResults = 0;
1474         try {
1475             remoteStore = Store.getInstance(account, context);
1476             final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1477             remoteFolder.open(OpenMode.READ_WRITE);
1478 
1479             SortableMessage[] sortableMessages = new SortableMessage[0];
1480             if (searchParams.mOffset == 0) {
1481                 // Get the "bare" messages (basically uid)
1482                 final Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
1483                 final int remoteCount = remoteMessages.length;
1484                 if (remoteCount > 0) {
1485                     sortableMessages = new SortableMessage[remoteCount];
1486                     int i = 0;
1487                     for (Message msg : remoteMessages) {
1488                         sortableMessages[i++] = new SortableMessage(msg,
1489                                 Long.parseLong(msg.getUid()));
1490                     }
1491                     // Sort the uid's, most recent first
1492                     // Note: Not all servers will be nice and return results in the order of
1493                     // request; those that do will see messages arrive from newest to oldest
1494                     Arrays.sort(sortableMessages, new Comparator<SortableMessage>() {
1495                         @Override
1496                         public int compare(SortableMessage lhs, SortableMessage rhs) {
1497                             return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
1498                         }
1499                     });
1500                     sSearchResults.put(accountId, sortableMessages);
1501                 }
1502             } else {
1503                 // It seems odd for this to happen, but if the previous query returned zero results,
1504                 // but the UI somehow still attempted to load more, then sSearchResults will have
1505                 // a null value for this account. We need to handle this below.
1506                 sortableMessages = sSearchResults.get(accountId);
1507             }
1508 
1509             numSearchResults = (sortableMessages != null ? sortableMessages.length : 0);
1510             final int numToLoad =
1511                     Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
1512             destMailbox.updateMessageCount(context, numSearchResults);
1513             if (numToLoad <= 0) {
1514                 return 0;
1515             }
1516 
1517             final ArrayList<Message> messageList = new ArrayList<>();
1518             for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
1519                 messageList.add(sortableMessages[i].mMessage);
1520             }
1521             // First fetch FLAGS and ENVELOPE. In a second pass, we'll fetch STRUCTURE and
1522             // the first body part.
1523             final FetchProfile fp = new FetchProfile();
1524             fp.add(FetchProfile.Item.FLAGS);
1525             fp.add(FetchProfile.Item.ENVELOPE);
1526 
1527             Message[] messageArray = messageList.toArray(new Message[messageList.size()]);
1528 
1529             // TODO: We are purposely processing messages with a MessageRetrievalListener here,
1530             // rather than just walking the messageArray after the operation completes. This is so
1531             // that we can immediately update the database so the user can see something useful
1532             // happening, even if the message body has not yet been fetched.
1533             // There are some issues with this approach:
1534             // 1. It means that we have a single thread doing both network and database operations,
1535             // and either can block the other. The database updates could slow down the network
1536             // reads, keeping our network connection open longer than is really necessary.
1537             // 2. We still load all of this data into messageArray, even though it's not used.
1538             // It would be nicer if we had one thread doing the network operation, and a separate
1539             // thread consuming that data and performing the appropriate database work, then
1540             // discarding the data as soon as it is no longer needed. This would reduce our memory
1541             // footprint and potentially allow our network operation to complete faster.
1542             remoteFolder.fetch(messageArray, fp, new MessageRetrievalListener() {
1543                 @Override
1544                 public void messageRetrieved(Message message) {
1545                     try {
1546                         EmailContent.Message localMessage = new EmailContent.Message();
1547 
1548                         // Copy the fields that are available into the message
1549                         LegacyConversions.updateMessageFields(localMessage,
1550                                 message, account.mId, mailbox.mId);
1551                         // Save off the mailbox that this message *really* belongs in.
1552                         // We need this information if we need to do more lookups
1553                         // (like loading attachments) for this message. See b/11294681
1554                         localMessage.mMainMailboxKey = localMessage.mMailboxKey;
1555                         localMessage.mMailboxKey = destMailboxId;
1556                         // We load 50k or so; maybe it's complete, maybe not...
1557                         int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
1558                         // We store the serverId of the source mailbox into protocolSearchInfo
1559                         // This will be used by loadMessageForView, etc. to use the proper remote
1560                         // folder
1561                         localMessage.mProtocolSearchInfo = mailbox.mServerId;
1562                         // Commit the message to the local store
1563                         Utilities.saveOrUpdate(localMessage, context);
1564                     } catch (MessagingException me) {
1565                         LogUtils.e(Logging.LOG_TAG, me,
1566                                 "Error while copying downloaded message.");
1567                     } catch (Exception e) {
1568                         LogUtils.e(Logging.LOG_TAG, e,
1569                                 "Error while storing downloaded message.");
1570                     }
1571                 }
1572 
1573                 @Override
1574                 public void loadAttachmentProgress(int progress) {
1575                 }
1576             });
1577 
1578             // Now load the structure for all of the messages:
1579             fp.clear();
1580             fp.add(FetchProfile.Item.STRUCTURE);
1581             remoteFolder.fetch(messageArray, fp, null);
1582 
1583             // Finally, load the first body part (i.e. message text).
1584             // This means attachment contents are not yet loaded, but that's okay,
1585             // we'll load them as needed, same as in synced messages.
1586             Message[] oneMessageArray = new Message[1];
1587             for (Message message : messageArray) {
1588                 // Build a list of parts we are interested in. Text parts will be downloaded
1589                 // right now, attachments will be left for later.
1590                 ArrayList<Part> viewables = new ArrayList<>();
1591                 ArrayList<Part> attachments = new ArrayList<>();
1592                 MimeUtility.collectParts(message, viewables, attachments);
1593                 // Download the viewables immediately
1594                 oneMessageArray[0] = message;
1595                 for (Part part : viewables) {
1596                     fp.clear();
1597                     fp.add(part);
1598                     remoteFolder.fetch(oneMessageArray, fp, null);
1599                 }
1600                 // Store the updated message locally and mark it fully loaded
1601                 Utilities.copyOneMessageToProvider(context, message, account, destMailbox,
1602                         EmailContent.Message.FLAG_LOADED_COMPLETE);
1603             }
1604 
1605         } finally {
1606             if (remoteStore != null) {
1607                 remoteStore.closeConnections();
1608             }
1609             // Tell UI that we're done loading messages
1610             statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1611             statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
1612             destMailbox.update(context, statusValues);
1613         }
1614 
1615         return numSearchResults;
1616     }
1617 }
1618