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