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