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