• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.net.TrafficStats;
25 import android.net.Uri;
26 import android.os.Process;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import com.android.email.mail.Sender;
31 import com.android.email.mail.Store;
32 import com.android.emailcommon.Logging;
33 import com.android.emailcommon.TrafficFlags;
34 import com.android.emailcommon.internet.MimeBodyPart;
35 import com.android.emailcommon.internet.MimeHeader;
36 import com.android.emailcommon.internet.MimeMultipart;
37 import com.android.emailcommon.internet.MimeUtility;
38 import com.android.emailcommon.mail.AuthenticationFailedException;
39 import com.android.emailcommon.mail.FetchProfile;
40 import com.android.emailcommon.mail.Flag;
41 import com.android.emailcommon.mail.Folder;
42 import com.android.emailcommon.mail.Folder.FolderType;
43 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
44 import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
45 import com.android.emailcommon.mail.Folder.OpenMode;
46 import com.android.emailcommon.mail.Message;
47 import com.android.emailcommon.mail.MessagingException;
48 import com.android.emailcommon.mail.Part;
49 import com.android.emailcommon.provider.Account;
50 import com.android.emailcommon.provider.EmailContent;
51 import com.android.emailcommon.provider.EmailContent.Attachment;
52 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
53 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
54 import com.android.emailcommon.provider.EmailContent.MessageColumns;
55 import com.android.emailcommon.provider.EmailContent.SyncColumns;
56 import com.android.emailcommon.provider.Mailbox;
57 import com.android.emailcommon.service.SearchParams;
58 import com.android.emailcommon.utility.AttachmentUtilities;
59 import com.android.emailcommon.utility.ConversionUtilities;
60 import com.android.emailcommon.utility.Utility;
61 
62 import java.io.IOException;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Comparator;
66 import java.util.Date;
67 import java.util.HashMap;
68 import java.util.HashSet;
69 import java.util.concurrent.BlockingQueue;
70 import java.util.concurrent.LinkedBlockingQueue;
71 
72 /**
73  * Starts a long running (application) Thread that will run through commands
74  * that require remote mailbox access. This class is used to serialize and
75  * prioritize these commands. Each method that will submit a command requires a
76  * MessagingListener instance to be provided. It is expected that that listener
77  * has also been added as a registered listener using addListener(). When a
78  * command is to be executed, if the listener that was provided with the command
79  * is no longer registered the command is skipped. The design idea for the above
80  * is that when an Activity starts it registers as a listener. When it is paused
81  * it removes itself. Thus, any commands that that activity submitted are
82  * removed from the queue once the activity is no longer active.
83  */
84 public class MessagingController implements Runnable {
85 
86     /**
87      * The maximum message size that we'll consider to be "small". A small message is downloaded
88      * in full immediately instead of in pieces. Anything over this size will be downloaded in
89      * pieces with attachments being left off completely and downloaded on demand.
90      *
91      *
92      * 25k for a "small" message was picked by educated trial and error.
93      * http://answers.google.com/answers/threadview?id=312463 claims that the
94      * average size of an email is 59k, which I feel is too large for our
95      * blind download. The following tests were performed on a download of
96      * 25 random messages.
97      * <pre>
98      * 5k - 61 seconds,
99      * 25k - 51 seconds,
100      * 55k - 53 seconds,
101      * </pre>
102      * So 25k gives good performance and a reasonable data footprint. Sounds good to me.
103      */
104     private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
105 
106     private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
107     private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
108     private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
109 
110     /**
111      * We write this into the serverId field of messages that will never be upsynced.
112      */
113     private static final String LOCAL_SERVERID_PREFIX = "Local-";
114 
115     /**
116      * Cache search results by account; this allows for "load more" support without having to
117      * redo the search (which can be quite slow).  SortableMessage is a smallish class, so memory
118      * shouldn't be an issue
119      */
120     private static final HashMap<Long, SortableMessage[]> sSearchResults =
121         new HashMap<Long, SortableMessage[]>();
122 
123     private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues();
124     static {
125         PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI);
126     }
127 
128     private static MessagingController sInstance = null;
129     private final BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
130     private final Thread mThread;
131 
132     /**
133      * All access to mListeners *must* be synchronized
134      */
135     private final GroupMessagingListener mListeners = new GroupMessagingListener();
136     private boolean mBusy;
137     private final Context mContext;
138     private final Controller mController;
139 
140     /**
141      * Simple cache for last search result mailbox by account and serverId, since the most common
142      * case will be repeated use of the same mailbox
143      */
144     private long mLastSearchAccountKey = Account.NO_ACCOUNT;
145     private String mLastSearchServerId = null;
146     private Mailbox mLastSearchRemoteMailbox = null;
147 
MessagingController(Context _context, Controller _controller)148     protected MessagingController(Context _context, Controller _controller) {
149         mContext = _context.getApplicationContext();
150         mController = _controller;
151         mThread = new Thread(this);
152         mThread.start();
153     }
154 
155     /**
156      * Gets or creates the singleton instance of MessagingController. Application is used to
157      * provide a Context to classes that need it.
158      */
getInstance(Context _context, Controller _controller)159     public synchronized static MessagingController getInstance(Context _context,
160             Controller _controller) {
161         if (sInstance == null) {
162             sInstance = new MessagingController(_context, _controller);
163         }
164         return sInstance;
165     }
166 
167     /**
168      * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
169      */
injectMockController(MessagingController mockController)170     public static void injectMockController(MessagingController mockController) {
171         sInstance = mockController;
172     }
173 
174     // TODO: seems that this reading of mBusy isn't thread-safe
isBusy()175     public boolean isBusy() {
176         return mBusy;
177     }
178 
run()179     public void run() {
180         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
181         // TODO: add an end test to this infinite loop
182         while (true) {
183             Command command;
184             try {
185                 command = mCommands.take();
186             } catch (InterruptedException e) {
187                 continue; //re-test the condition on the eclosing while
188             }
189             if (command.listener == null || isActiveListener(command.listener)) {
190                 mBusy = true;
191                 command.runnable.run();
192                 mListeners.controllerCommandCompleted(mCommands.size() > 0);
193             }
194             mBusy = false;
195         }
196     }
197 
put(String description, MessagingListener listener, Runnable runnable)198     private void put(String description, MessagingListener listener, Runnable runnable) {
199         try {
200             Command command = new Command();
201             command.listener = listener;
202             command.runnable = runnable;
203             command.description = description;
204             mCommands.add(command);
205         }
206         catch (IllegalStateException ie) {
207             throw new Error(ie);
208         }
209     }
210 
addListener(MessagingListener listener)211     public void addListener(MessagingListener listener) {
212         mListeners.addListener(listener);
213     }
214 
removeListener(MessagingListener listener)215     public void removeListener(MessagingListener listener) {
216         mListeners.removeListener(listener);
217     }
218 
isActiveListener(MessagingListener listener)219     private boolean isActiveListener(MessagingListener listener) {
220         return mListeners.isActiveListener(listener);
221     }
222 
223     private static final int MAILBOX_COLUMN_ID = 0;
224     private static final int MAILBOX_COLUMN_SERVER_ID = 1;
225     private static final int MAILBOX_COLUMN_TYPE = 2;
226 
227     /** Small projection for just the columns required for a sync. */
228     private static final String[] MAILBOX_PROJECTION = new String[] {
229         MailboxColumns.ID,
230         MailboxColumns.SERVER_ID,
231         MailboxColumns.TYPE,
232     };
233 
234     /**
235      * Synchronize the folder list with the remote server. Synchronization occurs in the
236      * background and results are passed through the {@link MessagingListener}. If the
237      * given listener is not {@code null}, it must have been previously added to the set
238      * of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no
239      * actions will be performed.
240      *
241      * TODO this needs to cache the remote folder list
242      * TODO break out an inner listFoldersSynchronized which could simplify checkMail
243      *
244      * @param accountId ID of the account for which to list the folders
245      * @param listener A listener to notify
246      */
listFolders(final long accountId, MessagingListener listener)247     void listFolders(final long accountId, MessagingListener listener) {
248         final Account account = Account.restoreAccountWithId(mContext, accountId);
249         if (account == null) {
250             Log.i(Logging.LOG_TAG, "Could not load account id " + accountId
251                     + ". Has it been removed?");
252             return;
253         }
254         mListeners.listFoldersStarted(accountId);
255         put("listFolders", listener, new Runnable() {
256             // TODO For now, mailbox addition occurs in the server-dependent store implementation,
257             // but, mailbox removal occurs here. Instead, each store should be responsible for
258             // content synchronization (addition AND removal) since each store will likely need
259             // to implement it's own, unique synchronization methodology.
260             public void run() {
261                 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
262                 Cursor localFolderCursor = null;
263                 try {
264                     // Step 1: Get remote mailboxes
265                     Store store = Store.getInstance(account, mContext);
266                     Folder[] remoteFolders = store.updateFolders();
267                     HashSet<String> remoteFolderNames = new HashSet<String>();
268                     for (int i = 0, count = remoteFolders.length; i < count; i++) {
269                         remoteFolderNames.add(remoteFolders[i].getName());
270                     }
271 
272                     // Step 2: Get local mailboxes
273                     localFolderCursor = mContext.getContentResolver().query(
274                             Mailbox.CONTENT_URI,
275                             MAILBOX_PROJECTION,
276                             EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
277                             new String[] { String.valueOf(account.mId) },
278                             null);
279 
280                     // Step 3: Remove any local mailbox not on the remote list
281                     while (localFolderCursor.moveToNext()) {
282                         String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
283                         // Short circuit if we have a remote mailbox with the same name
284                         if (remoteFolderNames.contains(mailboxPath)) {
285                             continue;
286                         }
287 
288                         int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
289                         long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
290                         switch (mailboxType) {
291                             case Mailbox.TYPE_INBOX:
292                             case Mailbox.TYPE_DRAFTS:
293                             case Mailbox.TYPE_OUTBOX:
294                             case Mailbox.TYPE_SENT:
295                             case Mailbox.TYPE_TRASH:
296                             case Mailbox.TYPE_SEARCH:
297                                 // Never, ever delete special mailboxes
298                                 break;
299                             default:
300                                 // Drop all attachment files related to this mailbox
301                                 AttachmentUtilities.deleteAllMailboxAttachmentFiles(
302                                         mContext, accountId, mailboxId);
303                                 // Delete the mailbox; database triggers take care of related
304                                 // Message, Body and Attachment records
305                                 Uri uri = ContentUris.withAppendedId(
306                                         Mailbox.CONTENT_URI, mailboxId);
307                                 mContext.getContentResolver().delete(uri, null, null);
308                                 break;
309                         }
310                     }
311                     mListeners.listFoldersFinished(accountId);
312                 } catch (Exception e) {
313                     mListeners.listFoldersFailed(accountId, e.toString());
314                 } finally {
315                     if (localFolderCursor != null) {
316                         localFolderCursor.close();
317                     }
318                 }
319             }
320         });
321     }
322 
323     /**
324      * Start background synchronization of the specified folder.
325      * @param account
326      * @param folder
327      * @param listener
328      */
synchronizeMailbox(final Account account, final Mailbox folder, MessagingListener listener)329     public void synchronizeMailbox(final Account account,
330             final Mailbox folder, MessagingListener listener) {
331         /*
332          * We don't ever sync the Outbox.
333          */
334         if (folder.mType == Mailbox.TYPE_OUTBOX) {
335             return;
336         }
337         mListeners.synchronizeMailboxStarted(account.mId, folder.mId);
338         put("synchronizeMailbox", listener, new Runnable() {
339             public void run() {
340                 synchronizeMailboxSynchronous(account, folder);
341             }
342         });
343     }
344 
345     /**
346      * Start foreground synchronization of the specified folder. This is called by
347      * synchronizeMailbox or checkMail.
348      * TODO this should use ID's instead of fully-restored objects
349      * @param account
350      * @param folder
351      */
synchronizeMailboxSynchronous(final Account account, final Mailbox folder)352     private void synchronizeMailboxSynchronous(final Account account,
353             final Mailbox folder) {
354         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
355         mListeners.synchronizeMailboxStarted(account.mId, folder.mId);
356         if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
357             // We don't hold messages, so, nothing to synchronize
358             mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null);
359             return;
360         }
361         NotificationController nc = NotificationController.getInstance(mContext);
362         try {
363             processPendingActionsSynchronous(account);
364 
365             // Select generic sync or store-specific sync
366             SyncResults results = synchronizeMailboxGeneric(account, folder);
367             mListeners.synchronizeMailboxFinished(account.mId, folder.mId,
368                                                   results.mTotalMessages,
369                                                   results.mAddedMessages.size(),
370                                                   results.mAddedMessages);
371             // Clear authentication notification for this account
372             nc.cancelLoginFailedNotification(account.mId);
373         } catch (MessagingException e) {
374             if (Logging.LOGD) {
375                 Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
376             }
377             if (e instanceof AuthenticationFailedException) {
378                 // Generate authentication notification
379                 nc.showLoginFailedNotification(account.mId);
380             }
381             mListeners.synchronizeMailboxFailed(account.mId, folder.mId, e);
382         }
383     }
384 
385     /**
386      * Lightweight record for the first pass of message sync, where I'm just seeing if
387      * the local message requires sync.  Later (for messages that need syncing) we'll do a full
388      * readout from the DB.
389      */
390     private static class LocalMessageInfo {
391         private static final int COLUMN_ID = 0;
392         private static final int COLUMN_FLAG_READ = 1;
393         private static final int COLUMN_FLAG_FAVORITE = 2;
394         private static final int COLUMN_FLAG_LOADED = 3;
395         private static final int COLUMN_SERVER_ID = 4;
396         private static final int COLUMN_FLAGS =  7;
397         private static final String[] PROJECTION = new String[] {
398             EmailContent.RECORD_ID,
399             MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
400             SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
401             MessageColumns.FLAGS
402         };
403 
404         final long mId;
405         final boolean mFlagRead;
406         final boolean mFlagFavorite;
407         final int mFlagLoaded;
408         final String mServerId;
409         final int mFlags;
410 
LocalMessageInfo(Cursor c)411         public LocalMessageInfo(Cursor c) {
412             mId = c.getLong(COLUMN_ID);
413             mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
414             mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
415             mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
416             mServerId = c.getString(COLUMN_SERVER_ID);
417             mFlags = c.getInt(COLUMN_FLAGS);
418             // Note: mailbox key and account key not needed - they are projected for the SELECT
419         }
420     }
421 
saveOrUpdate(EmailContent content, Context context)422     private void saveOrUpdate(EmailContent content, Context context) {
423         if (content.isSaved()) {
424             content.update(context, content.toContentValues());
425         } else {
426             content.save(context);
427         }
428     }
429 
430     /**
431      * Load the structure and body of messages not yet synced
432      * @param account the account we're syncing
433      * @param remoteFolder the (open) Folder we're working on
434      * @param unsyncedMessages an array of Message's we've got headers for
435      * @param toMailbox the destination mailbox we're syncing
436      * @throws MessagingException
437      */
loadUnsyncedMessages(final Account account, Folder remoteFolder, ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)438     void loadUnsyncedMessages(final Account account, Folder remoteFolder,
439             ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)
440             throws MessagingException {
441 
442         // 1. Divide the unsynced messages into small & large (by size)
443 
444         // TODO doing this work here (synchronously) is problematic because it prevents the UI
445         // from affecting the order (e.g. download a message because the user requested it.)  Much
446         // of this logic should move out to a different sync loop that attempts to update small
447         // groups of messages at a time, as a background task.  However, we can't just return
448         // (yet) because POP messages don't have an envelope yet....
449 
450         ArrayList<Message> largeMessages = new ArrayList<Message>();
451         ArrayList<Message> smallMessages = new ArrayList<Message>();
452         for (Message message : unsyncedMessages) {
453             if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
454                 largeMessages.add(message);
455             } else {
456                 smallMessages.add(message);
457             }
458         }
459 
460         // 2. Download small messages
461 
462         // TODO Problems with this implementation.  1. For IMAP, where we get a real envelope,
463         // this is going to be inefficient and duplicate work we've already done.  2.  It's going
464         // back to the DB for a local message that we already had (and discarded).
465 
466         // For small messages, we specify "body", which returns everything (incl. attachments)
467         FetchProfile fp = new FetchProfile();
468         fp.add(FetchProfile.Item.BODY);
469         remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
470                 new MessageRetrievalListener() {
471                     public void messageRetrieved(Message message) {
472                         // Store the updated message locally and mark it fully loaded
473                         copyOneMessageToProvider(message, account, toMailbox,
474                                 EmailContent.Message.FLAG_LOADED_COMPLETE);
475                     }
476 
477                     @Override
478                     public void loadAttachmentProgress(int progress) {
479                     }
480         });
481 
482         // 3. Download large messages.  We ask the server to give us the message structure,
483         // but not all of the attachments.
484         fp.clear();
485         fp.add(FetchProfile.Item.STRUCTURE);
486         remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
487         for (Message message : largeMessages) {
488             if (message.getBody() == null) {
489                 // POP doesn't support STRUCTURE mode, so we'll just do a partial download
490                 // (hopefully enough to see some/all of the body) and mark the message for
491                 // further download.
492                 fp.clear();
493                 fp.add(FetchProfile.Item.BODY_SANE);
494                 //  TODO a good optimization here would be to make sure that all Stores set
495                 //  the proper size after this fetch and compare the before and after size. If
496                 //  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
497                 remoteFolder.fetch(new Message[] { message }, fp, null);
498 
499                 // Store the partially-loaded message and mark it partially loaded
500                 copyOneMessageToProvider(message, account, toMailbox,
501                         EmailContent.Message.FLAG_LOADED_PARTIAL);
502             } else {
503                 // We have a structure to deal with, from which
504                 // we can pull down the parts we want to actually store.
505                 // Build a list of parts we are interested in. Text parts will be downloaded
506                 // right now, attachments will be left for later.
507                 ArrayList<Part> viewables = new ArrayList<Part>();
508                 ArrayList<Part> attachments = new ArrayList<Part>();
509                 MimeUtility.collectParts(message, viewables, attachments);
510                 // Download the viewables immediately
511                 for (Part part : viewables) {
512                     fp.clear();
513                     fp.add(part);
514                     // TODO what happens if the network connection dies? We've got partial
515                     // messages with incorrect status stored.
516                     remoteFolder.fetch(new Message[] { message }, fp, null);
517                 }
518                 // Store the updated message locally and mark it fully loaded
519                 copyOneMessageToProvider(message, account, toMailbox,
520                         EmailContent.Message.FLAG_LOADED_COMPLETE);
521             }
522         }
523 
524     }
525 
downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)526     public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox,
527             Folder remoteFolder, ArrayList<Message> unsyncedMessages,
528             HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
529             throws MessagingException {
530         FetchProfile fp = new FetchProfile();
531         fp.add(FetchProfile.Item.FLAGS);
532         fp.add(FetchProfile.Item.ENVELOPE);
533 
534         final HashMap<String, LocalMessageInfo> localMapCopy;
535         if (localMessageMap != null)
536             localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
537         else {
538             localMapCopy = new HashMap<String, LocalMessageInfo>();
539         }
540 
541         remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
542                 new MessageRetrievalListener() {
543                     @Override
544                     public void messageRetrieved(Message message) {
545                         try {
546                             // Determine if the new message was already known (e.g. partial)
547                             // And create or reload the full message info
548                             LocalMessageInfo localMessageInfo =
549                                 localMapCopy.get(message.getUid());
550                             EmailContent.Message localMessage = null;
551                             if (localMessageInfo == null) {
552                                 localMessage = new EmailContent.Message();
553                             } else {
554                                 localMessage = EmailContent.Message.restoreMessageWithId(
555                                         mContext, localMessageInfo.mId);
556                             }
557 
558                             if (localMessage != null) {
559                                 try {
560                                     // Copy the fields that are available into the message
561                                     LegacyConversions.updateMessageFields(localMessage,
562                                             message, account.mId, mailbox.mId);
563                                     // Commit the message to the local store
564                                     saveOrUpdate(localMessage, mContext);
565                                     // Track the "new" ness of the downloaded message
566                                     if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
567                                         unseenMessages.add(localMessage.mId);
568                                     }
569                                 } catch (MessagingException me) {
570                                     Log.e(Logging.LOG_TAG,
571                                             "Error while copying downloaded message." + me);
572                                 }
573 
574                             }
575                         }
576                         catch (Exception e) {
577                             Log.e(Logging.LOG_TAG,
578                                     "Error while storing downloaded message." + e.toString());
579                         }
580                     }
581 
582                     @Override
583                     public void loadAttachmentProgress(int progress) {
584                     }
585                 });
586 
587     }
588 
589     /**
590      * A message and numeric uid that's easily sortable
591      */
592     private static class SortableMessage {
593         private final Message mMessage;
594         private final long mUid;
595 
SortableMessage(Message message, long uid)596         SortableMessage(Message message, long uid) {
597             mMessage = message;
598             mUid = uid;
599         }
600     }
601 
searchMailbox(long accountId, SearchParams searchParams, long destMailboxId)602     public int searchMailbox(long accountId, SearchParams searchParams, long destMailboxId)
603             throws MessagingException {
604         try {
605             return searchMailboxImpl(accountId, searchParams, destMailboxId);
606         } finally {
607             // Tell UI that we're done loading any search results (no harm calling this even if we
608             // encountered an error or never sent a "started" message)
609             mListeners.synchronizeMailboxFinished(accountId, destMailboxId, 0, 0, null);
610         }
611     }
612 
searchMailboxImpl(long accountId, SearchParams searchParams, final long destMailboxId)613     private int searchMailboxImpl(long accountId, SearchParams searchParams,
614             final long destMailboxId) throws MessagingException {
615         final Account account = Account.restoreAccountWithId(mContext, accountId);
616         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId);
617         final Mailbox destMailbox = Mailbox.restoreMailboxWithId(mContext, destMailboxId);
618         if (account == null || mailbox == null || destMailbox == null) {
619             Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams
620                     + " but account or mailbox information was missing");
621             return 0;
622         }
623 
624         // Tell UI that we're loading messages
625         mListeners.synchronizeMailboxStarted(accountId, destMailbox.mId);
626 
627         Store remoteStore = Store.getInstance(account, mContext);
628         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
629         remoteFolder.open(OpenMode.READ_WRITE);
630 
631         SortableMessage[] sortableMessages = new SortableMessage[0];
632         if (searchParams.mOffset == 0) {
633             // Get the "bare" messages (basically uid)
634             Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
635             int remoteCount = remoteMessages.length;
636             if (remoteCount > 0) {
637                 sortableMessages = new SortableMessage[remoteCount];
638                 int i = 0;
639                 for (Message msg : remoteMessages) {
640                     sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid()));
641                 }
642                 // Sort the uid's, most recent first
643                 // Note: Not all servers will be nice and return results in the order of request;
644                 // those that do will see messages arrive from newest to oldest
645                 Arrays.sort(sortableMessages, new Comparator<SortableMessage>() {
646                     @Override
647                     public int compare(SortableMessage lhs, SortableMessage rhs) {
648                         return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
649                     }
650                 });
651                 sSearchResults.put(accountId, sortableMessages);
652             }
653         } else {
654             sortableMessages = sSearchResults.get(accountId);
655         }
656 
657         final int numSearchResults = sortableMessages.length;
658         final int numToLoad =
659             Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
660         if (numToLoad <= 0) {
661             return 0;
662         }
663 
664         final ArrayList<Message> messageList = new ArrayList<Message>();
665         for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
666             messageList.add(sortableMessages[i].mMessage);
667         }
668         // Get everything in one pass, rather than two (as in sync); this starts getting us
669         // usable results quickly.
670         FetchProfile fp = new FetchProfile();
671         fp.add(FetchProfile.Item.FLAGS);
672         fp.add(FetchProfile.Item.ENVELOPE);
673         fp.add(FetchProfile.Item.STRUCTURE);
674         fp.add(FetchProfile.Item.BODY_SANE);
675         remoteFolder.fetch(messageList.toArray(new Message[0]), fp,
676                 new MessageRetrievalListener() {
677             public void messageRetrieved(Message message) {
678                 try {
679                     // Determine if the new message was already known (e.g. partial)
680                     // And create or reload the full message info
681                     EmailContent.Message localMessage = new EmailContent.Message();
682                     try {
683                         // Copy the fields that are available into the message
684                         LegacyConversions.updateMessageFields(localMessage,
685                                 message, account.mId, mailbox.mId);
686                         // Commit the message to the local store
687                         saveOrUpdate(localMessage, mContext);
688                         localMessage.mMailboxKey = destMailboxId;
689                         // We load 50k or so; maybe it's complete, maybe not...
690                         int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
691                         // We store the serverId of the source mailbox into protocolSearchInfo
692                         // This will be used by loadMessageForView, etc. to use the proper remote
693                         // folder
694                         localMessage.mProtocolSearchInfo = mailbox.mServerId;
695                         if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) {
696                             flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
697                         }
698                         copyOneMessageToProvider(message, localMessage, flag, mContext);
699                     } catch (MessagingException me) {
700                         Log.e(Logging.LOG_TAG,
701                                 "Error while copying downloaded message." + me);
702                     }
703                 } catch (Exception e) {
704                     Log.e(Logging.LOG_TAG,
705                             "Error while storing downloaded message." + e.toString());
706                 }
707             }
708 
709             @Override
710             public void loadAttachmentProgress(int progress) {
711             }
712         });
713         return numSearchResults;
714     }
715 
716 
717     /**
718      * Generic synchronizer - used for POP3 and IMAP.
719      *
720      * TODO Break this method up into smaller chunks.
721      *
722      * @param account the account to sync
723      * @param mailbox the mailbox to sync
724      * @return results of the sync pass
725      * @throws MessagingException
726      */
synchronizeMailboxGeneric(final Account account, final Mailbox mailbox)727     private SyncResults synchronizeMailboxGeneric(final Account account, final Mailbox mailbox)
728             throws MessagingException {
729 
730         /*
731          * A list of IDs for messages that were downloaded and did not have the seen flag set.
732          * This serves as the "true" new message count reported to the user via notification.
733          */
734         final ArrayList<Long> unseenMessages = new ArrayList<Long>();
735 
736         Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***");
737         ContentResolver resolver = mContext.getContentResolver();
738 
739         // 0.  We do not ever sync DRAFTS or OUTBOX (down or up)
740         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
741             int totalMessages = EmailContent.count(mContext, mailbox.getUri(), null, null);
742             return new SyncResults(totalMessages, unseenMessages);
743         }
744 
745         // 1.  Get the message list from the local store and create an index of the uids
746 
747         Cursor localUidCursor = null;
748         HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
749 
750         try {
751             localUidCursor = resolver.query(
752                     EmailContent.Message.CONTENT_URI,
753                     LocalMessageInfo.PROJECTION,
754                     EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
755                     " AND " + MessageColumns.MAILBOX_KEY + "=?",
756                     new String[] {
757                             String.valueOf(account.mId),
758                             String.valueOf(mailbox.mId)
759                     },
760                     null);
761             while (localUidCursor.moveToNext()) {
762                 LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
763                 localMessageMap.put(info.mServerId, info);
764             }
765         } finally {
766             if (localUidCursor != null) {
767                 localUidCursor.close();
768             }
769         }
770 
771         // 2.  Open the remote folder and create the remote folder if necessary
772 
773         Store remoteStore = Store.getInstance(account, mContext);
774         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
775 
776         /*
777          * If the folder is a "special" folder we need to see if it exists
778          * on the remote server. It if does not exist we'll try to create it. If we
779          * can't create we'll abort. This will happen on every single Pop3 folder as
780          * designed and on Imap folders during error conditions. This allows us
781          * to treat Pop3 and Imap the same in this code.
782          */
783         if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
784                 || mailbox.mType == Mailbox.TYPE_DRAFTS) {
785             if (!remoteFolder.exists()) {
786                 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
787                     return new SyncResults(0, unseenMessages);
788                 }
789             }
790         }
791 
792         // 3, Open the remote folder. This pre-loads certain metadata like message count.
793         remoteFolder.open(OpenMode.READ_WRITE);
794 
795         // 4. Trash any remote messages that are marked as trashed locally.
796         // TODO - this comment was here, but no code was here.
797 
798         // 5. Get the remote message count.
799         int remoteMessageCount = remoteFolder.getMessageCount();
800 
801         // 6. Determine the limit # of messages to download
802         int visibleLimit = mailbox.mVisibleLimit;
803         if (visibleLimit <= 0) {
804             visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
805         }
806 
807         // 7.  Create a list of messages to download
808         Message[] remoteMessages = new Message[0];
809         final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
810         HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
811 
812         int newMessageCount = 0;
813         if (remoteMessageCount > 0) {
814             /*
815              * Message numbers start at 1.
816              */
817             int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
818             int remoteEnd = remoteMessageCount;
819             remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
820             // TODO Why are we running through the list twice? Combine w/ for loop below
821             for (Message message : remoteMessages) {
822                 remoteUidMap.put(message.getUid(), message);
823             }
824 
825             /*
826              * Get a list of the messages that are in the remote list but not on the
827              * local store, or messages that are in the local store but failed to download
828              * on the last sync. These are the new messages that we will download.
829              * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
830              * because they are locally deleted and we don't need or want the old message from
831              * the server.
832              */
833             for (Message message : remoteMessages) {
834                 LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
835                 if (localMessage == null) {
836                     newMessageCount++;
837                 }
838                 // localMessage == null -> message has never been created (not even headers)
839                 // mFlagLoaded = UNLOADED -> message created, but none of body loaded
840                 // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
841                 // mFlagLoaded = COMPLETE -> message body has been completely loaded
842                 // mFlagLoaded = DELETED -> message has been deleted
843                 // Only the first two of these are "unsynced", so let's retrieve them
844                 if (localMessage == null ||
845                         (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
846                     unsyncedMessages.add(message);
847                 }
848             }
849         }
850 
851         // 8.  Download basic info about the new/unloaded messages (if any)
852         /*
853          * Fetch the flags and envelope only of the new messages. This is intended to get us
854          * critical data as fast as possible, and then we'll fill in the details.
855          */
856         if (unsyncedMessages.size() > 0) {
857             downloadFlagAndEnvelope(account, mailbox, remoteFolder, unsyncedMessages,
858                     localMessageMap, unseenMessages);
859         }
860 
861         // 9. Refresh the flags for any messages in the local store that we didn't just download.
862         FetchProfile fp = new FetchProfile();
863         fp.add(FetchProfile.Item.FLAGS);
864         remoteFolder.fetch(remoteMessages, fp, null);
865         boolean remoteSupportsSeen = false;
866         boolean remoteSupportsFlagged = false;
867         boolean remoteSupportsAnswered = false;
868         for (Flag flag : remoteFolder.getPermanentFlags()) {
869             if (flag == Flag.SEEN) {
870                 remoteSupportsSeen = true;
871             }
872             if (flag == Flag.FLAGGED) {
873                 remoteSupportsFlagged = true;
874             }
875             if (flag == Flag.ANSWERED) {
876                 remoteSupportsAnswered = true;
877             }
878         }
879         // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
880         if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
881             for (Message remoteMessage : remoteMessages) {
882                 LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
883                 if (localMessageInfo == null) {
884                     continue;
885                 }
886                 boolean localSeen = localMessageInfo.mFlagRead;
887                 boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
888                 boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
889                 boolean localFlagged = localMessageInfo.mFlagFavorite;
890                 boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
891                 boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
892                 int localFlags = localMessageInfo.mFlags;
893                 boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
894                 boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
895                 boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
896                 if (newSeen || newFlagged || newAnswered) {
897                     Uri uri = ContentUris.withAppendedId(
898                             EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
899                     ContentValues updateValues = new ContentValues();
900                     updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
901                     updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
902                     if (remoteAnswered) {
903                         localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
904                     } else {
905                         localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
906                     }
907                     updateValues.put(MessageColumns.FLAGS, localFlags);
908                     resolver.update(uri, updateValues, null, null);
909                 }
910             }
911         }
912 
913         // 10. Remove any messages that are in the local store but no longer on the remote store.
914         HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
915         localUidsToDelete.removeAll(remoteUidMap.keySet());
916         for (String uidToDelete : localUidsToDelete) {
917             LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
918 
919             // Delete associated data (attachment files)
920             // Attachment & Body records are auto-deleted when we delete the Message record
921             AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
922                     infoToDelete.mId);
923 
924             // Delete the message itself
925             Uri uriToDelete = ContentUris.withAppendedId(
926                     EmailContent.Message.CONTENT_URI, infoToDelete.mId);
927             resolver.delete(uriToDelete, null, null);
928 
929             // Delete extra rows (e.g. synced or deleted)
930             Uri syncRowToDelete = ContentUris.withAppendedId(
931                     EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
932             resolver.delete(syncRowToDelete, null, null);
933             Uri deletERowToDelete = ContentUris.withAppendedId(
934                     EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
935             resolver.delete(deletERowToDelete, null, null);
936         }
937 
938         loadUnsyncedMessages(account, remoteFolder, unsyncedMessages, mailbox);
939 
940         // 14. Clean up and report results
941         remoteFolder.close(false);
942 
943         return new SyncResults(remoteMessageCount, unseenMessages);
944     }
945 
946     /**
947      * Copy one downloaded message (which may have partially-loaded sections)
948      * into a newly created EmailProvider Message, given the account and mailbox
949      *
950      * @param message the remote message we've just downloaded
951      * @param account the account it will be stored into
952      * @param folder the mailbox it will be stored into
953      * @param loadStatus when complete, the message will be marked with this status (e.g.
954      *        EmailContent.Message.LOADED)
955      */
copyOneMessageToProvider(Message message, Account account, Mailbox folder, int loadStatus)956     public void copyOneMessageToProvider(Message message, Account account,
957             Mailbox folder, int loadStatus) {
958         EmailContent.Message localMessage = null;
959         Cursor c = null;
960         try {
961             c = mContext.getContentResolver().query(
962                     EmailContent.Message.CONTENT_URI,
963                     EmailContent.Message.CONTENT_PROJECTION,
964                     EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
965                     " AND " + MessageColumns.MAILBOX_KEY + "=?" +
966                     " AND " + SyncColumns.SERVER_ID + "=?",
967                     new String[] {
968                             String.valueOf(account.mId),
969                             String.valueOf(folder.mId),
970                             String.valueOf(message.getUid())
971                     },
972                     null);
973             if (c.moveToNext()) {
974                 localMessage = EmailContent.getContent(c, EmailContent.Message.class);
975                 localMessage.mMailboxKey = folder.mId;
976                 localMessage.mAccountKey = account.mId;
977                 copyOneMessageToProvider(message, localMessage, loadStatus, mContext);
978             }
979         } finally {
980             if (c != null) {
981                 c.close();
982             }
983         }
984     }
985 
986     /**
987      * Copy one downloaded message (which may have partially-loaded sections)
988      * into an already-created EmailProvider Message
989      *
990      * @param message the remote message we've just downloaded
991      * @param localMessage the EmailProvider Message, already created
992      * @param loadStatus when complete, the message will be marked with this status (e.g.
993      *        EmailContent.Message.LOADED)
994      * @param context the context to be used for EmailProvider
995      */
copyOneMessageToProvider(Message message, EmailContent.Message localMessage, int loadStatus, Context context)996     public void copyOneMessageToProvider(Message message, EmailContent.Message localMessage,
997             int loadStatus, Context context) {
998         try {
999 
1000             EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
1001                     localMessage.mId);
1002             if (body == null) {
1003                 body = new EmailContent.Body();
1004             }
1005             try {
1006                 // Copy the fields that are available into the message object
1007                 LegacyConversions.updateMessageFields(localMessage, message,
1008                         localMessage.mAccountKey, localMessage.mMailboxKey);
1009 
1010                 // Now process body parts & attachments
1011                 ArrayList<Part> viewables = new ArrayList<Part>();
1012                 ArrayList<Part> attachments = new ArrayList<Part>();
1013                 MimeUtility.collectParts(message, viewables, attachments);
1014 
1015                 ConversionUtilities.updateBodyFields(body, localMessage, viewables);
1016 
1017                 // Commit the message & body to the local store immediately
1018                 saveOrUpdate(localMessage, context);
1019                 saveOrUpdate(body, context);
1020 
1021                 // process (and save) attachments
1022                 LegacyConversions.updateAttachments(context, localMessage, attachments);
1023 
1024                 // One last update of message with two updated flags
1025                 localMessage.mFlagLoaded = loadStatus;
1026 
1027                 ContentValues cv = new ContentValues();
1028                 cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
1029                 cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
1030                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
1031                         localMessage.mId);
1032                 context.getContentResolver().update(uri, cv, null, null);
1033 
1034             } catch (MessagingException me) {
1035                 Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
1036             }
1037 
1038         } catch (RuntimeException rte) {
1039             Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
1040         } catch (IOException ioe) {
1041             Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
1042         }
1043     }
1044 
processPendingActions(final long accountId)1045     public void processPendingActions(final long accountId) {
1046         put("processPendingActions", null, new Runnable() {
1047             public void run() {
1048                 try {
1049                     Account account = Account.restoreAccountWithId(mContext, accountId);
1050                     if (account == null) {
1051                         return;
1052                     }
1053                     processPendingActionsSynchronous(account);
1054                 }
1055                 catch (MessagingException me) {
1056                     if (Logging.LOGD) {
1057                         Log.v(Logging.LOG_TAG, "processPendingActions", me);
1058                     }
1059                     /*
1060                      * Ignore any exceptions from the commands. Commands will be processed
1061                      * on the next round.
1062                      */
1063                 }
1064             }
1065         });
1066     }
1067 
1068     /**
1069      * Find messages in the updated table that need to be written back to server.
1070      *
1071      * Handles:
1072      *   Read/Unread
1073      *   Flagged
1074      *   Append (upload)
1075      *   Move To Trash
1076      *   Empty trash
1077      * TODO:
1078      *   Move
1079      *
1080      * @param account the account to scan for pending actions
1081      * @throws MessagingException
1082      */
processPendingActionsSynchronous(Account account)1083     private void processPendingActionsSynchronous(Account account)
1084            throws MessagingException {
1085         TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
1086         ContentResolver resolver = mContext.getContentResolver();
1087         String[] accountIdArgs = new String[] { Long.toString(account.mId) };
1088 
1089         // Handle deletes first, it's always better to get rid of things first
1090         processPendingDeletesSynchronous(account, resolver, accountIdArgs);
1091 
1092         // Handle uploads (currently, only to sent messages)
1093         processPendingUploadsSynchronous(account, resolver, accountIdArgs);
1094 
1095         // Now handle updates / upsyncs
1096         processPendingUpdatesSynchronous(account, resolver, accountIdArgs);
1097     }
1098 
1099     /**
1100      * Get the mailbox corresponding to the remote location of a message; this will normally be
1101      * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
1102      * by serverId
1103      * @param message the message in question
1104      * @return the mailbox in which the message resides on the server
1105      */
getRemoteMailboxForMessage(EmailContent.Message message)1106     private Mailbox getRemoteMailboxForMessage(EmailContent.Message message) {
1107         // If this is a search result, use the protocolSearchInfo field to get the server info
1108         if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
1109             long accountKey = message.mAccountKey;
1110             String protocolSearchInfo = message.mProtocolSearchInfo;
1111             if (accountKey == mLastSearchAccountKey &&
1112                     protocolSearchInfo.equals(mLastSearchServerId)) {
1113                 return mLastSearchRemoteMailbox;
1114             }
1115             Cursor c =  mContext.getContentResolver().query(Mailbox.CONTENT_URI,
1116                     Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
1117                     new String[] {protocolSearchInfo, Long.toString(accountKey)},
1118                     null);
1119             try {
1120                 if (c.moveToNext()) {
1121                     Mailbox mailbox = new Mailbox();
1122                     mailbox.restore(c);
1123                     mLastSearchAccountKey = accountKey;
1124                     mLastSearchServerId = protocolSearchInfo;
1125                     mLastSearchRemoteMailbox = mailbox;
1126                     return mailbox;
1127                 } else {
1128                     return null;
1129                 }
1130             } finally {
1131                 c.close();
1132             }
1133         } else {
1134             return Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
1135         }
1136     }
1137 
1138     /**
1139      * Scan for messages that are in the Message_Deletes table, look for differences that
1140      * we can deal with, and do the work.
1141      *
1142      * @param account
1143      * @param resolver
1144      * @param accountIdArgs
1145      */
processPendingDeletesSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1146     private void processPendingDeletesSynchronous(Account account,
1147             ContentResolver resolver, String[] accountIdArgs) {
1148         Cursor deletes = resolver.query(EmailContent.Message.DELETED_CONTENT_URI,
1149                 EmailContent.Message.CONTENT_PROJECTION,
1150                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
1151                 EmailContent.MessageColumns.MAILBOX_KEY);
1152         long lastMessageId = -1;
1153         try {
1154             // Defer setting up the store until we know we need to access it
1155             Store remoteStore = null;
1156             // loop through messages marked as deleted
1157             while (deletes.moveToNext()) {
1158                 boolean deleteFromTrash = false;
1159 
1160                 EmailContent.Message oldMessage =
1161                         EmailContent.getContent(deletes, EmailContent.Message.class);
1162 
1163                 if (oldMessage != null) {
1164                     lastMessageId = oldMessage.mId;
1165 
1166                     Mailbox mailbox = getRemoteMailboxForMessage(oldMessage);
1167                     if (mailbox == null) {
1168                         continue; // Mailbox removed. Move to the next message.
1169                     }
1170                     deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
1171 
1172                     // Load the remote store if it will be needed
1173                     if (remoteStore == null && deleteFromTrash) {
1174                         remoteStore = Store.getInstance(account, mContext);
1175                     }
1176 
1177                     // Dispatch here for specific change types
1178                     if (deleteFromTrash) {
1179                         // Move message to trash
1180                         processPendingDeleteFromTrash(remoteStore, account, mailbox, oldMessage);
1181                     }
1182                 }
1183 
1184                 // Finally, delete the update
1185                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
1186                         oldMessage.mId);
1187                 resolver.delete(uri, null, null);
1188             }
1189         } catch (MessagingException me) {
1190             // Presumably an error here is an account connection failure, so there is
1191             // no point in continuing through the rest of the pending updates.
1192             if (Email.DEBUG) {
1193                 Log.d(Logging.LOG_TAG, "Unable to process pending delete for id="
1194                             + lastMessageId + ": " + me);
1195             }
1196         } finally {
1197             deletes.close();
1198         }
1199     }
1200 
1201     /**
1202      * Scan for messages that are in Sent, and are in need of upload,
1203      * and send them to the server.  "In need of upload" is defined as:
1204      *  serverId == null (no UID has been assigned)
1205      * or
1206      *  message is in the updated list
1207      *
1208      * Note we also look for messages that are moving from drafts->outbox->sent.  They never
1209      * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
1210      * uploaded directly to the Sent folder.
1211      *
1212      * @param account
1213      * @param resolver
1214      * @param accountIdArgs
1215      */
processPendingUploadsSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1216     private void processPendingUploadsSynchronous(Account account,
1217             ContentResolver resolver, String[] accountIdArgs) {
1218         // Find the Sent folder (since that's all we're uploading for now
1219         Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
1220                 MailboxColumns.ACCOUNT_KEY + "=?"
1221                 + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
1222                 accountIdArgs, null);
1223         long lastMessageId = -1;
1224         try {
1225             // Defer setting up the store until we know we need to access it
1226             Store remoteStore = null;
1227             while (mailboxes.moveToNext()) {
1228                 long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
1229                 String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
1230                 // Demand load mailbox
1231                 Mailbox mailbox = null;
1232 
1233                 // First handle the "new" messages (serverId == null)
1234                 Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
1235                         EmailContent.Message.ID_PROJECTION,
1236                         EmailContent.Message.MAILBOX_KEY + "=?"
1237                         + " and (" + EmailContent.Message.SERVER_ID + " is null"
1238                         + " or " + EmailContent.Message.SERVER_ID + "=''" + ")",
1239                         mailboxKeyArgs,
1240                         null);
1241                 try {
1242                     while (upsyncs1.moveToNext()) {
1243                         // Load the remote store if it will be needed
1244                         if (remoteStore == null) {
1245                             remoteStore = Store.getInstance(account, mContext);
1246                         }
1247                         // Load the mailbox if it will be needed
1248                         if (mailbox == null) {
1249                             mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1250                             if (mailbox == null) {
1251                                 continue; // Mailbox removed. Move to the next message.
1252                             }
1253                         }
1254                         // upsync the message
1255                         long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
1256                         lastMessageId = id;
1257                         processUploadMessage(resolver, remoteStore, account, mailbox, id);
1258                     }
1259                 } finally {
1260                     if (upsyncs1 != null) {
1261                         upsyncs1.close();
1262                     }
1263                 }
1264 
1265                 // Next, handle any updates (e.g. edited in place, although this shouldn't happen)
1266                 Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
1267                         EmailContent.Message.ID_PROJECTION,
1268                         EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs,
1269                         null);
1270                 try {
1271                     while (upsyncs2.moveToNext()) {
1272                         // Load the remote store if it will be needed
1273                         if (remoteStore == null) {
1274                             remoteStore = Store.getInstance(account, mContext);
1275                         }
1276                         // Load the mailbox if it will be needed
1277                         if (mailbox == null) {
1278                             mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1279                             if (mailbox == null) {
1280                                 continue; // Mailbox removed. Move to the next message.
1281                             }
1282                         }
1283                         // upsync the message
1284                         long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
1285                         lastMessageId = id;
1286                         processUploadMessage(resolver, remoteStore, account, mailbox, id);
1287                     }
1288                 } finally {
1289                     if (upsyncs2 != null) {
1290                         upsyncs2.close();
1291                     }
1292                 }
1293             }
1294         } catch (MessagingException me) {
1295             // Presumably an error here is an account connection failure, so there is
1296             // no point in continuing through the rest of the pending updates.
1297             if (Email.DEBUG) {
1298                 Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
1299                         + lastMessageId + ": " + me);
1300             }
1301         } finally {
1302             if (mailboxes != null) {
1303                 mailboxes.close();
1304             }
1305         }
1306     }
1307 
1308     /**
1309      * Scan for messages that are in the Message_Updates table, look for differences that
1310      * we can deal with, and do the work.
1311      *
1312      * @param account
1313      * @param resolver
1314      * @param accountIdArgs
1315      */
processPendingUpdatesSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1316     private void processPendingUpdatesSynchronous(Account account,
1317             ContentResolver resolver, String[] accountIdArgs) {
1318         Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
1319                 EmailContent.Message.CONTENT_PROJECTION,
1320                 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
1321                 EmailContent.MessageColumns.MAILBOX_KEY);
1322         long lastMessageId = -1;
1323         try {
1324             // Defer setting up the store until we know we need to access it
1325             Store remoteStore = null;
1326             // Demand load mailbox (note order-by to reduce thrashing here)
1327             Mailbox mailbox = null;
1328             // loop through messages marked as needing updates
1329             while (updates.moveToNext()) {
1330                 boolean changeMoveToTrash = false;
1331                 boolean changeRead = false;
1332                 boolean changeFlagged = false;
1333                 boolean changeMailbox = false;
1334                 boolean changeAnswered = false;
1335 
1336                 EmailContent.Message oldMessage =
1337                     EmailContent.getContent(updates, EmailContent.Message.class);
1338                 lastMessageId = oldMessage.mId;
1339                 EmailContent.Message newMessage =
1340                     EmailContent.Message.restoreMessageWithId(mContext, oldMessage.mId);
1341                 if (newMessage != null) {
1342                     mailbox = Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey);
1343                     if (mailbox == null) {
1344                         continue; // Mailbox removed. Move to the next message.
1345                     }
1346                     if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
1347                         if (mailbox.mType == Mailbox.TYPE_TRASH) {
1348                             changeMoveToTrash = true;
1349                         } else {
1350                             changeMailbox = true;
1351                         }
1352                     }
1353                     changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
1354                     changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
1355                     changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
1356                         (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
1357                }
1358 
1359                 // Load the remote store if it will be needed
1360                 if (remoteStore == null &&
1361                         (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
1362                                 changeAnswered)) {
1363                     remoteStore = Store.getInstance(account, mContext);
1364                 }
1365 
1366                 // Dispatch here for specific change types
1367                 if (changeMoveToTrash) {
1368                     // Move message to trash
1369                     processPendingMoveToTrash(remoteStore, account, mailbox, oldMessage,
1370                             newMessage);
1371                 } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
1372                     processPendingDataChange(remoteStore, mailbox, changeRead, changeFlagged,
1373                             changeMailbox, changeAnswered, oldMessage, newMessage);
1374                 }
1375 
1376                 // Finally, delete the update
1377                 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
1378                         oldMessage.mId);
1379                 resolver.delete(uri, null, null);
1380             }
1381 
1382         } catch (MessagingException me) {
1383             // Presumably an error here is an account connection failure, so there is
1384             // no point in continuing through the rest of the pending updates.
1385             if (Email.DEBUG) {
1386                 Log.d(Logging.LOG_TAG, "Unable to process pending update for id="
1387                             + lastMessageId + ": " + me);
1388             }
1389         } finally {
1390             updates.close();
1391         }
1392     }
1393 
1394     /**
1395      * Upsync an entire message.  This must also unwind whatever triggered it (either by
1396      * updating the serverId, or by deleting the update record, or it's going to keep happening
1397      * over and over again.
1398      *
1399      * Note:  If the message is being uploaded into an unexpected mailbox, we *do not* upload.
1400      * This is to avoid unnecessary uploads into the trash.  Although the caller attempts to select
1401      * only the Drafts and Sent folders, this can happen when the update record and the current
1402      * record mismatch.  In this case, we let the update record remain, because the filters
1403      * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
1404      * appropriately.
1405      *
1406      * @param resolver
1407      * @param remoteStore
1408      * @param account
1409      * @param mailbox the actual mailbox
1410      * @param messageId
1411      */
processUploadMessage(ContentResolver resolver, Store remoteStore, Account account, Mailbox mailbox, long messageId)1412     private void processUploadMessage(ContentResolver resolver, Store remoteStore,
1413             Account account, Mailbox mailbox, long messageId)
1414             throws MessagingException {
1415         EmailContent.Message newMessage =
1416             EmailContent.Message.restoreMessageWithId(mContext, messageId);
1417         boolean deleteUpdate = false;
1418         if (newMessage == null) {
1419             deleteUpdate = true;
1420             Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
1421         } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
1422             deleteUpdate = false;
1423             Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
1424         } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
1425             deleteUpdate = false;
1426             Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
1427         } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
1428             deleteUpdate = false;
1429             Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
1430         } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) {
1431             deleteUpdate = false;
1432             Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
1433         } else {
1434             Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId);
1435             deleteUpdate = processPendingAppend(remoteStore, account, mailbox, newMessage);
1436         }
1437         if (deleteUpdate) {
1438             // Finally, delete the update (if any)
1439             Uri uri = ContentUris.withAppendedId(
1440                     EmailContent.Message.UPDATED_CONTENT_URI, messageId);
1441             resolver.delete(uri, null, null);
1442         }
1443     }
1444 
1445     /**
1446      * Upsync changes to read, flagged, or mailbox
1447      *
1448      * @param remoteStore the remote store for this mailbox
1449      * @param mailbox the mailbox the message is stored in
1450      * @param changeRead whether the message's read state has changed
1451      * @param changeFlagged whether the message's flagged state has changed
1452      * @param changeMailbox whether the message's mailbox has changed
1453      * @param oldMessage the message in it's pre-change state
1454      * @param newMessage the current version of the message
1455      */
processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1456     private void processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead,
1457             boolean changeFlagged, boolean changeMailbox, boolean changeAnswered,
1458             EmailContent.Message oldMessage, final EmailContent.Message newMessage)
1459             throws MessagingException {
1460         // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
1461         // being moved
1462         Mailbox newMailbox = mailbox;
1463         // Mailbox is the original remote mailbox (the one we're acting on)
1464         mailbox = getRemoteMailboxForMessage(oldMessage);
1465 
1466         // 0. No remote update if the message is local-only
1467         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1468                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
1469             return;
1470         }
1471 
1472         // 1. No remote update for DRAFTS or OUTBOX
1473         if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
1474             return;
1475         }
1476 
1477         // 2. Open the remote store & folder
1478         Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1479         if (!remoteFolder.exists()) {
1480             return;
1481         }
1482         remoteFolder.open(OpenMode.READ_WRITE);
1483         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1484             return;
1485         }
1486 
1487         // 3. Finally, apply the changes to the message
1488         Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
1489         if (remoteMessage == null) {
1490             return;
1491         }
1492         if (Email.DEBUG) {
1493             Log.d(Logging.LOG_TAG,
1494                     "Update for msg id=" + newMessage.mId
1495                     + " read=" + newMessage.mFlagRead
1496                     + " flagged=" + newMessage.mFlagFavorite
1497                     + " answered="
1498                     + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
1499                     + " new mailbox=" + newMessage.mMailboxKey);
1500         }
1501         Message[] messages = new Message[] { remoteMessage };
1502         if (changeRead) {
1503             remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
1504         }
1505         if (changeFlagged) {
1506             remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
1507         }
1508         if (changeAnswered) {
1509             remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
1510                     (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
1511         }
1512         if (changeMailbox) {
1513             Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
1514             if (!remoteFolder.exists()) {
1515                 return;
1516             }
1517             // We may need the message id to search for the message in the destination folder
1518             remoteMessage.setMessageId(newMessage.mMessageId);
1519             // Copy the message to its new folder
1520             remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
1521                 @Override
1522                 public void onMessageUidChange(Message message, String newUid) {
1523                     ContentValues cv = new ContentValues();
1524                     cv.put(EmailContent.Message.SERVER_ID, newUid);
1525                     // We only have one message, so, any updates _must_ be for it. Otherwise,
1526                     // we'd have to cycle through to find the one with the same server ID.
1527                     mContext.getContentResolver().update(ContentUris.withAppendedId(
1528                             EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
1529                 }
1530                 @Override
1531                 public void onMessageNotFound(Message message) {
1532                 }
1533             });
1534             // Delete the message from the remote source folder
1535             remoteMessage.setFlag(Flag.DELETED, true);
1536             remoteFolder.expunge();
1537         }
1538         remoteFolder.close(false);
1539     }
1540 
1541     /**
1542      * Process a pending trash message command.
1543      *
1544      * @param remoteStore the remote store we're working in
1545      * @param account The account in which we are working
1546      * @param newMailbox The local trash mailbox
1547      * @param oldMessage The message copy that was saved in the updates shadow table
1548      * @param newMessage The message that was moved to the mailbox
1549      */
processPendingMoveToTrash(Store remoteStore, Account account, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1550     private void processPendingMoveToTrash(Store remoteStore,
1551             Account account, Mailbox newMailbox, EmailContent.Message oldMessage,
1552             final EmailContent.Message newMessage) throws MessagingException {
1553 
1554         // 0. No remote move if the message is local-only
1555         if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1556                 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
1557             return;
1558         }
1559 
1560         // 1. Escape early if we can't find the local mailbox
1561         // TODO smaller projection here
1562         Mailbox oldMailbox = getRemoteMailboxForMessage(oldMessage);
1563         if (oldMailbox == null) {
1564             // can't find old mailbox, it may have been deleted.  just return.
1565             return;
1566         }
1567         // 2. We don't support delete-from-trash here
1568         if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
1569             return;
1570         }
1571 
1572         // 3. If DELETE_POLICY_NEVER, simply write back the deleted sentinel and return
1573         //
1574         // This sentinel takes the place of the server-side message, and locally "deletes" it
1575         // by inhibiting future sync or display of the message.  It will eventually go out of
1576         // scope when it becomes old, or is deleted on the server, and the regular sync code
1577         // will clean it up for us.
1578         if (account.getDeletePolicy() == Account.DELETE_POLICY_NEVER) {
1579             EmailContent.Message sentinel = new EmailContent.Message();
1580             sentinel.mAccountKey = oldMessage.mAccountKey;
1581             sentinel.mMailboxKey = oldMessage.mMailboxKey;
1582             sentinel.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED;
1583             sentinel.mFlagRead = true;
1584             sentinel.mServerId = oldMessage.mServerId;
1585             sentinel.save(mContext);
1586 
1587             return;
1588         }
1589 
1590         // The rest of this method handles server-side deletion
1591 
1592         // 4.  Find the remote mailbox (that we deleted from), and open it
1593         Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
1594         if (!remoteFolder.exists()) {
1595             return;
1596         }
1597 
1598         remoteFolder.open(OpenMode.READ_WRITE);
1599         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1600             remoteFolder.close(false);
1601             return;
1602         }
1603 
1604         // 5. Find the remote original message
1605         Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
1606         if (remoteMessage == null) {
1607             remoteFolder.close(false);
1608             return;
1609         }
1610 
1611         // 6. Find the remote trash folder, and create it if not found
1612         Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
1613         if (!remoteTrashFolder.exists()) {
1614             /*
1615              * If the remote trash folder doesn't exist we try to create it.
1616              */
1617             remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
1618         }
1619 
1620         // 7.  Try to copy the message into the remote trash folder
1621         // Note, this entire section will be skipped for POP3 because there's no remote trash
1622         if (remoteTrashFolder.exists()) {
1623             /*
1624              * Because remoteTrashFolder may be new, we need to explicitly open it
1625              */
1626             remoteTrashFolder.open(OpenMode.READ_WRITE);
1627             if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1628                 remoteFolder.close(false);
1629                 remoteTrashFolder.close(false);
1630                 return;
1631             }
1632 
1633             remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
1634                     new Folder.MessageUpdateCallbacks() {
1635                 public void onMessageUidChange(Message message, String newUid) {
1636                     // update the UID in the local trash folder, because some stores will
1637                     // have to change it when copying to remoteTrashFolder
1638                     ContentValues cv = new ContentValues();
1639                     cv.put(EmailContent.Message.SERVER_ID, newUid);
1640                     mContext.getContentResolver().update(newMessage.getUri(), cv, null, null);
1641                 }
1642 
1643                 /**
1644                  * This will be called if the deleted message doesn't exist and can't be
1645                  * deleted (e.g. it was already deleted from the server.)  In this case,
1646                  * attempt to delete the local copy as well.
1647                  */
1648                 public void onMessageNotFound(Message message) {
1649                     mContext.getContentResolver().delete(newMessage.getUri(), null, null);
1650                 }
1651             });
1652             remoteTrashFolder.close(false);
1653         }
1654 
1655         // 8. Delete the message from the remote source folder
1656         remoteMessage.setFlag(Flag.DELETED, true);
1657         remoteFolder.expunge();
1658         remoteFolder.close(false);
1659     }
1660 
1661     /**
1662      * Process a pending trash message command.
1663      *
1664      * @param remoteStore the remote store we're working in
1665      * @param account The account in which we are working
1666      * @param oldMailbox The local trash mailbox
1667      * @param oldMessage The message that was deleted from the trash
1668      */
processPendingDeleteFromTrash(Store remoteStore, Account account, Mailbox oldMailbox, EmailContent.Message oldMessage)1669     private void processPendingDeleteFromTrash(Store remoteStore,
1670             Account account, Mailbox oldMailbox, EmailContent.Message oldMessage)
1671             throws MessagingException {
1672 
1673         // 1. We only support delete-from-trash here
1674         if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
1675             return;
1676         }
1677 
1678         // 2.  Find the remote trash folder (that we are deleting from), and open it
1679         Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
1680         if (!remoteTrashFolder.exists()) {
1681             return;
1682         }
1683 
1684         remoteTrashFolder.open(OpenMode.READ_WRITE);
1685         if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1686             remoteTrashFolder.close(false);
1687             return;
1688         }
1689 
1690         // 3. Find the remote original message
1691         Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
1692         if (remoteMessage == null) {
1693             remoteTrashFolder.close(false);
1694             return;
1695         }
1696 
1697         // 4. Delete the message from the remote trash folder
1698         remoteMessage.setFlag(Flag.DELETED, true);
1699         remoteTrashFolder.expunge();
1700         remoteTrashFolder.close(false);
1701     }
1702 
1703     /**
1704      * Process a pending append message command. This command uploads a local message to the
1705      * server, first checking to be sure that the server message is not newer than
1706      * the local message.
1707      *
1708      * @param remoteStore the remote store we're working in
1709      * @param account The account in which we are working
1710      * @param newMailbox The mailbox we're appending to
1711      * @param message The message we're appending
1712      * @return true if successfully uploaded
1713      */
processPendingAppend(Store remoteStore, Account account, Mailbox newMailbox, EmailContent.Message message)1714     private boolean processPendingAppend(Store remoteStore, Account account,
1715             Mailbox newMailbox, EmailContent.Message message)
1716             throws MessagingException {
1717 
1718         boolean updateInternalDate = false;
1719         boolean updateMessage = false;
1720         boolean deleteMessage = false;
1721 
1722         // 1. Find the remote folder that we're appending to and create and/or open it
1723         Folder remoteFolder = remoteStore.getFolder(newMailbox.mServerId);
1724         if (!remoteFolder.exists()) {
1725             if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) {
1726                 // This is POP3, we cannot actually upload.  Instead, we'll update the message
1727                 // locally with a fake serverId (so we don't keep trying here) and return.
1728                 if (message.mServerId == null || message.mServerId.length() == 0) {
1729                     message.mServerId = LOCAL_SERVERID_PREFIX + message.mId;
1730                     Uri uri =
1731                         ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
1732                     ContentValues cv = new ContentValues();
1733                     cv.put(EmailContent.Message.SERVER_ID, message.mServerId);
1734                     mContext.getContentResolver().update(uri, cv, null, null);
1735                 }
1736                 return true;
1737             }
1738             if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
1739                 // This is a (hopefully) transient error and we return false to try again later
1740                 return false;
1741             }
1742         }
1743         remoteFolder.open(OpenMode.READ_WRITE);
1744         if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1745             return false;
1746         }
1747 
1748         // 2. If possible, load a remote message with the matching UID
1749         Message remoteMessage = null;
1750         if (message.mServerId != null && message.mServerId.length() > 0) {
1751             remoteMessage = remoteFolder.getMessage(message.mServerId);
1752         }
1753 
1754         // 3. If a remote message could not be found, upload our local message
1755         if (remoteMessage == null) {
1756             // 3a. Create a legacy message to upload
1757             Message localMessage = LegacyConversions.makeMessage(mContext, message);
1758 
1759             // 3b. Upload it
1760             FetchProfile fp = new FetchProfile();
1761             fp.add(FetchProfile.Item.BODY);
1762             remoteFolder.appendMessages(new Message[] { localMessage });
1763 
1764             // 3b. And record the UID from the server
1765             message.mServerId = localMessage.getUid();
1766             updateInternalDate = true;
1767             updateMessage = true;
1768         } else {
1769             // 4. If the remote message exists we need to determine which copy to keep.
1770             FetchProfile fp = new FetchProfile();
1771             fp.add(FetchProfile.Item.ENVELOPE);
1772             remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1773             Date localDate = new Date(message.mServerTimeStamp);
1774             Date remoteDate = remoteMessage.getInternalDate();
1775             if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
1776                 // 4a. If the remote message is newer than ours we'll just
1777                 // delete ours and move on. A sync will get the server message
1778                 // if we need to be able to see it.
1779                 deleteMessage = true;
1780             } else {
1781                 // 4b. Otherwise we'll upload our message and then delete the remote message.
1782 
1783                 // Create a legacy message to upload
1784                 Message localMessage = LegacyConversions.makeMessage(mContext, message);
1785 
1786                 // 4c. Upload it
1787                 fp.clear();
1788                 fp = new FetchProfile();
1789                 fp.add(FetchProfile.Item.BODY);
1790                 remoteFolder.appendMessages(new Message[] { localMessage });
1791 
1792                 // 4d. Record the UID and new internalDate from the server
1793                 message.mServerId = localMessage.getUid();
1794                 updateInternalDate = true;
1795                 updateMessage = true;
1796 
1797                 // 4e. And delete the old copy of the message from the server
1798                 remoteMessage.setFlag(Flag.DELETED, true);
1799             }
1800         }
1801 
1802         // 5. If requested, Best-effort to capture new "internaldate" from the server
1803         if (updateInternalDate && message.mServerId != null) {
1804             try {
1805                 Message remoteMessage2 = remoteFolder.getMessage(message.mServerId);
1806                 if (remoteMessage2 != null) {
1807                     FetchProfile fp2 = new FetchProfile();
1808                     fp2.add(FetchProfile.Item.ENVELOPE);
1809                     remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null);
1810                     message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime();
1811                     updateMessage = true;
1812                 }
1813             } catch (MessagingException me) {
1814                 // skip it - we can live without this
1815             }
1816         }
1817 
1818         // 6. Perform required edits to local copy of message
1819         if (deleteMessage || updateMessage) {
1820             Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
1821             ContentResolver resolver = mContext.getContentResolver();
1822             if (deleteMessage) {
1823                 resolver.delete(uri, null, null);
1824             } else if (updateMessage) {
1825                 ContentValues cv = new ContentValues();
1826                 cv.put(EmailContent.Message.SERVER_ID, message.mServerId);
1827                 cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp);
1828                 resolver.update(uri, cv, null, null);
1829             }
1830         }
1831 
1832         return true;
1833     }
1834 
1835     /**
1836      * Finish loading a message that have been partially downloaded.
1837      *
1838      * @param messageId the message to load
1839      * @param listener the callback by which results will be reported
1840      */
loadMessageForView(final long messageId, MessagingListener listener)1841     public void loadMessageForView(final long messageId, MessagingListener listener) {
1842         mListeners.loadMessageForViewStarted(messageId);
1843         put("loadMessageForViewRemote", listener, new Runnable() {
1844             public void run() {
1845                 try {
1846                     // 1. Resample the message, in case it disappeared or synced while
1847                     // this command was in queue
1848                     EmailContent.Message message =
1849                         EmailContent.Message.restoreMessageWithId(mContext, messageId);
1850                     if (message == null) {
1851                         mListeners.loadMessageForViewFailed(messageId, "Unknown message");
1852                         return;
1853                     }
1854                     if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
1855                         mListeners.loadMessageForViewFinished(messageId);
1856                         return;
1857                     }
1858 
1859                     // 2. Open the remote folder.
1860                     // TODO combine with common code in loadAttachment
1861                     Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
1862                     Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
1863                     if (account == null || mailbox == null) {
1864                         mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
1865                         return;
1866                     }
1867                     TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
1868 
1869                     Store remoteStore = Store.getInstance(account, mContext);
1870                     String remoteServerId = mailbox.mServerId;
1871                     // If this is a search result, use the protocolSearchInfo field to get the
1872                     // correct remote location
1873                     if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
1874                         remoteServerId = message.mProtocolSearchInfo;
1875                     }
1876                     Folder remoteFolder = remoteStore.getFolder(remoteServerId);
1877                     remoteFolder.open(OpenMode.READ_WRITE);
1878 
1879                     // 3. Set up to download the entire message
1880                     Message remoteMessage = remoteFolder.getMessage(message.mServerId);
1881                     FetchProfile fp = new FetchProfile();
1882                     fp.add(FetchProfile.Item.BODY);
1883                     remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1884 
1885                     // 4. Write to provider
1886                     copyOneMessageToProvider(remoteMessage, account, mailbox,
1887                             EmailContent.Message.FLAG_LOADED_COMPLETE);
1888 
1889                     // 5. Notify UI
1890                     mListeners.loadMessageForViewFinished(messageId);
1891 
1892                 } catch (MessagingException me) {
1893                     if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
1894                     mListeners.loadMessageForViewFailed(messageId, me.getMessage());
1895                 } catch (RuntimeException rte) {
1896                     mListeners.loadMessageForViewFailed(messageId, rte.getMessage());
1897                 }
1898             }
1899         });
1900     }
1901 
1902     /**
1903      * Attempts to load the attachment specified by id from the given account and message.
1904      */
loadAttachment(final long accountId, final long messageId, final long mailboxId, final long attachmentId, MessagingListener listener, final boolean background)1905     public void loadAttachment(final long accountId, final long messageId, final long mailboxId,
1906             final long attachmentId, MessagingListener listener, final boolean background) {
1907         mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true);
1908 
1909         put("loadAttachment", listener, new Runnable() {
1910             public void run() {
1911                 try {
1912                     //1. Check if the attachment is already here and return early in that case
1913                     Attachment attachment =
1914                         Attachment.restoreAttachmentWithId(mContext, attachmentId);
1915                     if (attachment == null) {
1916                         mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
1917                                    new MessagingException("The attachment is null"),
1918                                    background);
1919                         return;
1920                     }
1921                     if (Utility.attachmentExists(mContext, attachment)) {
1922                         mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
1923                         return;
1924                     }
1925 
1926                     // 2. Open the remote folder.
1927                     // TODO all of these could be narrower projections
1928                     Account account = Account.restoreAccountWithId(mContext, accountId);
1929                     Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1930                     EmailContent.Message message =
1931                         EmailContent.Message.restoreMessageWithId(mContext, messageId);
1932 
1933                     if (account == null || mailbox == null || message == null) {
1934                         mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
1935                                 new MessagingException(
1936                                         "Account, mailbox, message or attachment are null"),
1937                                 background);
1938                         return;
1939                     }
1940                     TrafficStats.setThreadStatsTag(
1941                             TrafficFlags.getAttachmentFlags(mContext, account));
1942 
1943                     Store remoteStore = Store.getInstance(account, mContext);
1944                     Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1945                     remoteFolder.open(OpenMode.READ_WRITE);
1946 
1947                     // 3. Generate a shell message in which to retrieve the attachment,
1948                     // and a shell BodyPart for the attachment.  Then glue them together.
1949                     Message storeMessage = remoteFolder.createMessage(message.mServerId);
1950                     MimeBodyPart storePart = new MimeBodyPart();
1951                     storePart.setSize((int)attachment.mSize);
1952                     storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
1953                             attachment.mLocation);
1954                     storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
1955                             String.format("%s;\n name=\"%s\"",
1956                             attachment.mMimeType,
1957                             attachment.mFileName));
1958                     // TODO is this always true for attachments?  I think we dropped the
1959                     // true encoding along the way
1960                     storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
1961 
1962                     MimeMultipart multipart = new MimeMultipart();
1963                     multipart.setSubType("mixed");
1964                     multipart.addBodyPart(storePart);
1965 
1966                     storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
1967                     storeMessage.setBody(multipart);
1968 
1969                     // 4. Now ask for the attachment to be fetched
1970                     FetchProfile fp = new FetchProfile();
1971                     fp.add(storePart);
1972                     remoteFolder.fetch(new Message[] { storeMessage }, fp,
1973                             mController.new MessageRetrievalListenerBridge(
1974                                     messageId, attachmentId));
1975 
1976                     // If we failed to load the attachment, throw an Exception here, so that
1977                     // AttachmentDownloadService knows that we failed
1978                     if (storePart.getBody() == null) {
1979                         throw new MessagingException("Attachment not loaded.");
1980                     }
1981 
1982                     // 5. Save the downloaded file and update the attachment as necessary
1983                     LegacyConversions.saveAttachmentBody(mContext, storePart, attachment,
1984                             accountId);
1985 
1986                     // 6. Report success
1987                     mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
1988                 }
1989                 catch (MessagingException me) {
1990                     if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
1991                     mListeners.loadAttachmentFailed(
1992                             accountId, messageId, attachmentId, me, background);
1993                 } catch (IOException ioe) {
1994                     Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
1995                 }
1996             }});
1997     }
1998 
1999     /**
2000      * Attempt to send any messages that are sitting in the Outbox.
2001      * @param account
2002      * @param listener
2003      */
sendPendingMessages(final Account account, final long sentFolderId, MessagingListener listener)2004     public void sendPendingMessages(final Account account, final long sentFolderId,
2005             MessagingListener listener) {
2006         put("sendPendingMessages", listener, new Runnable() {
2007             public void run() {
2008                 sendPendingMessagesSynchronous(account, sentFolderId);
2009             }
2010         });
2011     }
2012 
2013     /**
2014      * Attempt to send all messages sitting in the given account's outbox. Optionally,
2015      * if the server requires it, the message will be moved to the given sent folder.
2016      */
sendPendingMessagesSynchronous(final Account account, long sentFolderId)2017     public void sendPendingMessagesSynchronous(final Account account,
2018             long sentFolderId) {
2019         TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account));
2020         NotificationController nc = NotificationController.getInstance(mContext);
2021         // 1.  Loop through all messages in the account's outbox
2022         long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
2023         if (outboxId == Mailbox.NO_MAILBOX) {
2024             return;
2025         }
2026         ContentResolver resolver = mContext.getContentResolver();
2027         Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
2028                 EmailContent.Message.ID_COLUMN_PROJECTION,
2029                 EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
2030                 null);
2031         try {
2032             // 2.  exit early
2033             if (c.getCount() <= 0) {
2034                 return;
2035             }
2036             // 3. do one-time setup of the Sender & other stuff
2037             mListeners.sendPendingMessagesStarted(account.mId, -1);
2038 
2039             Sender sender = Sender.getInstance(mContext, account);
2040             Store remoteStore = Store.getInstance(account, mContext);
2041             boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
2042             ContentValues moveToSentValues = null;
2043             if (requireMoveMessageToSentFolder) {
2044                 moveToSentValues = new ContentValues();
2045                 moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId);
2046             }
2047 
2048             // 4.  loop through the available messages and send them
2049             while (c.moveToNext()) {
2050                 long messageId = -1;
2051                 try {
2052                     messageId = c.getLong(0);
2053                     mListeners.sendPendingMessagesStarted(account.mId, messageId);
2054                     // Don't send messages with unloaded attachments
2055                     if (Utility.hasUnloadedAttachments(mContext, messageId)) {
2056                         if (Email.DEBUG) {
2057                             Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
2058                                     "; unloaded attachments");
2059                         }
2060                         continue;
2061                     }
2062                     sender.sendMessage(messageId);
2063                 } catch (MessagingException me) {
2064                     // report error for this message, but keep trying others
2065                     if (me instanceof AuthenticationFailedException) {
2066                         nc.showLoginFailedNotification(account.mId);
2067                     }
2068                     mListeners.sendPendingMessagesFailed(account.mId, messageId, me);
2069                     continue;
2070                 }
2071                 // 5. move to sent, or delete
2072                 Uri syncedUri =
2073                     ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
2074                 if (requireMoveMessageToSentFolder) {
2075                     // If this is a forwarded message and it has attachments, delete them, as they
2076                     // duplicate information found elsewhere (on the server).  This saves storage.
2077                     EmailContent.Message msg =
2078                         EmailContent.Message.restoreMessageWithId(mContext, messageId);
2079                     if (msg != null &&
2080                             ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
2081                         AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
2082                                 messageId);
2083                     }
2084                     resolver.update(syncedUri, moveToSentValues, null, null);
2085                 } else {
2086                     AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
2087                             messageId);
2088                     Uri uri =
2089                         ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
2090                     resolver.delete(uri, null, null);
2091                     resolver.delete(syncedUri, null, null);
2092                 }
2093             }
2094             // 6. report completion/success
2095             mListeners.sendPendingMessagesCompleted(account.mId);
2096             nc.cancelLoginFailedNotification(account.mId);
2097         } catch (MessagingException me) {
2098             if (me instanceof AuthenticationFailedException) {
2099                 nc.showLoginFailedNotification(account.mId);
2100             }
2101             mListeners.sendPendingMessagesFailed(account.mId, -1, me);
2102         } finally {
2103             c.close();
2104         }
2105     }
2106 
2107     /**
2108      * Checks mail for an account.
2109      * This entry point is for use by the mail checking service only, because it
2110      * gives slightly different callbacks (so the service doesn't get confused by callbacks
2111      * triggered by/for the foreground UI.
2112      *
2113      * TODO clean up the execution model which is unnecessarily threaded due to legacy code
2114      *
2115      * @param accountId the account to check
2116      * @param listener
2117      */
checkMail(final long accountId, final long tag, final MessagingListener listener)2118     public void checkMail(final long accountId, final long tag, final MessagingListener listener) {
2119         mListeners.checkMailStarted(mContext, accountId, tag);
2120 
2121         // This puts the command on the queue (not synchronous)
2122         listFolders(accountId, null);
2123 
2124         // Put this on the queue as well so it follows listFolders
2125         put("checkMail", listener, new Runnable() {
2126             public void run() {
2127                 // send any pending outbound messages.  note, there is a slight race condition
2128                 // here if we somehow don't have a sent folder, but this should never happen
2129                 // because the call to sendMessage() would have built one previously.
2130                 long inboxId = -1;
2131                 Account account = Account.restoreAccountWithId(mContext, accountId);
2132                 if (account != null) {
2133                     long sentboxId = Mailbox.findMailboxOfType(mContext, accountId,
2134                             Mailbox.TYPE_SENT);
2135                     if (sentboxId != Mailbox.NO_MAILBOX) {
2136                         sendPendingMessagesSynchronous(account, sentboxId);
2137                     }
2138                     // find mailbox # for inbox and sync it.
2139                     // TODO we already know this in Controller, can we pass it in?
2140                     inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
2141                     if (inboxId != Mailbox.NO_MAILBOX) {
2142                         Mailbox mailbox =
2143                             Mailbox.restoreMailboxWithId(mContext, inboxId);
2144                         if (mailbox != null) {
2145                             synchronizeMailboxSynchronous(account, mailbox);
2146                         }
2147                     }
2148                 }
2149                 mListeners.checkMailFinished(mContext, accountId, inboxId, tag);
2150             }
2151         });
2152     }
2153 
2154     private static class Command {
2155         public Runnable runnable;
2156 
2157         public MessagingListener listener;
2158 
2159         public String description;
2160 
2161         @Override
toString()2162         public String toString() {
2163             return description;
2164         }
2165     }
2166 
2167     /** Results of the latest synchronization. */
2168     private static class SyncResults {
2169         /** The total # of messages in the folder */
2170         public final int mTotalMessages;
2171         /** A list of new message IDs; must not be {@code null} */
2172         public final ArrayList<Long> mAddedMessages;
2173 
SyncResults(int totalMessages, ArrayList<Long> addedMessages)2174         public SyncResults(int totalMessages, ArrayList<Long> addedMessages) {
2175             if (addedMessages == null) {
2176                 throw new IllegalArgumentException("addedMessages must not be null");
2177             }
2178             mTotalMessages = totalMessages;
2179             mAddedMessages = addedMessages;
2180         }
2181     }
2182 }
2183