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