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