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