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