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