MessagingController.java revision 1df530294d37573e1b1a97bc1c01a43af3127224
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
248                        // Signal the remote store so it can used folder-based callbacks
249                        for (Folder remoteFolder : remoteFolders) {
250                            Folder localFolder = localStore.getFolder(remoteFolder.getName());
251                            remoteFolder.localFolderSetupComplete(localFolder);
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        final 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                    new Folder.MessageUpdateCallbacks() {
1003                        @Override
1004                        public void onMessageUidChange(Message message, String newUid)
1005                                throws MessagingException {
1006                            // update the UID in the original folder to match
1007                            // because some stores will have to change it when copying
1008                            // to remoteTrashFolder
1009                            message.setUid(newUid);
1010                            remoteFolder.updateMessages(new Message[] { message });
1011                        }
1012                    }
1013            );
1014        }
1015
1016        remoteMessage.setFlag(Flag.DELETED, true);
1017        remoteFolder.expunge();
1018    }
1019
1020    /**
1021     * Processes a pending mark read or unread command.
1022     *
1023     * @param command arguments = (String folder, String uid, boolean read)
1024     * @param account
1025     */
1026    private void processPendingMarkRead(PendingCommand command, Account account)
1027            throws MessagingException {
1028        String folder = command.arguments[0];
1029        String uid = command.arguments[1];
1030        boolean read = Boolean.parseBoolean(command.arguments[2]);
1031
1032        LocalStore localStore = (LocalStore) Store.getInstance(
1033                account.getLocalStoreUri(), mApplication, null);
1034        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1035
1036        Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication,
1037                account.getStoreCallbacks());
1038        Folder remoteFolder = remoteStore.getFolder(folder);
1039        if (!remoteFolder.exists()) {
1040            return;
1041        }
1042        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1043        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1044            return;
1045        }
1046        Message remoteMessage = null;
1047        if (!uid.startsWith("Local")
1048                && !uid.contains("-")) {
1049            remoteMessage = remoteFolder.getMessage(uid);
1050        }
1051        if (remoteMessage == null) {
1052            return;
1053        }
1054        remoteMessage.setFlag(Flag.SEEN, read);
1055    }
1056
1057    /**
1058     * Mark the message with the given account, folder and uid either Seen or not Seen.
1059     * @param account
1060     * @param folder
1061     * @param uid
1062     * @param seen
1063     */
1064    public void markMessageRead(
1065            final Account account,
1066            final String folder,
1067            final String uid,
1068            final boolean seen) {
1069        try {
1070            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1071            Folder localFolder = localStore.getFolder(folder);
1072            localFolder.open(OpenMode.READ_WRITE, null);
1073
1074            Message message = localFolder.getMessage(uid);
1075            message.setFlag(Flag.SEEN, seen);
1076            PendingCommand command = new PendingCommand();
1077            command.command = PENDING_COMMAND_MARK_READ;
1078            command.arguments = new String[] { folder, uid, Boolean.toString(seen) };
1079            queuePendingCommand(account, command);
1080            processPendingCommands(account);
1081        }
1082        catch (MessagingException me) {
1083            throw new RuntimeException(me);
1084        }
1085    }
1086
1087    private void loadMessageForViewRemote(final Account account, final String folder,
1088            final String uid, MessagingListener listener) {
1089        put("loadMessageForViewRemote", listener, new Runnable() {
1090            public void run() {
1091                try {
1092                    Store localStore = Store.getInstance(
1093                            account.getLocalStoreUri(), mApplication, null);
1094                    LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1095                    localFolder.open(OpenMode.READ_WRITE, null);
1096
1097                    Message message = localFolder.getMessage(uid);
1098
1099                    if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
1100                        /*
1101                         * If the message has been synchronized since we were called we'll
1102                         * just hand it back cause it's ready to go.
1103                         */
1104                        FetchProfile fp = new FetchProfile();
1105                        fp.add(FetchProfile.Item.ENVELOPE);
1106                        fp.add(FetchProfile.Item.BODY);
1107                        localFolder.fetch(new Message[] { message }, fp, null);
1108
1109                        for (MessagingListener l : mListeners) {
1110                            l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1111                        }
1112                        for (MessagingListener l : mListeners) {
1113                            l.loadMessageForViewFinished(account, folder, uid, message);
1114                        }
1115                        localFolder.close(false);
1116                        return;
1117                    }
1118
1119                    /*
1120                     * At this point the message is not available, so we need to download it
1121                     * fully if possible.
1122                     */
1123
1124                    Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication,
1125                            account.getStoreCallbacks());
1126                    Folder remoteFolder = remoteStore.getFolder(folder);
1127                    remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1128
1129                    // Get the remote message and fully download it
1130                    Message remoteMessage = remoteFolder.getMessage(uid);
1131                    FetchProfile fp = new FetchProfile();
1132                    fp.add(FetchProfile.Item.BODY);
1133                    remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1134
1135                    // Store the message locally and load the stored message into memory
1136                    localFolder.appendMessages(new Message[] { remoteMessage });
1137                    message = localFolder.getMessage(uid);
1138                    localFolder.fetch(new Message[] { message }, fp, null);
1139
1140                    // This is a view message request, so mark it read
1141                    if (!message.isSet(Flag.SEEN)) {
1142                        markMessageRead(account, folder, uid, true);
1143                    }
1144
1145                    // Mark that this message is now fully synched
1146                    message.setFlag(Flag.X_DOWNLOADED_FULL, true);
1147
1148                    for (MessagingListener l : mListeners) {
1149                        l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1150                    }
1151                    for (MessagingListener l : mListeners) {
1152                        l.loadMessageForViewFinished(account, folder, uid, message);
1153                    }
1154                    remoteFolder.close(false);
1155                    localFolder.close(false);
1156                }
1157                catch (Exception e) {
1158                    for (MessagingListener l : mListeners) {
1159                        l.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1160                    }
1161                }
1162            }
1163        });
1164    }
1165
1166    public void loadMessageForView(final Account account, final String folder, final String uid,
1167            MessagingListener listener) {
1168        for (MessagingListener l : mListeners) {
1169            l.loadMessageForViewStarted(account, folder, uid);
1170        }
1171        try {
1172            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1173            LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1174            localFolder.open(OpenMode.READ_WRITE, null);
1175
1176            Message message = localFolder.getMessage(uid);
1177
1178            for (MessagingListener l : mListeners) {
1179                l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
1180            }
1181
1182            if (!message.isSet(Flag.X_DOWNLOADED_FULL)) {
1183                loadMessageForViewRemote(account, folder, uid, listener);
1184                localFolder.close(false);
1185                return;
1186            }
1187
1188            if (!message.isSet(Flag.SEEN)) {
1189                markMessageRead(account, folder, uid, true);
1190            }
1191
1192            FetchProfile fp = new FetchProfile();
1193            fp.add(FetchProfile.Item.ENVELOPE);
1194            fp.add(FetchProfile.Item.BODY);
1195            localFolder.fetch(new Message[] {
1196                message
1197            }, fp, null);
1198
1199            for (MessagingListener l : mListeners) {
1200                l.loadMessageForViewBodyAvailable(account, folder, uid, message);
1201            }
1202
1203            for (MessagingListener l : mListeners) {
1204                l.loadMessageForViewFinished(account, folder, uid, message);
1205            }
1206            localFolder.close(false);
1207        }
1208        catch (Exception e) {
1209            for (MessagingListener l : mListeners) {
1210                l.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1211            }
1212        }
1213    }
1214
1215    /**
1216     * Attempts to load the attachment specified by part from the given account and message.
1217     * @param account
1218     * @param message
1219     * @param part
1220     * @param listener
1221     */
1222    public void loadAttachment(
1223            final Account account,
1224            final Message message,
1225            final Part part,
1226            final Object tag,
1227            MessagingListener listener) {
1228        /*
1229         * Check if the attachment has already been downloaded. If it has there's no reason to
1230         * download it, so we just tell the listener that it's ready to go.
1231         */
1232        try {
1233            if (part.getBody() != null) {
1234                for (MessagingListener l : mListeners) {
1235                    l.loadAttachmentStarted(account, message, part, tag, false);
1236                }
1237
1238                for (MessagingListener l : mListeners) {
1239                    l.loadAttachmentFinished(account, message, part, tag);
1240                }
1241                return;
1242            }
1243        }
1244        catch (MessagingException me) {
1245            /*
1246             * If the header isn't there the attachment isn't downloaded yet, so just continue
1247             * on.
1248             */
1249        }
1250
1251        for (MessagingListener l : mListeners) {
1252            l.loadAttachmentStarted(account, message, part, tag, true);
1253        }
1254
1255        put("loadAttachment", listener, new Runnable() {
1256            public void run() {
1257                try {
1258                    LocalStore localStore = (LocalStore) Store.getInstance(
1259                            account.getLocalStoreUri(), mApplication, null);
1260                    /*
1261                     * We clear out any attachments already cached in the entire store and then
1262                     * we update the passed in message to reflect that there are no cached
1263                     * attachments. This is in support of limiting the account to having one
1264                     * attachment downloaded at a time.
1265                     */
1266                    localStore.pruneCachedAttachments();
1267                    ArrayList<Part> viewables = new ArrayList<Part>();
1268                    ArrayList<Part> attachments = new ArrayList<Part>();
1269                    MimeUtility.collectParts(message, viewables, attachments);
1270                    for (Part attachment : attachments) {
1271                        attachment.setBody(null);
1272                    }
1273                    Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication,
1274                            account.getStoreCallbacks());
1275                    LocalFolder localFolder =
1276                        (LocalFolder) localStore.getFolder(message.getFolder().getName());
1277                    Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName());
1278                    remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1279
1280                    FetchProfile fp = new FetchProfile();
1281                    fp.add(part);
1282                    remoteFolder.fetch(new Message[] { message }, fp, null);
1283                    localFolder.updateMessage((LocalMessage)message);
1284                    localFolder.close(false);
1285                    for (MessagingListener l : mListeners) {
1286                        l.loadAttachmentFinished(account, message, part, tag);
1287                    }
1288                }
1289                catch (MessagingException me) {
1290                    if (Config.LOGV) {
1291                        Log.v(Email.LOG_TAG, "", me);
1292                    }
1293                    for (MessagingListener l : mListeners) {
1294                        l.loadAttachmentFailed(account, message, part, tag, me.getMessage());
1295                    }
1296                }
1297            }
1298        });
1299    }
1300
1301    /**
1302     * Stores the given message in the Outbox and starts a sendPendingMessages command to
1303     * attempt to send the message.
1304     * @param account
1305     * @param message
1306     * @param listener
1307     */
1308    public void sendMessage(final Account account,
1309            final Message message,
1310            MessagingListener listener) {
1311        try {
1312            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1313            LocalFolder localFolder =
1314                (LocalFolder) localStore.getFolder(account.getOutboxFolderName());
1315            localFolder.open(OpenMode.READ_WRITE, null);
1316            localFolder.appendMessages(new Message[] {
1317                message
1318            });
1319            Message localMessage = localFolder.getMessage(message.getUid());
1320            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1321            localFolder.close(false);
1322            sendPendingMessages(account, null);
1323        }
1324        catch (Exception e) {
1325            for (MessagingListener l : mListeners) {
1326                // TODO general failed
1327            }
1328        }
1329    }
1330
1331    /**
1332     * Attempt to send any messages that are sitting in the Outbox.
1333     * @param account
1334     * @param listener
1335     */
1336    public void sendPendingMessages(final Account account,
1337            MessagingListener listener) {
1338        put("sendPendingMessages", listener, new Runnable() {
1339            public void run() {
1340                sendPendingMessagesSynchronous(account);
1341            }
1342        });
1343    }
1344
1345    /**
1346     * Attempt to send any messages that are sitting in the Outbox.
1347     * @param account
1348     * @param listener
1349     */
1350    public void sendPendingMessagesSynchronous(final Account account) {
1351        try {
1352            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1353            Folder localFolder = localStore.getFolder(
1354                    account.getOutboxFolderName());
1355            if (!localFolder.exists()) {
1356                return;
1357            }
1358            localFolder.open(OpenMode.READ_WRITE, null);
1359
1360            Message[] localMessages = localFolder.getMessages(null);
1361
1362            /*
1363             * The profile we will use to pull all of the content
1364             * for a given local message into memory for sending.
1365             */
1366            FetchProfile fp = new FetchProfile();
1367            fp.add(FetchProfile.Item.ENVELOPE);
1368            fp.add(FetchProfile.Item.BODY);
1369
1370            LocalFolder localSentFolder =
1371                (LocalFolder) localStore.getFolder(
1372                        account.getSentFolderName());
1373
1374            Sender sender = Sender.getInstance(account.getSenderUri(), mApplication);
1375            for (Message message : localMessages) {
1376                try {
1377                    localFolder.fetch(new Message[] { message }, fp, null);
1378                    try {
1379                        message.setFlag(Flag.X_SEND_IN_PROGRESS, true);
1380                        sender.sendMessage(message);
1381                        message.setFlag(Flag.X_SEND_IN_PROGRESS, false);
1382                        localFolder.copyMessages(new Message[] { message }, localSentFolder, null);
1383
1384                        PendingCommand command = new PendingCommand();
1385                        command.command = PENDING_COMMAND_APPEND;
1386                        command.arguments =
1387                            new String[] {
1388                                localSentFolder.getName(),
1389                                message.getUid() };
1390                        queuePendingCommand(account, command);
1391                        processPendingCommands(account);
1392                        message.setFlag(Flag.X_DESTROYED, true);
1393                    }
1394                    catch (Exception e) {
1395                        message.setFlag(Flag.X_SEND_FAILED, true);
1396                    }
1397                }
1398                catch (Exception e) {
1399                    /*
1400                     * We ignore this exception because a future refresh will retry this
1401                     * message.
1402                     */
1403                }
1404            }
1405            localFolder.expunge();
1406            if (localFolder.getMessageCount() == 0) {
1407                localFolder.delete(false);
1408            }
1409            for (MessagingListener l : mListeners) {
1410                l.sendPendingMessagesCompleted(account);
1411            }
1412        }
1413        catch (Exception e) {
1414            for (MessagingListener l : mListeners) {
1415                // TODO general failed
1416            }
1417        }
1418    }
1419
1420    /**
1421     * We do the local portion of this synchronously because other activities may have to make
1422     * updates based on what happens here
1423     * @param account
1424     * @param folder
1425     * @param message
1426     * @param listener
1427     */
1428    public void deleteMessage(final Account account, final String folder, final Message message,
1429            MessagingListener listener) {
1430        if (folder.equals(account.getTrashFolderName())) {
1431            return;
1432        }
1433        try {
1434            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1435            Folder localFolder = localStore.getFolder(folder);
1436            Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName());
1437
1438            localFolder.copyMessages(new Message[] { message }, localTrashFolder, null);
1439            message.setFlag(Flag.DELETED, true);
1440
1441            if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) {
1442                PendingCommand command = new PendingCommand();
1443                command.command = PENDING_COMMAND_TRASH;
1444                command.arguments = new String[] { folder, message.getUid() };
1445                queuePendingCommand(account, command);
1446                processPendingCommands(account);
1447            }
1448        }
1449        catch (MessagingException me) {
1450            throw new RuntimeException("Error deleting message from local store.", me);
1451        }
1452    }
1453
1454    public void emptyTrash(final Account account, MessagingListener listener) {
1455        put("emptyTrash", listener, new Runnable() {
1456            public void run() {
1457                // TODO IMAP
1458                try {
1459                    Store localStore = Store.getInstance(
1460                            account.getLocalStoreUri(), mApplication, null);
1461                    Folder localFolder = localStore.getFolder(account.getTrashFolderName());
1462                    localFolder.open(OpenMode.READ_WRITE, null);
1463                    Message[] messages = localFolder.getMessages(null);
1464                    localFolder.setFlags(messages, new Flag[] {
1465                        Flag.DELETED
1466                    }, true);
1467                    localFolder.close(true);
1468                    for (MessagingListener l : mListeners) {
1469                        l.emptyTrashCompleted(account);
1470                    }
1471                }
1472                catch (Exception e) {
1473                    // TODO
1474                    if (Config.LOGV) {
1475                        Log.v(Email.LOG_TAG, "emptyTrash");
1476                    }
1477                }
1478            }
1479        });
1480    }
1481
1482    /**
1483     * Checks mail for one or multiple accounts. If account is null all accounts
1484     * are checked.
1485     *
1486     * TODO:  There is no use case for "check all accounts".  Clean up this API to remove
1487     * that case.  Callers can supply the appropriate list.
1488     *
1489     * @param context
1490     * @param accountsToCheck List of accounts to check, or null to check all accounts
1491     * @param listener
1492     */
1493    public void checkMail(final Context context, final Account[] accountsToCheck,
1494            final MessagingListener listener) {
1495        for (MessagingListener l : mListeners) {
1496            l.checkMailStarted(context, null);      // TODO this needs to pass the actual array
1497        }
1498        put("checkMail", listener, new Runnable() {
1499            public void run() {
1500                Account[] accounts = accountsToCheck;
1501                if (accounts == null) {
1502                    accounts = Preferences.getPreferences(context).getAccounts();
1503                }
1504                for (Account account : accounts) {
1505                    sendPendingMessagesSynchronous(account);
1506                    synchronizeMailboxSyncronous(account, Email.INBOX);
1507                }
1508                for (MessagingListener l : mListeners) {
1509                    l.checkMailFinished(context, null);  // TODO this needs to pass the actual array
1510                }
1511            }
1512        });
1513    }
1514
1515    public void saveDraft(final Account account, final Message message) {
1516        try {
1517            Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
1518            LocalFolder localFolder =
1519                (LocalFolder) localStore.getFolder(account.getDraftsFolderName());
1520            localFolder.open(OpenMode.READ_WRITE, null);
1521            localFolder.appendMessages(new Message[] {
1522                message
1523            });
1524            Message localMessage = localFolder.getMessage(message.getUid());
1525            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1526
1527            PendingCommand command = new PendingCommand();
1528            command.command = PENDING_COMMAND_APPEND;
1529            command.arguments = new String[] {
1530                    localFolder.getName(),
1531                    localMessage.getUid() };
1532            queuePendingCommand(account, command);
1533            processPendingCommands(account);
1534        }
1535        catch (MessagingException e) {
1536            Log.e(Email.LOG_TAG, "Unable to save message as draft.", e);
1537        }
1538    }
1539
1540    class Command {
1541        public Runnable runnable;
1542
1543        public MessagingListener listener;
1544
1545        public String description;
1546    }
1547}
1548