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