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