MessagingController.java revision 8978aac1977408b05e386ae846c30920c7faa0a6
1
2package com.android.email;
3
4import java.util.ArrayList;
5import java.util.Collections;
6import java.util.Date;
7import java.util.HashMap;
8import java.util.HashSet;
9import java.util.concurrent.BlockingQueue;
10import java.util.concurrent.LinkedBlockingQueue;
11
12import android.app.Application;
13import android.content.Context;
14import android.os.Process;
15import android.util.Config;
16import android.util.Log;
17
18import com.android.email.mail.FetchProfile;
19import com.android.email.mail.Flag;
20import com.android.email.mail.Folder;
21import com.android.email.mail.Message;
22import com.android.email.mail.MessageRetrievalListener;
23import com.android.email.mail.MessagingException;
24import com.android.email.mail.Part;
25import com.android.email.mail.Store;
26import com.android.email.mail.Transport;
27import com.android.email.mail.Folder.FolderType;
28import com.android.email.mail.Folder.OpenMode;
29import com.android.email.mail.internet.MimeHeader;
30import com.android.email.mail.internet.MimeUtility;
31import com.android.email.mail.store.LocalStore;
32import com.android.email.mail.store.LocalStore.LocalFolder;
33import com.android.email.mail.store.LocalStore.LocalMessage;
34import com.android.email.mail.store.LocalStore.PendingCommand;
35
36/**
37 * Starts a long running (application) Thread that will run through commands
38 * that require remote mailbox access. This class is used to serialize and
39 * prioritize these commands. Each method that will submit a command requires a
40 * MessagingListener instance to be provided. It is expected that that listener
41 * has also been added as a registered listener using addListener(). When a
42 * command is to be executed, if the listener that was provided with the command
43 * is no longer registered the command is skipped. The design idea for the above
44 * is that when an Activity starts it registers as a listener. When it is paused
45 * it removes itself. Thus, any commands that that activity submitted are
46 * removed from the queue once the activity is no longer active.
47 */
48public class MessagingController implements Runnable {
49    /**
50     * The maximum message size that we'll consider to be "small". A small message is downloaded
51     * in full immediately instead of in pieces. Anything over this size will be downloaded in
52     * pieces with attachments being left off completely and downloaded on demand.
53     *
54     *
55     * 25k for a "small" message was picked by educated trial and error.
56     * http://answers.google.com/answers/threadview?id=312463 claims that the
57     * average size of an email is 59k, which I feel is too large for our
58     * blind download. The following tests were performed on a download of
59     * 25 random messages.
60     * <pre>
61     * 5k - 61 seconds,
62     * 25k - 51 seconds,
63     * 55k - 53 seconds,
64     * </pre>
65     * So 25k gives good performance and a reasonable data footprint. Sounds good to me.
66     */
67    private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
68
69    private static final String PENDING_COMMAND_TRASH =
70        "com.android.email.MessagingController.trash";
71    private static final String PENDING_COMMAND_MARK_READ =
72        "com.android.email.MessagingController.markRead";
73    private static final String PENDING_COMMAND_APPEND =
74        "com.android.email.MessagingController.append";
75
76    private static MessagingController inst = null;
77    private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
78    private Thread mThread;
79    private HashSet<MessagingListener> mListeners = new HashSet<MessagingListener>();
80    private boolean mBusy;
81    private Application mApplication;
82
83    private MessagingController(Application application) {
84        mApplication = application;
85        mThread = new Thread(this);
86        mThread.start();
87    }
88
89    /**
90     * Gets or creates the singleton instance of MessagingController. Application is used to
91     * provide a Context to classes that need it.
92     * @param application
93     * @return
94     */
95    public synchronized static MessagingController getInstance(Application application) {
96        if (inst == null) {
97            inst = new MessagingController(application);
98        }
99        return inst;
100    }
101
102    public boolean isBusy() {
103        return mBusy;
104    }
105
106    public void run() {
107        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
108        while (true) {
109            try {
110                Command command = mCommands.take();
111                if (command.listener == null || mListeners.contains(command.listener)) {
112                    mBusy = true;
113                    command.runnable.run();
114                    for (MessagingListener l : mListeners) {
115                        l.controllerCommandCompleted(mCommands.size() > 0);
116                    }
117                }
118            }
119            catch (Exception e) {
120                if (Config.LOGV) {
121                    Log.v(Email.LOG_TAG, "Error running command", e);
122                }
123            }
124            mBusy = false;
125        }
126    }
127
128    private void put(String description, MessagingListener listener, Runnable runnable) {
129        try {
130            Command command = new Command();
131            command.listener = listener;
132            command.runnable = runnable;
133            command.description = description;
134            mCommands.put(command);
135        }
136        catch (InterruptedException ie) {
137            throw new Error(ie);
138        }
139    }
140
141    public void addListener(MessagingListener listener) {
142        mListeners.add(listener);
143    }
144
145    public void removeListener(MessagingListener listener) {
146        mListeners.remove(listener);
147    }
148
149    /**
150     * Lists folders that are available locally and remotely. This method calls
151     * listFoldersCallback for local folders before it returns, and then for
152     * remote folders at some later point. If there are no local folders
153     * includeRemote is forced by this method. This method should be called from
154     * a Thread as it may take several seconds to list the local folders. TODO
155     * this needs to cache the remote folder list
156     *
157     * @param account
158     * @param includeRemote
159     * @param listener
160     * @throws MessagingException
161     */
162    public void listFolders(
163            final Account account,
164            boolean refreshRemote,
165            MessagingListener listener) {
166        for (MessagingListener l : mListeners) {
167            l.listFoldersStarted(account);
168        }
169        try {
170            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
171            Folder[] localFolders = localStore.getPersonalNamespaces();
172
173            if (localFolders == null || localFolders.length == 0) {
174                refreshRemote = true;
175            } else {
176                for (MessagingListener l : mListeners) {
177                    l.listFolders(account, localFolders);
178                }
179            }
180        }
181        catch (Exception e) {
182            for (MessagingListener l : mListeners) {
183                l.listFoldersFailed(account, e.getMessage());
184                return;
185            }
186        }
187        if (refreshRemote) {
188            put("listFolders", listener, new Runnable() {
189                public void run() {
190                    try {
191                        Store store = Store.getInstance(account.getStoreUri(), mApplication);
192
193                        Folder[] remoteFolders = store.getPersonalNamespaces();
194
195                        Store localStore = Store.getInstance(
196                                account.getLocalStoreUri(),
197                                mApplication);
198                        HashSet<String> remoteFolderNames = new HashSet<String>();
199                        for (int i = 0, count = remoteFolders.length; i < count; i++) {
200                            Folder localFolder = localStore.getFolder(remoteFolders[i].getName());
201                            if (!localFolder.exists()) {
202                                localFolder.create(FolderType.HOLDS_MESSAGES);
203                            }
204                            remoteFolderNames.add(remoteFolders[i].getName());
205                        }
206
207                        Folder[] localFolders = localStore.getPersonalNamespaces();
208
209                        /*
210                         * Clear out any folders that are no longer on the remote store.
211                         */
212                        for (Folder localFolder : localFolders) {
213                            String localFolderName = localFolder.getName();
214                            if (localFolderName.equalsIgnoreCase(Email.INBOX) ||
215                                    localFolderName.equals(account.getTrashFolderName()) ||
216                                    localFolderName.equals(account.getOutboxFolderName()) ||
217                                    localFolderName.equals(account.getDraftsFolderName()) ||
218                                    localFolderName.equals(account.getSentFolderName())) {
219                                continue;
220                            }
221                            if (!remoteFolderNames.contains(localFolder.getName())) {
222                                localFolder.delete(false);
223                            }
224                        }
225
226                        localFolders = localStore.getPersonalNamespaces();
227
228                        for (MessagingListener l : mListeners) {
229                            l.listFolders(account, localFolders);
230                        }
231                        for (MessagingListener l : mListeners) {
232                            l.listFoldersFinished(account);
233                        }
234                    }
235                    catch (Exception e) {
236                        for (MessagingListener l : mListeners) {
237                            l.listFoldersFailed(account, "");
238                        }
239                    }
240                }
241            });
242        } else {
243            for (MessagingListener l : mListeners) {
244                l.listFoldersFinished(account);
245            }
246        }
247    }
248
249    /**
250     * List the local message store for the given folder. This work is done
251     * synchronously.
252     *
253     * @param account
254     * @param folder
255     * @param listener
256     * @throws MessagingException
257     */
258    public void listLocalMessages(final Account account, final String folder,
259            MessagingListener listener) {
260        for (MessagingListener l : mListeners) {
261            l.listLocalMessagesStarted(account, folder);
262        }
263
264        try {
265            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
266            Folder localFolder = localStore.getFolder(folder);
267            localFolder.open(OpenMode.READ_WRITE);
268            Message[] localMessages = localFolder.getMessages(null);
269            ArrayList<Message> messages = new ArrayList<Message>();
270            for (Message message : localMessages) {
271                if (!message.isSet(Flag.DELETED)) {
272                    messages.add(message);
273                }
274            }
275            for (MessagingListener l : mListeners) {
276                l.listLocalMessages(account, folder, messages.toArray(new Message[0]));
277            }
278            for (MessagingListener l : mListeners) {
279                l.listLocalMessagesFinished(account, folder);
280            }
281        }
282        catch (Exception e) {
283            for (MessagingListener l : mListeners) {
284                l.listLocalMessagesFailed(account, folder, e.getMessage());
285            }
286        }
287    }
288
289    public void loadMoreMessages(Account account, String folder, MessagingListener listener) {
290        try {
291            LocalStore localStore = (LocalStore) Store.getInstance(
292                    account.getLocalStoreUri(),
293                    mApplication);
294            LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
295            localFolder.setVisibleLimit(localFolder.getVisibleLimit()
296                    + Email.VISIBLE_LIMIT_INCREMENT);
297            synchronizeMailbox(account, folder, listener);
298        }
299        catch (MessagingException me) {
300            throw new RuntimeException("Unable to set visible limit on folder", me);
301        }
302    }
303
304    public void resetVisibleLimits(Account[] accounts) {
305        for (Account account : accounts) {
306            try {
307                LocalStore localStore =
308                    (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication);
309                localStore.resetVisibleLimits();
310            }
311            catch (MessagingException e) {
312                Log.e(Email.LOG_TAG, "Unable to reset visible limits", e);
313            }
314        }
315    }
316
317    /**
318     * Start background synchronization of the specified folder.
319     * @param account
320     * @param folder
321     * @param numNewestMessagesToKeep Specifies the number of messages that should be
322     * considered as part of the window of available messages. This number effectively limits
323     * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages.
324     * @param listener
325     */
326    public void synchronizeMailbox(final Account account, final String folder,
327            MessagingListener listener) {
328        /*
329         * We don't ever sync the Outbox.
330         */
331        if (folder.equals(account.getOutboxFolderName())) {
332            return;
333        }
334        for (MessagingListener l : mListeners) {
335            l.synchronizeMailboxStarted(account, folder);
336        }
337        put("synchronizeMailbox", listener, new Runnable() {
338            public void run() {
339                synchronizeMailboxSyncronous(account, folder);
340            }
341        });
342    }
343
344    /**
345     * Start foreground synchronization of the specified folder. This is generally only called
346     * by synchronizeMailbox.
347     * @param account
348     * @param folder
349     * @param numNewestMessagesToKeep Specifies the number of messages that should be
350     * considered as part of the window of available messages. This number effectively limits
351     * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages.
352     * @param listener
353     *
354     * TODO Break this method up into smaller chunks.
355     */
356    public void synchronizeMailboxSyncronous(final Account account, final String folder) {
357        for (MessagingListener l : mListeners) {
358            l.synchronizeMailboxStarted(account, folder);
359        }
360        try {
361            processPendingCommandsSynchronous(account);
362
363            /*
364             * Get the message list from the local store and create an index of
365             * the uids within the list.
366             */
367            final LocalStore localStore =
368                (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication);
369            final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
370            localFolder.open(OpenMode.READ_WRITE);
371            Message[] localMessages = localFolder.getMessages(null);
372            HashMap<String, Message> localUidMap = new HashMap<String, Message>();
373            for (Message message : localMessages) {
374                localUidMap.put(message.getUid(), message);
375            }
376
377            Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
378            Folder remoteFolder = remoteStore.getFolder(folder);
379
380            /*
381             * If the folder is a "special" folder we need to see if it exists
382             * on the remote server. It if does not exist we'll try to create it. If we
383             * can't create we'll abort. This will happen on every single Pop3 folder as
384             * designed and on Imap folders during error conditions. This allows us
385             * to treat Pop3 and Imap the same in this code.
386             */
387            if (folder.equals(account.getTrashFolderName()) ||
388                    folder.equals(account.getSentFolderName()) ||
389                    folder.equals(account.getDraftsFolderName())) {
390                if (!remoteFolder.exists()) {
391                    if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
392                        for (MessagingListener l : mListeners) {
393                            l.synchronizeMailboxFinished(account, folder, 0, 0);
394                        }
395                        return;
396                    }
397                }
398            }
399
400            /*
401             * Synchronization process:
402                Open the folder
403                Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash)
404                Get the message count
405                Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages
406                    getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount)
407                See if we have each message locally, if not fetch it's flags and envelope
408                Get and update the unread count for the folder
409                Update the remote flags of any messages we have locally with an internal date
410                    newer than the remote message.
411                Get the current flags for any messages we have locally but did not just download
412                    Update local flags
413                For any message we have locally but not remotely, delete the local message to keep
414                    cache clean.
415                Download larger parts of any new messages.
416                (Optional) Download small attachments in the background.
417             */
418
419            /*
420             * Open the remote folder. This pre-loads certain metadata like message count.
421             */
422            remoteFolder.open(OpenMode.READ_WRITE);
423
424            /*
425             * Trash any remote messages that are marked as trashed locally.
426             */
427
428            /*
429             * Get the remote message count.
430             */
431            int remoteMessageCount = remoteFolder.getMessageCount();
432
433            int visibleLimit = localFolder.getVisibleLimit();
434
435            Message[] remoteMessages = new Message[0];
436            final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
437            HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
438
439            if (remoteMessageCount > 0) {
440                /*
441                 * Message numbers start at 1.
442                 */
443                int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
444                int remoteEnd = remoteMessageCount;
445                remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
446                for (Message message : remoteMessages) {
447                    remoteUidMap.put(message.getUid(), message);
448                }
449
450                /*
451                 * Get a list of the messages that are in the remote list but not on the
452                 * local store, or messages that are in the local store but failed to download
453                 * on the last sync. These are the new messages that we will download.
454                 */
455                for (Message message : remoteMessages) {
456                    Message localMessage = localUidMap.get(message.getUid());
457                    if (localMessage == null ||
458                            (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) &&
459                            !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) {
460                        unsyncedMessages.add(message);
461                    }
462                }
463            }
464
465            /*
466             * A list of messages that were downloaded and which did not have the Seen flag set.
467             * This will serve to indicate the true "new" message count that will be reported to
468             * the user via notification.
469             */
470            final ArrayList<Message> newMessages = new ArrayList<Message>();
471
472            /*
473             * Fetch the flags and envelope only of the new messages. This is intended to get us
474s             * critical data as fast as possible, and then we'll fill in the details.
475             */
476            if (unsyncedMessages.size() > 0) {
477
478                /*
479                 * Reverse the order of the messages. Depending on the server this may get us
480                 * fetch results for newest to oldest. If not, no harm done.
481                 */
482                Collections.reverse(unsyncedMessages);
483
484                FetchProfile fp = new FetchProfile();
485                fp.add(FetchProfile.Item.FLAGS);
486                fp.add(FetchProfile.Item.ENVELOPE);
487                remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
488                        new MessageRetrievalListener() {
489                            public void messageFinished(Message message, int number, int ofTotal) {
490                                try {
491                                    // Store the new message locally
492                                    localFolder.appendMessages(new Message[] {
493                                        message
494                                    });
495
496                                    // And include it in the view
497                                    if (message.getSubject() != null &&
498                                            message.getFrom() != null) {
499                                        /*
500                                         * We check to make sure that we got something worth
501                                         * showing (subject and from) because some protocols
502                                         * (POP) may not be able to give us headers for
503                                         * ENVELOPE, only size.
504                                         */
505                                        for (MessagingListener l : mListeners) {
506                                            l.synchronizeMailboxNewMessage(account, folder,
507                                                    localFolder.getMessage(message.getUid()));
508                                        }
509                                    }
510
511                                    if (!message.isSet(Flag.SEEN)) {
512                                        newMessages.add(message);
513                                    }
514                                }
515                                catch (Exception e) {
516                                    Log.e(Email.LOG_TAG,
517                                            "Error while storing downloaded message.",
518                                            e);
519                                }
520                            }
521
522                            public void messageStarted(String uid, int number, int ofTotal) {
523                            }
524                        });
525            }
526
527            /*
528             * Refresh the flags for any messages in the local store that we didn't just
529             * download.
530             */
531            FetchProfile fp = new FetchProfile();
532            fp.add(FetchProfile.Item.FLAGS);
533            remoteFolder.fetch(remoteMessages, fp, null);
534            for (Message remoteMessage : remoteMessages) {
535                Message localMessage = localFolder.getMessage(remoteMessage.getUid());
536                if (localMessage == null) {
537                    continue;
538                }
539                if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN)) {
540                    localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN));
541                    for (MessagingListener l : mListeners) {
542                        l.synchronizeMailboxNewMessage(account, folder, localMessage);
543                    }
544                }
545            }
546
547            /*
548             * Get and store the unread message count.
549             */
550            int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount();
551            if (remoteUnreadMessageCount == -1) {
552                localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount()
553                        + newMessages.size());
554            }
555            else {
556                localFolder.setUnreadMessageCount(remoteUnreadMessageCount);
557            }
558
559            /*
560             * Remove any messages that are in the local store but no longer on the remote store.
561             */
562            for (Message localMessage : localMessages) {
563                if (remoteUidMap.get(localMessage.getUid()) == null) {
564                    localMessage.setFlag(Flag.X_DESTROYED, true);
565                    for (MessagingListener l : mListeners) {
566                        l.synchronizeMailboxRemovedMessage(account, folder, localMessage);
567                    }
568                }
569            }
570
571            /*
572             * Now we download the actual content of messages.
573             */
574            ArrayList<Message> largeMessages = new ArrayList<Message>();
575            ArrayList<Message> smallMessages = new ArrayList<Message>();
576            for (Message message : unsyncedMessages) {
577                /*
578                 * Sort the messages into two buckets, small and large. Small messages will be
579                 * downloaded fully and large messages will be downloaded in parts. By sorting
580                 * into two buckets we can pipeline the commands for each set of messages
581                 * into a single command to the server saving lots of round trips.
582                 */
583                if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
584                    largeMessages.add(message);
585                } else {
586                    smallMessages.add(message);
587                }
588            }
589            /*
590             * Grab the content of the small messages first. This is going to
591             * be very fast and at very worst will be a single up of a few bytes and a single
592             * download of 625k.
593             */
594            fp = new FetchProfile();
595            fp.add(FetchProfile.Item.BODY);
596            remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]),
597                    fp, new MessageRetrievalListener() {
598                public void messageFinished(Message message, int number, int ofTotal) {
599                    try {
600                        // Store the updated message locally
601                        localFolder.appendMessages(new Message[] {
602                            message
603                        });
604
605                        Message localMessage = localFolder.getMessage(message.getUid());
606
607                        // Set a flag indicating this message has now be fully downloaded
608                        localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
609
610                        // Update the listener with what we've found
611                        for (MessagingListener l : mListeners) {
612                            l.synchronizeMailboxNewMessage(
613                                    account,
614                                    folder,
615                                    localMessage);
616                        }
617                    }
618                    catch (MessagingException me) {
619
620                    }
621                }
622
623                public void messageStarted(String uid, int number, int ofTotal) {
624                }
625            });
626
627            /*
628             * Now do the large messages that require more round trips.
629             */
630            fp.clear();
631            fp.add(FetchProfile.Item.STRUCTURE);
632            remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]),
633                    fp, null);
634            for (Message message : largeMessages) {
635                if (message.getBody() == null) {
636                    /*
637                     * The provider was unable to get the structure of the message, so
638                     * we'll download a reasonable portion of the messge and mark it as
639                     * incomplete so the entire thing can be downloaded later if the user
640                     * wishes to download it.
641                     */
642                    fp.clear();
643                    fp.add(FetchProfile.Item.BODY_SANE);
644                    /*
645                     *  TODO a good optimization here would be to make sure that all Stores set
646                     *  the proper size after this fetch and compare the before and after size. If
647                     *  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
648                     */
649
650                    remoteFolder.fetch(new Message[] { message }, fp, null);
651                    // Store the updated message locally
652                    localFolder.appendMessages(new Message[] {
653                        message
654                    });
655
656                    Message localMessage = localFolder.getMessage(message.getUid());
657
658                    // Set a flag indicating that the message has been partially downloaded and
659                    // is ready for view.
660                    localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
661                } else {
662                    /*
663                     * We have a structure to deal with, from which
664                     * we can pull down the parts we want to actually store.
665                     * Build a list of parts we are interested in. Text parts will be downloaded
666                     * right now, attachments will be left for later.
667                     */
668
669                    ArrayList<Part> viewables = new ArrayList<Part>();
670                    ArrayList<Part> attachments = new ArrayList<Part>();
671                    MimeUtility.collectParts(message, viewables, attachments);
672
673                    /*
674                     * Now download the parts we're interested in storing.
675                     */
676                    for (Part part : viewables) {
677                        fp.clear();
678                        fp.add(part);
679                        // TODO what happens if the network connection dies? We've got partial
680                        // messages with incorrect status stored.
681                        remoteFolder.fetch(new Message[] { message }, fp, null);
682                    }
683                    // Store the updated message locally
684                    localFolder.appendMessages(new Message[] {
685                        message
686                    });
687
688                    Message localMessage = localFolder.getMessage(message.getUid());
689
690                    // Set a flag indicating this message has been fully downloaded and can be
691                    // viewed.
692                    localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
693                }
694
695                // Update the listener with what we've found
696                for (MessagingListener l : mListeners) {
697                    l.synchronizeMailboxNewMessage(
698                            account,
699                            folder,
700                            localFolder.getMessage(message.getUid()));
701                }
702            }
703
704
705            /*
706             * Notify listeners that we're finally done.
707             */
708            for (MessagingListener l : mListeners) {
709                l.synchronizeMailboxFinished(
710                        account,
711                        folder,
712                        remoteFolder.getMessageCount(), newMessages.size());
713            }
714
715            remoteFolder.close(false);
716            localFolder.close(false);
717        }
718        catch (Exception e) {
719            if (Config.LOGV) {
720                Log.v(Email.LOG_TAG, "synchronizeMailbox", e);
721            }
722            for (MessagingListener l : mListeners) {
723                l.synchronizeMailboxFailed(
724                        account,
725                        folder,
726                        e.getMessage());
727            }
728        }
729    }
730
731    private void queuePendingCommand(Account account, PendingCommand command) {
732        try {
733            LocalStore localStore = (LocalStore) Store.getInstance(
734                    account.getLocalStoreUri(),
735                    mApplication);
736            localStore.addPendingCommand(command);
737        }
738        catch (Exception e) {
739            throw new RuntimeException("Unable to enqueue pending command", e);
740        }
741    }
742
743    private void processPendingCommands(final Account account) {
744        put("processPendingCommands", null, new Runnable() {
745            public void run() {
746                try {
747                    processPendingCommandsSynchronous(account);
748                }
749                catch (MessagingException me) {
750                    if (Config.LOGV) {
751                        Log.v(Email.LOG_TAG, "processPendingCommands", me);
752                    }
753                    /*
754                     * Ignore any exceptions from the commands. Commands will be processed
755                     * on the next round.
756                     */
757                }
758            }
759        });
760    }
761
762    private void processPendingCommandsSynchronous(Account account) throws MessagingException {
763        LocalStore localStore = (LocalStore) Store.getInstance(
764                account.getLocalStoreUri(),
765                mApplication);
766        ArrayList<PendingCommand> commands = localStore.getPendingCommands();
767        for (PendingCommand command : commands) {
768            /*
769             * We specifically do not catch any exceptions here. If a command fails it is
770             * most likely due to a server or IO error and it must be retried before any
771             * other command processes. This maintains the order of the commands.
772             */
773            if (PENDING_COMMAND_APPEND.equals(command.command)) {
774                processPendingAppend(command, account);
775            }
776            else if (PENDING_COMMAND_MARK_READ.equals(command.command)) {
777                processPendingMarkRead(command, account);
778            }
779            else if (PENDING_COMMAND_TRASH.equals(command.command)) {
780                processPendingTrash(command, account);
781            }
782            localStore.removePendingCommand(command);
783        }
784    }
785
786    /**
787     * Process a pending append message command. This command uploads a local message to the
788     * server, first checking to be sure that the server message is not newer than
789     * the local message. Once the local message is successfully processed it is deleted so
790     * that the server message will be synchronized down without an additional copy being
791     * created.
792     * TODO update the local message UID instead of deleteing it
793     *
794     * @param command arguments = (String folder, String uid)
795     * @param account
796     * @throws MessagingException
797     */
798    private void processPendingAppend(PendingCommand command, Account account)
799            throws MessagingException {
800        String folder = command.arguments[0];
801        String uid = command.arguments[1];
802
803        LocalStore localStore = (LocalStore) Store.getInstance(
804                account.getLocalStoreUri(),
805                mApplication);
806        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
807        LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid);
808
809        if (localMessage == null) {
810            return;
811        }
812
813        Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
814        Folder remoteFolder = remoteStore.getFolder(folder);
815        if (!remoteFolder.exists()) {
816            if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
817                return;
818            }
819        }
820        remoteFolder.open(OpenMode.READ_WRITE);
821        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
822            return;
823        }
824
825        Message remoteMessage = null;
826        if (!localMessage.getUid().startsWith("Local")
827                && !localMessage.getUid().contains("-")) {
828            remoteMessage = remoteFolder.getMessage(localMessage.getUid());
829        }
830
831        if (remoteMessage == null) {
832            /*
833             * If the message does not exist remotely we just upload it and then
834             * update our local copy with the new uid.
835             */
836            FetchProfile fp = new FetchProfile();
837            fp.add(FetchProfile.Item.BODY);
838            localFolder.fetch(new Message[] { localMessage }, fp, null);
839            String oldUid = localMessage.getUid();
840            remoteFolder.appendMessages(new Message[] { localMessage });
841            localFolder.changeUid(localMessage);
842            for (MessagingListener l : mListeners) {
843                l.messageUidChanged(account, folder, oldUid, localMessage.getUid());
844            }
845        }
846        else {
847            /*
848             * If the remote message exists we need to determine which copy to keep.
849             */
850            /*
851             * See if the remote message is newer than ours.
852             */
853            FetchProfile fp = new FetchProfile();
854            fp.add(FetchProfile.Item.ENVELOPE);
855            remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
856            Date localDate = localMessage.getInternalDate();
857            Date remoteDate = remoteMessage.getInternalDate();
858            if (remoteDate.compareTo(localDate) > 0) {
859                /*
860                 * If the remote message is newer than ours we'll just
861                 * delete ours and move on. A sync will get the server message
862                 * if we need to be able to see it.
863                 */
864                localMessage.setFlag(Flag.DELETED, true);
865            }
866            else {
867                /*
868                 * Otherwise we'll upload our message and then delete the remote message.
869                 */
870                fp.clear();
871                fp = new FetchProfile();
872                fp.add(FetchProfile.Item.BODY);
873                localFolder.fetch(new Message[] { localMessage }, fp, null);
874                String oldUid = localMessage.getUid();
875                remoteFolder.appendMessages(new Message[] { localMessage });
876                localFolder.changeUid(localMessage);
877                for (MessagingListener l : mListeners) {
878                    l.messageUidChanged(account, folder, oldUid, localMessage.getUid());
879                }
880                remoteMessage.setFlag(Flag.DELETED, true);
881            }
882        }
883    }
884
885    /**
886     * Process a pending trash message command.
887     *
888     * @param command arguments = (String folder, String uid)
889     * @param account
890     * @throws MessagingException
891     */
892    private void processPendingTrash(PendingCommand command, Account account)
893            throws MessagingException {
894        String folder = command.arguments[0];
895        String uid = command.arguments[1];
896
897        Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
898        Folder remoteFolder = remoteStore.getFolder(folder);
899        if (!remoteFolder.exists()) {
900            return;
901        }
902        remoteFolder.open(OpenMode.READ_WRITE);
903        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
904            return;
905        }
906
907        Message remoteMessage = null;
908        if (!uid.startsWith("Local")
909                && !uid.contains("-")) {
910            remoteMessage = remoteFolder.getMessage(uid);
911        }
912        if (remoteMessage == null) {
913            return;
914        }
915
916        Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName());
917        /*
918         * Attempt to copy the remote message to the remote trash folder.
919         */
920        if (!remoteTrashFolder.exists()) {
921            /*
922             * If the remote trash folder doesn't exist we try to create it.
923             */
924            remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
925        }
926
927        if (remoteTrashFolder.exists()) {
928            remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder);
929        }
930
931        remoteMessage.setFlag(Flag.DELETED, true);
932        remoteFolder.expunge();
933    }
934
935    /**
936     * Processes a pending mark read or unread command.
937     *
938     * @param command arguments = (String folder, String uid, boolean read)
939     * @param account
940     */
941    private void processPendingMarkRead(PendingCommand command, Account account)
942            throws MessagingException {
943        String folder = command.arguments[0];
944        String uid = command.arguments[1];
945        boolean read = Boolean.parseBoolean(command.arguments[2]);
946
947        Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
948        Folder remoteFolder = remoteStore.getFolder(folder);
949        if (!remoteFolder.exists()) {
950            return;
951        }
952        remoteFolder.open(OpenMode.READ_WRITE);
953        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
954            return;
955        }
956        Message remoteMessage = null;
957        if (!uid.startsWith("Local")
958                && !uid.contains("-")) {
959            remoteMessage = remoteFolder.getMessage(uid);
960        }
961        if (remoteMessage == null) {
962            return;
963        }
964        remoteMessage.setFlag(Flag.SEEN, read);
965    }
966
967    /**
968     * Mark the message with the given account, folder and uid either Seen or not Seen.
969     * @param account
970     * @param folder
971     * @param uid
972     * @param seen
973     */
974    public void markMessageRead(
975            final Account account,
976            final String folder,
977            final String uid,
978            final boolean seen) {
979        try {
980            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
981            Folder localFolder = localStore.getFolder(folder);
982            localFolder.open(OpenMode.READ_WRITE);
983
984            Message message = localFolder.getMessage(uid);
985            message.setFlag(Flag.SEEN, seen);
986            PendingCommand command = new PendingCommand();
987            command.command = PENDING_COMMAND_MARK_READ;
988            command.arguments = new String[] { folder, uid, Boolean.toString(seen) };
989            queuePendingCommand(account, command);
990            processPendingCommands(account);
991        }
992        catch (MessagingException me) {
993            throw new RuntimeException(me);
994        }
995    }
996
997    private void loadMessageForViewRemote(final Account account, final String folder,
998            final String uid, MessagingListener listener) {
999        put("loadMessageForViewRemote", listener, new Runnable() {
1000            public void run() {
1001                try {
1002                    Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1003                    LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1004                    localFolder.open(OpenMode.READ_WRITE);
1005
1006                    Message message = localFolder.getMessage(uid);
1007
1008                    if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
1009                        /*
1010                         * If the message has been synchronized since we were called we'll
1011                         * just hand it back cause it's ready to go.
1012                         */
1013                        FetchProfile fp = new FetchProfile();
1014                        fp.add(FetchProfile.Item.ENVELOPE);
1015                        fp.add(FetchProfile.Item.BODY);
1016                        localFolder.fetch(new Message[] { message }, fp, null);
1017
1018                        for (MessagingListener l : mListeners) {
1019                            l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1020                        }
1021                        for (MessagingListener l : mListeners) {
1022                            l.loadMessageForViewFinished(account, folder, uid, message);
1023                        }
1024                        localFolder.close(false);
1025                        return;
1026                    }
1027
1028                    /*
1029                     * At this point the message is not available, so we need to download it
1030                     * fully if possible.
1031                     */
1032
1033                    Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
1034                    Folder remoteFolder = remoteStore.getFolder(folder);
1035                    remoteFolder.open(OpenMode.READ_WRITE);
1036
1037                    // Get the remote message and fully download it
1038                    Message remoteMessage = remoteFolder.getMessage(uid);
1039                    FetchProfile fp = new FetchProfile();
1040                    fp.add(FetchProfile.Item.BODY);
1041                    remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1042
1043                    // Store the message locally and load the stored message into memory
1044                    localFolder.appendMessages(new Message[] { remoteMessage });
1045                    message = localFolder.getMessage(uid);
1046                    localFolder.fetch(new Message[] { message }, fp, null);
1047
1048                    // This is a view message request, so mark it read
1049                    if (!message.isSet(Flag.SEEN)) {
1050                        markMessageRead(account, folder, uid, true);
1051                    }
1052
1053                    // Mark that this message is now fully synched
1054                    message.setFlag(Flag.X_DOWNLOADED_FULL, true);
1055
1056                    for (MessagingListener l : mListeners) {
1057                        l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1058                    }
1059                    for (MessagingListener l : mListeners) {
1060                        l.loadMessageForViewFinished(account, folder, uid, message);
1061                    }
1062                    remoteFolder.close(false);
1063                    localFolder.close(false);
1064                }
1065                catch (Exception e) {
1066                    for (MessagingListener l : mListeners) {
1067                        l.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1068                    }
1069                }
1070            }
1071        });
1072    }
1073
1074    public void loadMessageForView(final Account account, final String folder, final String uid,
1075            MessagingListener listener) {
1076        for (MessagingListener l : mListeners) {
1077            l.loadMessageForViewStarted(account, folder, uid);
1078        }
1079        try {
1080            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1081            LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1082            localFolder.open(OpenMode.READ_WRITE);
1083
1084            Message message = localFolder.getMessage(uid);
1085
1086            for (MessagingListener l : mListeners) {
1087                l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
1088            }
1089
1090            if (!message.isSet(Flag.X_DOWNLOADED_FULL)) {
1091                loadMessageForViewRemote(account, folder, uid, listener);
1092                localFolder.close(false);
1093                return;
1094            }
1095
1096            if (!message.isSet(Flag.SEEN)) {
1097                markMessageRead(account, folder, uid, true);
1098            }
1099
1100            FetchProfile fp = new FetchProfile();
1101            fp.add(FetchProfile.Item.ENVELOPE);
1102            fp.add(FetchProfile.Item.BODY);
1103            localFolder.fetch(new Message[] {
1104                message
1105            }, fp, null);
1106
1107            for (MessagingListener l : mListeners) {
1108                l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1109            }
1110
1111            for (MessagingListener l : mListeners) {
1112                l.loadMessageForViewFinished(account, folder, uid, message);
1113            }
1114            localFolder.close(false);
1115        }
1116        catch (Exception e) {
1117            for (MessagingListener l : mListeners) {
1118                l.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1119            }
1120        }
1121    }
1122
1123    /**
1124     * Attempts to load the attachment specified by part from the given account and message.
1125     * @param account
1126     * @param message
1127     * @param part
1128     * @param listener
1129     */
1130    public void loadAttachment(
1131            final Account account,
1132            final Message message,
1133            final Part part,
1134            final Object tag,
1135            MessagingListener listener) {
1136        /*
1137         * Check if the attachment has already been downloaded. If it has there's no reason to
1138         * download it, so we just tell the listener that it's ready to go.
1139         */
1140        try {
1141            if (part.getBody() != null) {
1142                for (MessagingListener l : mListeners) {
1143                    l.loadAttachmentStarted(account, message, part, tag, false);
1144                }
1145
1146                for (MessagingListener l : mListeners) {
1147                    l.loadAttachmentFinished(account, message, part, tag);
1148                }
1149                return;
1150            }
1151        }
1152        catch (MessagingException me) {
1153            /*
1154             * If the header isn't there the attachment isn't downloaded yet, so just continue
1155             * on.
1156             */
1157        }
1158
1159        for (MessagingListener l : mListeners) {
1160            l.loadAttachmentStarted(account, message, part, tag, true);
1161        }
1162
1163        put("loadAttachment", listener, new Runnable() {
1164            public void run() {
1165                try {
1166                    LocalStore localStore =
1167                        (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication);
1168                    /*
1169                     * We clear out any attachments already cached in the entire store and then
1170                     * we update the passed in message to reflect that there are no cached
1171                     * attachments. This is in support of limiting the account to having one
1172                     * attachment downloaded at a time.
1173                     */
1174                    localStore.pruneCachedAttachments();
1175                    ArrayList<Part> viewables = new ArrayList<Part>();
1176                    ArrayList<Part> attachments = new ArrayList<Part>();
1177                    MimeUtility.collectParts(message, viewables, attachments);
1178                    for (Part attachment : attachments) {
1179                        attachment.setBody(null);
1180                    }
1181                    Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication);
1182                    LocalFolder localFolder =
1183                        (LocalFolder) localStore.getFolder(message.getFolder().getName());
1184                    Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName());
1185                    remoteFolder.open(OpenMode.READ_WRITE);
1186
1187                    FetchProfile fp = new FetchProfile();
1188                    fp.add(part);
1189                    remoteFolder.fetch(new Message[] { message }, fp, null);
1190                    localFolder.updateMessage((LocalMessage)message);
1191                    localFolder.close(false);
1192                    for (MessagingListener l : mListeners) {
1193                        l.loadAttachmentFinished(account, message, part, tag);
1194                    }
1195                }
1196                catch (MessagingException me) {
1197                    if (Config.LOGV) {
1198                        Log.v(Email.LOG_TAG, "", me);
1199                    }
1200                    for (MessagingListener l : mListeners) {
1201                        l.loadAttachmentFailed(account, message, part, tag, me.getMessage());
1202                    }
1203                }
1204            }
1205        });
1206    }
1207
1208    /**
1209     * Stores the given message in the Outbox and starts a sendPendingMessages command to
1210     * attempt to send the message.
1211     * @param account
1212     * @param message
1213     * @param listener
1214     */
1215    public void sendMessage(final Account account,
1216            final Message message,
1217            MessagingListener listener) {
1218        try {
1219            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1220            LocalFolder localFolder =
1221                (LocalFolder) localStore.getFolder(account.getOutboxFolderName());
1222            localFolder.open(OpenMode.READ_WRITE);
1223            localFolder.appendMessages(new Message[] {
1224                message
1225            });
1226            Message localMessage = localFolder.getMessage(message.getUid());
1227            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1228            localFolder.close(false);
1229            sendPendingMessages(account, null);
1230        }
1231        catch (Exception e) {
1232            for (MessagingListener l : mListeners) {
1233                // TODO general failed
1234            }
1235        }
1236    }
1237
1238    /**
1239     * Attempt to send any messages that are sitting in the Outbox.
1240     * @param account
1241     * @param listener
1242     */
1243    public void sendPendingMessages(final Account account,
1244            MessagingListener listener) {
1245        put("sendPendingMessages", listener, new Runnable() {
1246            public void run() {
1247                sendPendingMessagesSynchronous(account);
1248            }
1249        });
1250    }
1251
1252    /**
1253     * Attempt to send any messages that are sitting in the Outbox.
1254     * @param account
1255     * @param listener
1256     */
1257    public void sendPendingMessagesSynchronous(final Account account) {
1258        try {
1259            Store localStore = Store.getInstance(
1260                    account.getLocalStoreUri(),
1261                    mApplication);
1262            Folder localFolder = localStore.getFolder(
1263                    account.getOutboxFolderName());
1264            if (!localFolder.exists()) {
1265                return;
1266            }
1267            localFolder.open(OpenMode.READ_WRITE);
1268
1269            Message[] localMessages = localFolder.getMessages(null);
1270
1271            /*
1272             * The profile we will use to pull all of the content
1273             * for a given local message into memory for sending.
1274             */
1275            FetchProfile fp = new FetchProfile();
1276            fp.add(FetchProfile.Item.ENVELOPE);
1277            fp.add(FetchProfile.Item.BODY);
1278
1279            LocalFolder localSentFolder =
1280                (LocalFolder) localStore.getFolder(
1281                        account.getSentFolderName());
1282
1283            Transport transport = Transport.getInstance(account.getTransportUri());
1284            for (Message message : localMessages) {
1285                try {
1286                    localFolder.fetch(new Message[] { message }, fp, null);
1287                    try {
1288                        message.setFlag(Flag.X_SEND_IN_PROGRESS, true);
1289                        transport.sendMessage(message);
1290                        message.setFlag(Flag.X_SEND_IN_PROGRESS, false);
1291                        localFolder.copyMessages(
1292                                new Message[] { message },
1293                                localSentFolder);
1294
1295                        PendingCommand command = new PendingCommand();
1296                        command.command = PENDING_COMMAND_APPEND;
1297                        command.arguments =
1298                            new String[] {
1299                                localSentFolder.getName(),
1300                                message.getUid() };
1301                        queuePendingCommand(account, command);
1302                        processPendingCommands(account);
1303                        message.setFlag(Flag.X_DESTROYED, true);
1304                    }
1305                    catch (Exception e) {
1306                        message.setFlag(Flag.X_SEND_FAILED, true);
1307                    }
1308                }
1309                catch (Exception e) {
1310                    /*
1311                     * We ignore this exception because a future refresh will retry this
1312                     * message.
1313                     */
1314                }
1315            }
1316            localFolder.expunge();
1317            if (localFolder.getMessageCount() == 0) {
1318                localFolder.delete(false);
1319            }
1320            for (MessagingListener l : mListeners) {
1321                l.sendPendingMessagesCompleted(account);
1322            }
1323        }
1324        catch (Exception e) {
1325            for (MessagingListener l : mListeners) {
1326                // TODO general failed
1327            }
1328        }
1329    }
1330
1331    /**
1332     * We do the local portion of this synchronously because other activities may have to make
1333     * updates based on what happens here
1334     * @param account
1335     * @param folder
1336     * @param message
1337     * @param listener
1338     */
1339    public void deleteMessage(final Account account, final String folder, final Message message,
1340            MessagingListener listener) {
1341        if (folder.equals(account.getTrashFolderName())) {
1342            return;
1343        }
1344        try {
1345            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1346            Folder localFolder = localStore.getFolder(folder);
1347            Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName());
1348
1349            localFolder.copyMessages(new Message[] { message }, localTrashFolder);
1350            message.setFlag(Flag.DELETED, true);
1351
1352            if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) {
1353                PendingCommand command = new PendingCommand();
1354                command.command = PENDING_COMMAND_TRASH;
1355                command.arguments = new String[] { folder, message.getUid() };
1356                queuePendingCommand(account, command);
1357                processPendingCommands(account);
1358            }
1359        }
1360        catch (MessagingException me) {
1361            throw new RuntimeException("Error deleting message from local store.", me);
1362        }
1363    }
1364
1365    public void emptyTrash(final Account account, MessagingListener listener) {
1366        put("emptyTrash", listener, new Runnable() {
1367            public void run() {
1368                // TODO IMAP
1369                try {
1370                    Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1371                    Folder localFolder = localStore.getFolder(account.getTrashFolderName());
1372                    localFolder.open(OpenMode.READ_WRITE);
1373                    Message[] messages = localFolder.getMessages(null);
1374                    localFolder.setFlags(messages, new Flag[] {
1375                        Flag.DELETED
1376                    }, true);
1377                    localFolder.close(true);
1378                    for (MessagingListener l : mListeners) {
1379                        l.emptyTrashCompleted(account);
1380                    }
1381                }
1382                catch (Exception e) {
1383                    // TODO
1384                    if (Config.LOGV) {
1385                        Log.v(Email.LOG_TAG, "emptyTrash");
1386                    }
1387                }
1388            }
1389        });
1390    }
1391
1392    /**
1393     * Checks mail for one or multiple accounts. If account is null all accounts
1394     * are checked.
1395     *
1396     * @param context
1397     * @param account
1398     * @param listener
1399     */
1400    public void checkMail(final Context context, final Account account,
1401            final MessagingListener listener) {
1402        for (MessagingListener l : mListeners) {
1403            l.checkMailStarted(context, account);
1404        }
1405        put("checkMail", listener, new Runnable() {
1406            public void run() {
1407                Account[] accounts;
1408                if (account != null) {
1409                    accounts = new Account[] {
1410                        account
1411                    };
1412                } else {
1413                    accounts = Preferences.getPreferences(context).getAccounts();
1414                }
1415                for (Account account : accounts) {
1416                    sendPendingMessagesSynchronous(account);
1417                    synchronizeMailboxSyncronous(account, Email.INBOX);
1418                }
1419                for (MessagingListener l : mListeners) {
1420                    l.checkMailFinished(context, account);
1421                }
1422            }
1423        });
1424    }
1425
1426    public void saveDraft(final Account account, final Message message) {
1427        try {
1428            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
1429            LocalFolder localFolder =
1430                (LocalFolder) localStore.getFolder(account.getDraftsFolderName());
1431            localFolder.open(OpenMode.READ_WRITE);
1432            localFolder.appendMessages(new Message[] {
1433                message
1434            });
1435            Message localMessage = localFolder.getMessage(message.getUid());
1436            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1437
1438            PendingCommand command = new PendingCommand();
1439            command.command = PENDING_COMMAND_APPEND;
1440            command.arguments = new String[] {
1441                    localFolder.getName(),
1442                    localMessage.getUid() };
1443            queuePendingCommand(account, command);
1444            processPendingCommands(account);
1445        }
1446        catch (MessagingException e) {
1447            Log.e(Email.LOG_TAG, "Unable to save message as draft.", e);
1448        }
1449    }
1450
1451    class Command {
1452        public Runnable runnable;
1453
1454        public MessagingListener listener;
1455
1456        public String description;
1457    }
1458}
1459