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