MessagingController.java revision b7e954bba6539141d24efdb0a0091962e5002ba0
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.Address;
20import com.android.email.mail.FetchProfile;
21import com.android.email.mail.Flag;
22import com.android.email.mail.Folder;
23import com.android.email.mail.Message;
24import com.android.email.mail.MessageRetrievalListener;
25import com.android.email.mail.MessagingException;
26import com.android.email.mail.Part;
27import com.android.email.mail.Sender;
28import com.android.email.mail.Store;
29import com.android.email.mail.StoreSynchronizer;
30import com.android.email.mail.Folder.FolderType;
31import com.android.email.mail.Folder.OpenMode;
32import com.android.email.mail.internet.MimeMessage;
33import com.android.email.mail.internet.MimeUtility;
34import com.android.email.mail.store.LocalStore;
35import com.android.email.mail.store.LocalStore.LocalFolder;
36import com.android.email.mail.store.LocalStore.LocalMessage;
37import com.android.email.mail.store.LocalStore.PendingCommand;
38import com.android.email.provider.EmailContent;
39import com.android.email.provider.EmailContent.MailboxColumns;
40import com.android.email.provider.EmailContent.MessageColumns;
41import com.android.email.provider.EmailContent.SyncColumns;
42
43import android.content.ContentUris;
44import android.content.ContentValues;
45import android.content.Context;
46import android.database.Cursor;
47import android.net.Uri;
48import android.os.Process;
49import android.util.Config;
50import android.util.Log;
51
52import java.util.ArrayList;
53import java.util.Date;
54import java.util.HashMap;
55import java.util.HashSet;
56import java.util.concurrent.BlockingQueue;
57import java.util.concurrent.LinkedBlockingQueue;
58
59/**
60 * Starts a long running (application) Thread that will run through commands
61 * that require remote mailbox access. This class is used to serialize and
62 * prioritize these commands. Each method that will submit a command requires a
63 * MessagingListener instance to be provided. It is expected that that listener
64 * has also been added as a registered listener using addListener(). When a
65 * command is to be executed, if the listener that was provided with the command
66 * is no longer registered the command is skipped. The design idea for the above
67 * is that when an Activity starts it registers as a listener. When it is paused
68 * it removes itself. Thus, any commands that that activity submitted are
69 * removed from the queue once the activity is no longer active.
70 */
71public class MessagingController implements Runnable {
72    /**
73     * The maximum message size that we'll consider to be "small". A small message is downloaded
74     * in full immediately instead of in pieces. Anything over this size will be downloaded in
75     * pieces with attachments being left off completely and downloaded on demand.
76     *
77     *
78     * 25k for a "small" message was picked by educated trial and error.
79     * http://answers.google.com/answers/threadview?id=312463 claims that the
80     * average size of an email is 59k, which I feel is too large for our
81     * blind download. The following tests were performed on a download of
82     * 25 random messages.
83     * <pre>
84     * 5k - 61 seconds,
85     * 25k - 51 seconds,
86     * 55k - 53 seconds,
87     * </pre>
88     * So 25k gives good performance and a reasonable data footprint. Sounds good to me.
89     */
90    private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
91
92    private static final String PENDING_COMMAND_TRASH =
93        "com.android.email.MessagingController.trash";
94    private static final String PENDING_COMMAND_MARK_READ =
95        "com.android.email.MessagingController.markRead";
96    private static final String PENDING_COMMAND_APPEND =
97        "com.android.email.MessagingController.append";
98
99    private static MessagingController inst = null;
100    private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
101    private Thread mThread;
102
103    /**
104     * All access to mListeners *must* be synchronized
105     */
106    private GroupMessagingListener mListeners = new GroupMessagingListener();
107    private boolean mBusy;
108    private Context mContext;
109
110    protected MessagingController(Context _context) {
111        mContext = _context;
112        mThread = new Thread(this);
113        mThread.start();
114    }
115
116    /**
117     * Gets or creates the singleton instance of MessagingController. Application is used to
118     * provide a Context to classes that need it.
119     * @param application
120     * @return
121     */
122    public synchronized static MessagingController getInstance(Context _context) {
123        if (inst == null) {
124            inst = new MessagingController(_context);
125        }
126        return inst;
127    }
128
129    /**
130     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
131     */
132    public static void injectMockController(MessagingController mockController) {
133        inst = mockController;
134    }
135
136    // TODO: seems that this reading of mBusy isn't thread-safe
137    public boolean isBusy() {
138        return mBusy;
139    }
140
141    public void run() {
142        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
143        // TODO: add an end test to this infinite loop
144        while (true) {
145            Command command;
146            try {
147                command = mCommands.take();
148            } catch (InterruptedException e) {
149                continue; //re-test the condition on the eclosing while
150            }
151            if (command.listener == null || isActiveListener(command.listener)) {
152                mBusy = true;
153                command.runnable.run();
154                mListeners.controllerCommandCompleted(mCommands.size() > 0);
155            }
156            mBusy = false;
157        }
158    }
159
160    private void put(String description, MessagingListener listener, Runnable runnable) {
161        try {
162            Command command = new Command();
163            command.listener = listener;
164            command.runnable = runnable;
165            command.description = description;
166            mCommands.add(command);
167        }
168        catch (IllegalStateException ie) {
169            throw new Error(ie);
170        }
171    }
172
173    public void addListener(MessagingListener listener) {
174        mListeners.addListener(listener);
175    }
176
177    public void removeListener(MessagingListener listener) {
178        mListeners.removeListener(listener);
179    }
180
181    private boolean isActiveListener(MessagingListener listener) {
182        return mListeners.isActiveListener(listener);
183    }
184
185    /**
186     * Lightweight class for capturing local mailboxes in an account.  Just the columns
187     * necessary for a sync.
188     */
189    private static class LocalMailboxInfo {
190        private static final int COLUMN_ID = 0;
191        private static final int COLUMN_DISPLAY_NAME = 1;
192        private static final int COLUMN_ACCOUNT_KEY = 2;
193
194        private static final String[] PROJECTION = new String[] {
195            EmailContent.RECORD_ID,
196            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
197        };
198
199        long mId;
200        String mDisplayName;
201        long mAccountKey;
202
203        public LocalMailboxInfo(Cursor c) {
204            mId = c.getLong(COLUMN_ID);
205            mDisplayName = c.getString(COLUMN_DISPLAY_NAME);
206            mAccountKey = c.getLong(COLUMN_ACCOUNT_KEY);
207        }
208    }
209
210    /**
211     * Lists folders that are available locally and remotely. This method calls
212     * listFoldersCallback for local folders before it returns, and then for
213     * remote folders at some later point. If there are no local folders
214     * includeRemote is forced by this method. This method should be called from
215     * a Thread as it may take several seconds to list the local folders.
216     *
217     * TODO this needs to cache the remote folder list
218     * TODO break out an inner listFoldersSynchronized which could simplify checkMail
219     *
220     * @param account
221     * @param listener
222     * @throws MessagingException
223     */
224    public void listFolders(final EmailContent.Account account, MessagingListener listener) {
225        mListeners.listFoldersStarted(account);
226        put("listFolders", listener, new Runnable() {
227            public void run() {
228                Cursor localFolderCursor = null;
229                try {
230                    // Step 1:  Get remote folders, make a list, and add any local folders
231                    // that don't already exist.
232
233                    Store store = Store.getInstance(account.getStoreUri(mContext), mContext, null);
234
235                    Folder[] remoteFolders = store.getPersonalNamespaces();
236                    updateAccountFolderNames(account, remoteFolders);
237
238                    HashSet<String> remoteFolderNames = new HashSet<String>();
239                    for (int i = 0, count = remoteFolders.length; i < count; i++) {
240                        remoteFolderNames.add(remoteFolders[i].getName());
241                    }
242
243                    HashMap<String, LocalMailboxInfo> localFolders =
244                        new HashMap<String, LocalMailboxInfo>();
245                    HashSet<String> localFolderNames = new HashSet<String>();
246                    localFolderCursor = mContext.getContentResolver().query(
247                            EmailContent.Mailbox.CONTENT_URI,
248                            LocalMailboxInfo.PROJECTION,
249                            EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
250                            new String[] { String.valueOf(account.mId) },
251                            null);
252                    while (localFolderCursor.moveToNext()) {
253                        LocalMailboxInfo info = new LocalMailboxInfo(localFolderCursor);
254                        localFolders.put(info.mDisplayName, info);
255                        localFolderNames.add(info.mDisplayName);
256                    }
257
258                    // Short circuit the rest if the sets are the same (the usual case)
259                    if (!remoteFolderNames.equals(localFolderNames)) {
260
261                        // They are different, so we have to do some adds and drops
262
263                        // Drops first, to make things smaller rather than larger
264                        HashSet<String> localsToDrop = new HashSet<String>(localFolderNames);
265                        localsToDrop.removeAll(remoteFolderNames);
266                        for (String localNameToDrop : localsToDrop) {
267                            LocalMailboxInfo localInfo = localFolders.get(localNameToDrop);
268                            Uri uri = ContentUris.withAppendedId(
269                                    EmailContent.Mailbox.CONTENT_URI, localInfo.mId);
270                            mContext.getContentResolver().delete(uri, null, null);
271                        }
272
273                        // Now do the adds
274                        remoteFolderNames.removeAll(localFolderNames);
275                        for (String remoteNameToAdd : remoteFolderNames) {
276                            EmailContent.Mailbox box = new EmailContent.Mailbox();
277                            box.mDisplayName = remoteNameToAdd;
278                            // box.mServerId;
279                            // box.mParentServerId;
280                            box.mAccountKey = account.mId;
281                            box.mType = inferMailboxTypeFromName(account, remoteNameToAdd);
282                            // box.mDelimiter;
283                            // box.mSyncKey;
284                            // box.mSyncLookback;
285                            // box.mSyncFrequency;
286                            // box.mSyncTime;
287                            // box.mUnreadCount;
288                            box.mFlagVisible = true;
289                            // box.mFlags;
290                            box.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
291                            box.save(mContext);
292                        }
293                    }
294                    mListeners.listFoldersFinished(account);
295                } catch (Exception e) {
296                    mListeners.listFoldersFailed(account, "");
297                } finally {
298                    if (localFolderCursor != null) {
299                        localFolderCursor.close();
300                    }
301                }
302            }
303        });
304    }
305
306    /**
307     * Temporarily:  Infer mailbox type from mailbox name.  This should probably be
308     * mutated into something that the stores can provide directly, instead of the two-step
309     * where we scan and report.
310     */
311    public int inferMailboxTypeFromName(EmailContent.Account account, String mailboxName) {
312        if (mailboxName == null || mailboxName.length() == 0) {
313            return EmailContent.Mailbox.TYPE_MAIL;
314        }
315        if (mailboxName.equals(Email.INBOX)) {
316            return EmailContent.Mailbox.TYPE_INBOX;
317        }
318        if (mailboxName.equals(account.getTrashFolderName(mContext))) {
319            return EmailContent.Mailbox.TYPE_TRASH;
320        }
321        if (mailboxName.equals(account.getOutboxFolderName(mContext))) {
322            return EmailContent.Mailbox.TYPE_OUTBOX;
323        }
324        if (mailboxName.equals(account.getDraftsFolderName(mContext))) {
325            return EmailContent.Mailbox.TYPE_DRAFTS;
326        }
327        if (mailboxName.equals(account.getSentFolderName(mContext))) {
328            return EmailContent.Mailbox.TYPE_SENT;
329        }
330
331        return EmailContent.Mailbox.TYPE_MAIL;
332    }
333
334    /**
335     * Asks the store for a list of server-specific folder names and, if provided, updates
336     * the account record for future getFolder() operations.
337     *
338     * NOTE:  Inbox is not queried, because we require it to be INBOX, and outbox is not
339     * queried, because outbox is local-only.
340     *
341     * TODO: Rewrite this to use simple folder tagging and none of this account nonsense
342     */
343    /* package */ void updateAccountFolderNames(EmailContent.Account account,
344            Folder[] remoteFolders) {
345        String trash = null;
346        String sent = null;
347        String drafts = null;
348
349        for (Folder folder : remoteFolders) {
350            Folder.FolderRole role = folder.getRole();
351            if (role == Folder.FolderRole.TRASH) {
352                trash = folder.getName();
353            } else if (role == Folder.FolderRole.SENT) {
354                sent = folder.getName();
355            } else if (role == Folder.FolderRole.DRAFTS) {
356                drafts = folder.getName();
357            }
358        }
359/*
360        // Do not update when null (defaults are already in place)
361        boolean commit = false;
362        if (trash != null && !trash.equals(account.getTrashFolderName(mContext))) {
363            account.setTrashFolderName(trash);
364            commit = true;
365        }
366        if (sent != null && !sent.equals(account.getSentFolderName(mContext))) {
367            account.setSentFolderName(sent);
368            commit = true;
369        }
370        if (drafts != null && !drafts.equals(account.getDraftsFolderName(mContext))) {
371            account.setDraftsFolderName(drafts);
372            commit = true;
373        }
374        if (commit) {
375            account.saveOrUpdate(mContext);
376        }
377*/
378    }
379
380    /**
381     * List the local message store for the given folder. This work is done
382     * synchronously.
383     *
384     * @param account
385     * @param folder
386     * @param listener
387     * @throws MessagingException
388     */
389/*
390    public void listLocalMessages(final EmailContent.Account account, final String folder,
391            MessagingListener listener) {
392        synchronized (mListeners) {
393            for (MessagingListener l : mListeners) {
394                l.listLocalMessagesStarted(account, folder);
395            }
396        }
397
398        try {
399            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
400                    null);
401            Folder localFolder = localStore.getFolder(folder);
402            localFolder.open(OpenMode.READ_WRITE, null);
403            Message[] localMessages = localFolder.getMessages(null);
404            ArrayList<Message> messages = new ArrayList<Message>();
405            for (Message message : localMessages) {
406                if (!message.isSet(Flag.DELETED)) {
407                    messages.add(message);
408                }
409            }
410            synchronized (mListeners) {
411                for (MessagingListener l : mListeners) {
412                    l.listLocalMessages(account, folder, messages.toArray(new Message[0]));
413                }
414                for (MessagingListener l : mListeners) {
415                    l.listLocalMessagesFinished(account, folder);
416                }
417            }
418        }
419        catch (Exception e) {
420            synchronized (mListeners) {
421                for (MessagingListener l : mListeners) {
422                    l.listLocalMessagesFailed(account, folder, e.getMessage());
423                }
424            }
425        }
426    }
427*/
428
429    /**
430     * Increase the window size for a given mailbox, and load more from server.
431     */
432    public void loadMoreMessages(EmailContent.Account account, EmailContent.Mailbox folder,
433            MessagingListener listener) {
434
435        // TODO redo implementation
436/*
437        try {
438            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext),
439                    mContext);
440            LocalStore localStore = (LocalStore) Store.getInstance(
441                    account.getLocalStoreUri(mContext), mContext, null);
442            LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
443            int oldLimit = localFolder.getVisibleLimit();
444            if (oldLimit <= 0) {
445                oldLimit = info.mVisibleLimitDefault;
446            }
447            localFolder.setVisibleLimit(oldLimit + info.mVisibleLimitIncrement);
448            synchronizeMailbox(account, folder, listener);
449        }
450        catch (MessagingException me) {
451            throw new RuntimeException("Unable to set visible limit on folder", me);
452        }
453*/
454    }
455
456    public void resetVisibleLimits(EmailContent.Account account) {
457        try {
458            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext),
459                    mContext);
460            if (info != null) {
461                LocalStore localStore = (LocalStore) Store.getInstance(
462                        account.getLocalStoreUri(mContext), mContext, null);
463                localStore.resetVisibleLimits(info.mVisibleLimitDefault);
464            }
465        }
466        catch (MessagingException e) {
467            Log.e(Email.LOG_TAG, "Unable to reset visible limits", e);
468        }
469    }
470
471    /**
472     * Start background synchronization of the specified folder.
473     * @param account
474     * @param folder
475     * @param listener
476     */
477    public void synchronizeMailbox(final EmailContent.Account account,
478            final EmailContent.Mailbox folder, MessagingListener listener) {
479        /*
480         * We don't ever sync the Outbox.
481         */
482        if (folder.mType == EmailContent.Mailbox.TYPE_OUTBOX) {
483            return;
484        }
485        mListeners.synchronizeMailboxStarted(account, folder);
486        put("synchronizeMailbox", listener, new Runnable() {
487            public void run() {
488                synchronizeMailboxSynchronous(account, folder);
489            }
490        });
491    }
492
493    /**
494     * Start foreground synchronization of the specified folder. This is called by
495     * synchronizeMailbox or checkMail.
496     * @param account
497     * @param folder
498     * @param listener
499     */
500    private void synchronizeMailboxSynchronous(final EmailContent.Account account,
501            final EmailContent.Mailbox folder) {
502        mListeners.synchronizeMailboxStarted(account, folder);
503        try {
504            processPendingCommandsSynchronous(account);
505
506            StoreSynchronizer.SyncResults results;
507
508            // Select generic sync or store-specific sync
509            final LocalStore localStore =
510                (LocalStore) Store.getInstance(account.getLocalStoreUri(mContext), mContext, null);
511            Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
512                    localStore.getPersistentCallbacks());
513            StoreSynchronizer customSync = remoteStore.getMessageSynchronizer();
514            if (customSync == null) {
515                results = synchronizeMailboxGeneric(account, folder);
516            } else {
517                results = customSync.SynchronizeMessagesSynchronous(
518                        account, folder, mListeners, mContext);
519            }
520            mListeners.synchronizeMailboxFinished(account,
521                                                  folder,
522                                                  results.mTotalMessages,
523                                                  results.mNewMessages);
524        } catch (MessagingException e) {
525            if (Config.LOGV) {
526                Log.v(Email.LOG_TAG, "synchronizeMailbox", e);
527            }
528            mListeners.synchronizeMailboxFailed(account, folder, e);
529        }
530    }
531
532    // TODO move all this to top
533/*
534        public static final int CONTENT_ID_COLUMN = 0;
535        public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
536        public static final int CONTENT_TIMESTAMP_COLUMN = 2;
537        public static final int CONTENT_SUBJECT_COLUMN = 3;
538        public static final int CONTENT_PREVIEW_COLUMN = 4;
539        public static final int CONTENT_FLAG_READ_COLUMN = 5;
540        public static final int CONTENT_FLAG_LOADED_COLUMN = 6;
541        public static final int CONTENT_FLAG_FAVORITE_COLUMN = 7;
542        public static final int CONTENT_FLAG_ATTACHMENT_COLUMN = 8;
543        public static final int CONTENT_FLAGS_COLUMN = 9;
544        public static final int CONTENT_TEXT_INFO_COLUMN = 10;
545        public static final int CONTENT_HTML_INFO_COLUMN = 11;
546        public static final int CONTENT_BODY_ID_COLUMN = 12;
547        public static final int CONTENT_SERVER_ID_COLUMN = 13;
548        public static final int CONTENT_CLIENT_ID_COLUMN = 14;
549        public static final int CONTENT_MESSAGE_ID_COLUMN = 15;
550        public static final int CONTENT_THREAD_ID_COLUMN = 16;
551        public static final int CONTENT_MAILBOX_KEY_COLUMN = 17;
552        public static final int CONTENT_ACCOUNT_KEY_COLUMN = 18;
553        public static final int CONTENT_REFERENCE_KEY_COLUMN = 19;
554        public static final int CONTENT_SENDER_LIST_COLUMN = 20;
555        public static final int CONTENT_FROM_LIST_COLUMN = 21;
556        public static final int CONTENT_TO_LIST_COLUMN = 22;
557        public static final int CONTENT_CC_LIST_COLUMN = 23;
558        public static final int CONTENT_BCC_LIST_COLUMN = 24;
559        public static final int CONTENT_REPLY_TO_COLUMN = 25;
560        public static final int CONTENT_SERVER_VERSION_COLUMN = 26;
561        public static final String[] CONTENT_PROJECTION = new String[] {
562            RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP,
563            MessageColumns.SUBJECT, MessageColumns.PREVIEW, MessageColumns.FLAG_READ,
564            MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
565            MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS, MessageColumns.TEXT_INFO,
566            MessageColumns.HTML_INFO, MessageColumns.BODY_ID, SyncColumns.SERVER_ID,
567            MessageColumns.CLIENT_ID, MessageColumns.MESSAGE_ID, MessageColumns.THREAD_ID,
568            MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, MessageColumns.REFERENCE_KEY,
569            MessageColumns.SENDER_LIST, MessageColumns.FROM_LIST, MessageColumns.TO_LIST,
570            MessageColumns.CC_LIST, MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
571            SyncColumns.SERVER_VERSION
572        };
573*/
574
575    /**
576     * Lightweight record for the first pass of message sync, where I'm just seeing if
577     * the local message requires sync.  Later (for messages that need syncing) we'll do a full
578     * readout from the DB.
579     */
580    private static class LocalMessageInfo {
581        private static final int COLUMN_ID = 0;
582        private static final int COLUMN_FLAG_READ = 1;
583        private static final int COLUMN_FLAG_LOADED = 2;
584        private static final int COLUMN_SERVER_ID = 3;
585        private static final int COLUMN_MAILBOX_KEY = 4;
586        private static final int COLUMN_ACCOUNT_KEY = 5;
587        private static final String[] PROJECTION = new String[] {
588            EmailContent.RECORD_ID,
589            MessageColumns.FLAG_READ, MessageColumns.FLAG_LOADED,
590            SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY
591        };
592
593        int mCursorIndex;
594        long mId;
595        boolean mFlagRead;
596        int mFlagLoaded;
597        String mServerId;
598
599        public LocalMessageInfo(Cursor c) {
600            mCursorIndex = c.getPosition();
601            mId = c.getLong(COLUMN_ID);
602            mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
603            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
604            mServerId = c.getString(COLUMN_SERVER_ID);
605            // Note: mailbox key and account key not needed - they are projected for the SELECT
606        }
607    }
608
609    /**
610     * Generic synchronizer - used for POP3 and IMAP.
611     *
612     * TODO Break this method up into smaller chunks.
613     *
614     * @param account the account to sync
615     * @param folder the mailbox to sync
616     * @return results of the sync pass
617     * @throws MessagingException
618     */
619    private StoreSynchronizer.SyncResults synchronizeMailboxGeneric(
620            final EmailContent.Account account, final EmailContent.Mailbox folder)
621            throws MessagingException {
622
623        Log.d(Email.LOG_TAG, "*** synchronizeMailboxGeneric ***");
624
625        // 1.  Get the message list from the local store and create an index of the uids
626
627        Cursor localUidCursor = null;
628        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
629
630        try {
631            localUidCursor = mContext.getContentResolver().query(
632                    EmailContent.Message.CONTENT_URI,
633                    LocalMessageInfo.PROJECTION,
634                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
635                    " AND " + MessageColumns.MAILBOX_KEY + "=?",
636                    new String[] {
637                            String.valueOf(account.mId),
638                            String.valueOf(folder.mId)
639                    },
640                    null);
641            while (localUidCursor.moveToNext()) {
642                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
643                localMessageMap.put(info.mServerId, info);
644            }
645        } finally {
646            if (localUidCursor != null) {
647                localUidCursor.close();
648            }
649        }
650
651        // 1a. Count the unread messages before changing anything
652        int localUnreadCount = EmailContent.count(mContext, EmailContent.Message.CONTENT_URI,
653                EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
654                " AND " + MessageColumns.MAILBOX_KEY + "=?" +
655                " AND " + MessageColumns.FLAG_READ + "=0",
656                new String[] {
657                        String.valueOf(account.mId),
658                        String.valueOf(folder.mId)
659                });
660
661        // 2.  Open the remote folder and create the remote folder if necessary
662
663        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, null);
664        Folder remoteFolder = remoteStore.getFolder(folder.mDisplayName);
665
666        /*
667         * If the folder is a "special" folder we need to see if it exists
668         * on the remote server. It if does not exist we'll try to create it. If we
669         * can't create we'll abort. This will happen on every single Pop3 folder as
670         * designed and on Imap folders during error conditions. This allows us
671         * to treat Pop3 and Imap the same in this code.
672         */
673        if (folder.equals(account.getTrashFolderName(mContext)) ||
674                folder.equals(account.getSentFolderName(mContext)) ||
675                folder.equals(account.getDraftsFolderName(mContext))) {
676            if (!remoteFolder.exists()) {
677                if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
678                    return new StoreSynchronizer.SyncResults(0, 0);
679                }
680            }
681        }
682
683        // 3, Open the remote folder. This pre-loads certain metadata like message count.
684        remoteFolder.open(OpenMode.READ_WRITE, null);
685
686        // 4. Trash any remote messages that are marked as trashed locally.
687        // TODO - this comment was here, but no code was here.
688
689        // 5. Get the remote message count.
690        int remoteMessageCount = remoteFolder.getMessageCount();
691
692        // 6. Determine the limit # of messages to download
693        // TODO decide where to persist the visible limit (account?) until we switch UI model
694        int visibleLimit = -1;  // localFolder.getVisibleLimit();
695        if (visibleLimit <= 0) {
696            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext),
697                    mContext);
698            visibleLimit = info.mVisibleLimitDefault;
699            // localFolder.setVisibleLimit(visibleLimit);
700        }
701
702        // 7.  Create a list of messages to download
703        Message[] remoteMessages = new Message[0];
704        final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
705        HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
706
707        int newMessageCount = 0;
708        if (remoteMessageCount > 0) {
709            /*
710             * Message numbers start at 1.
711             */
712            int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
713            int remoteEnd = remoteMessageCount;
714            remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
715            for (Message message : remoteMessages) {
716                remoteUidMap.put(message.getUid(), message);
717            }
718
719            /*
720             * Get a list of the messages that are in the remote list but not on the
721             * local store, or messages that are in the local store but failed to download
722             * on the last sync. These are the new messages that we will download.
723             */
724            for (Message message : remoteMessages) {
725                LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
726                if (localMessage == null) {
727                    newMessageCount++;
728                }
729                if (localMessage == null ||
730                        localMessage.mFlagLoaded != EmailContent.Message.LOADED) {
731                    unsyncedMessages.add(message);
732                }
733            }
734        }
735
736        // 8.  Download basic info about the new/unloaded messages (if any)
737        /*
738         * A list of messages that were downloaded and which did not have the Seen flag set.
739         * This will serve to indicate the true "new" message count that will be reported to
740         * the user via notification.
741         */
742        final ArrayList<Message> newMessages = new ArrayList<Message>();
743
744        /*
745         * Fetch the flags and envelope only of the new messages. This is intended to get us
746         * critical data as fast as possible, and then we'll fill in the details.
747         */
748        if (unsyncedMessages.size() > 0) {
749            FetchProfile fp = new FetchProfile();
750            fp.add(FetchProfile.Item.FLAGS);
751            fp.add(FetchProfile.Item.ENVELOPE);
752            final HashMap<String, LocalMessageInfo> localMapCopy =
753                new HashMap<String, LocalMessageInfo>(localMessageMap);
754
755            remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
756                    new MessageRetrievalListener() {
757                        public void messageFinished(Message message, int number, int ofTotal) {
758                            try {
759                                // Determine if the new message was already known (e.g. partial)
760                                // And create or reload the full message info
761                                LocalMessageInfo localMessageInfo =
762                                    localMapCopy.get(message.getUid());
763                                EmailContent.Message localMessage = null;
764                                if (localMessageInfo == null) {
765                                    localMessage = new EmailContent.Message();
766                                } else {
767                                    localMessage = EmailContent.Message.restoreMessageWithId(
768                                            mContext, localMessageInfo.mId);
769                                }
770
771                                if (localMessage != null) {
772                                    try {
773                                        // Copy the fields that are available into the message
774                                        updateMessageFields(localMessage,
775                                                message, account.mId, folder.mId);
776                                        // Commit the message to the local store
777                                        localMessage.saveOrUpdate(mContext);
778                                        // Track the "new" ness of the downloaded message
779                                        if (!message.isSet(Flag.SEEN)) {
780                                            newMessages.add(message);
781                                        }
782                                    } catch (MessagingException me) {
783                                        Log.e(Email.LOG_TAG,
784                                                "Error while copying downloaded message." + me);
785                                    }
786
787                                }
788                            }
789                            catch (Exception e) {
790                                Log.e(Email.LOG_TAG,
791                                        "Error while storing downloaded message." + e.toString());
792                            }
793                        }
794
795                        public void messageStarted(String uid, int number, int ofTotal) {
796                        }
797                    });
798        }
799
800        // 9. Refresh the flags for any messages in the local store that we didn't just download.
801        FetchProfile fp = new FetchProfile();
802        fp.add(FetchProfile.Item.FLAGS);
803        remoteFolder.fetch(remoteMessages, fp, null);
804        boolean remoteSupportsSeenFlag = false;
805        for (Flag flag : remoteFolder.getPermanentFlags()) {
806            if (flag == Flag.SEEN) {
807                remoteSupportsSeenFlag = true;
808            }
809        }
810        // Update the SEEN flag (if supported remotely - e.g. not for POP3)
811        if (remoteSupportsSeenFlag) {
812            for (Message remoteMessage : remoteMessages) {
813                LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
814                if (localMessageInfo == null) {
815                    continue;
816                }
817                boolean localSeen = localMessageInfo.mFlagRead;
818                boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
819                if (remoteSeen != localSeen) {
820                    Uri uri = ContentUris.withAppendedId(
821                            EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
822                    ContentValues updateValues = new ContentValues();
823                    updateValues.put(EmailContent.Message.FLAG_READ, remoteSeen ? 1 : 0);
824                    mContext.getContentResolver().update(uri, updateValues, null, null);
825                }
826            }
827        }
828
829        // 10. Compute and store the unread message count.
830
831        int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount();
832        if (remoteUnreadMessageCount == -1) {
833            if (remoteSupportsSeenFlag) {
834                /*
835                 * If remote folder doesn't supported unread message count but supports
836                 * seen flag, use local folder's unread message count and the size of
837                 * new messages. This mode is not used for POP3, or IMAP.
838                 */
839
840                remoteUnreadMessageCount = folder.mUnreadCount + newMessages.size();
841            } else {
842                /*
843                 * If remote folder doesn't supported unread message count and doesn't
844                 * support seen flag, use localUnreadCount and newMessageCount which
845                 * don't rely on remote SEEN flag.  This mode is used by POP3.
846                 */
847                remoteUnreadMessageCount = localUnreadCount + newMessageCount;
848            }
849        } else {
850            /*
851             * If remote folder supports unread message count, use remoteUnreadMessageCount.
852             * This mode is used by IMAP.
853             */
854         }
855        Uri uri = ContentUris.withAppendedId(EmailContent.Mailbox.CONTENT_URI, folder.mId);
856        ContentValues updateValues = new ContentValues();
857        updateValues.put(EmailContent.Mailbox.UNREAD_COUNT, remoteUnreadMessageCount);
858        mContext.getContentResolver().update(uri, updateValues, null, null);
859
860        // 11. Remove any messages that are in the local store but no longer on the remote store.
861
862        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
863        localUidsToDelete.removeAll(remoteUidMap.keySet());
864        for (String uidToDelete : localUidsToDelete) {
865            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
866
867            Uri uriToDelete = ContentUris.withAppendedId(
868                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
869            mContext.getContentResolver().delete(uriToDelete, null, null);
870        }
871
872        // 12. Divide the unsynced messages into small & large (by size)
873
874        // TODO doing this work here (synchronously) is problematic because it prevents the UI
875        // from affecting the order (e.g. download a message because the user requested it.)  Much
876        // of this logic should move out to a different sync loop that attempts to update small
877        // groups of messages at a time, as a background task.  However, we can't just return
878        // (yet) because POP messages don't have an envelope yet....
879
880        ArrayList<Message> largeMessages = new ArrayList<Message>();
881        ArrayList<Message> smallMessages = new ArrayList<Message>();
882        for (Message message : unsyncedMessages) {
883            if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
884                largeMessages.add(message);
885            } else {
886                smallMessages.add(message);
887            }
888        }
889
890        // 13. Download small messages
891
892        // TODO Problems with this implementation.  1. For IMAP, where we get a real envelope,
893        // this is going to be inefficient and duplicate work we've already done.  2.  It's going
894        // back to the DB for a local message that we already had (and discarded).
895
896        fp = new FetchProfile();
897        fp.add(FetchProfile.Item.BODY);
898        remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
899                new MessageRetrievalListener() {
900                    public void messageFinished(Message message, int number, int ofTotal) {
901                        try {
902                            EmailContent.Message localMessage = null;
903                            Cursor c = null;
904                            try {
905                                c = mContext.getContentResolver().query(
906                                        EmailContent.Message.CONTENT_URI,
907                                        EmailContent.Message.CONTENT_PROJECTION,
908                                        EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
909                                        " AND " + MessageColumns.MAILBOX_KEY + "=?" +
910                                        " AND " + SyncColumns.SERVER_ID + "=?",
911                                        new String[] {
912                                                String.valueOf(account.mId),
913                                                String.valueOf(folder.mId),
914                                                String.valueOf(message.getUid())
915                                        },
916                                        null);
917                                if (c.moveToNext()) {
918                                    localMessage = EmailContent.getContent(
919                                            c, EmailContent.Message.class);
920                                }
921                            } finally {
922                                if (c != null) {
923                                    c.close();
924                                }
925                            }
926
927                            if (localMessage != null) {
928                                EmailContent.Body body = EmailContent.Body.restoreBodyWithId(
929                                        mContext, localMessage.mId);
930                                if (body == null) {
931                                    body = new EmailContent.Body();
932                                }
933                                try {
934                                    // Copy the fields that are available into the message
935                                    updateMessageFields(localMessage,
936                                            message, account.mId, folder.mId);
937                                    updateBodyFields(body, localMessage, message);
938                                    // TODO should updateMessageFields do this for us?
939                                    // localMessage.mFlagLoaded = EmailContent.Message.LOADED;
940                                    // Commit the message to the local store
941                                    localMessage.saveOrUpdate(mContext);
942                                    body.saveOrUpdate(mContext);
943                                } catch (MessagingException me) {
944                                    Log.e(Email.LOG_TAG,
945                                            "Error while copying downloaded message." + me);
946                                }
947
948                            }
949                        }
950                        catch (Exception e) {
951                            Log.e(Email.LOG_TAG,
952                                    "Error while storing downloaded message." + e.toString());
953                        }
954                    }
955
956                    public void messageStarted(String uid, int number, int ofTotal) {
957                    }
958        });
959
960        // 14. Download large messages
961
962        // 15. Clean up and report results
963
964        // Original sync code.  Using for reference, will delete when done.
965        if (false) {
966        /*
967         * Grab the content of the small messages first. This is going to
968         * be very fast and at very worst will be a single up of a few bytes and a single
969         * download of 625k.
970         */
971        fp = new FetchProfile();
972        fp.add(FetchProfile.Item.BODY);
973        remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]),
974                fp, new MessageRetrievalListener() {
975            public void messageFinished(Message message, int number, int ofTotal) {
976//                try {
977//                    // Store the updated message locally
978//                    localFolder.appendMessages(new Message[] {
979//                        message
980//                    });
981//
982//                    Message localMessage = localFolder.getMessage(message.getUid());
983//
984//                    // Set a flag indicating this message has now be fully downloaded
985//                    localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
986//
987//                    // Update the listener with what we've found
988//                    synchronized (mListeners) {
989//                        for (MessagingListener l : mListeners) {
990//                            l.synchronizeMailboxNewMessage(
991//                                    account,
992//                                    folder,
993//                                    localMessage);
994//                        }
995//                    }
996//                }
997//                catch (MessagingException me) {
998//
999//                }
1000            }
1001
1002            public void messageStarted(String uid, int number, int ofTotal) {
1003            }
1004        });
1005
1006        /*
1007         * Now do the large messages that require more round trips.
1008         */
1009        fp.clear();
1010        fp.add(FetchProfile.Item.STRUCTURE);
1011        remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]),
1012                fp, null);
1013        for (Message message : largeMessages) {
1014            if (message.getBody() == null) {
1015                /*
1016                 * The provider was unable to get the structure of the message, so
1017                 * we'll download a reasonable portion of the messge and mark it as
1018                 * incomplete so the entire thing can be downloaded later if the user
1019                 * wishes to download it.
1020                 */
1021                fp.clear();
1022                fp.add(FetchProfile.Item.BODY_SANE);
1023                /*
1024                 *  TODO a good optimization here would be to make sure that all Stores set
1025                 *  the proper size after this fetch and compare the before and after size. If
1026                 *  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
1027                 */
1028
1029                remoteFolder.fetch(new Message[] { message }, fp, null);
1030                // Store the updated message locally
1031//                localFolder.appendMessages(new Message[] {
1032//                    message
1033//                });
1034
1035//                Message localMessage = localFolder.getMessage(message.getUid());
1036
1037                // Set a flag indicating that the message has been partially downloaded and
1038                // is ready for view.
1039//                localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true);
1040            } else {
1041                /*
1042                 * We have a structure to deal with, from which
1043                 * we can pull down the parts we want to actually store.
1044                 * Build a list of parts we are interested in. Text parts will be downloaded
1045                 * right now, attachments will be left for later.
1046                 */
1047
1048                ArrayList<Part> viewables = new ArrayList<Part>();
1049                ArrayList<Part> attachments = new ArrayList<Part>();
1050                MimeUtility.collectParts(message, viewables, attachments);
1051
1052                /*
1053                 * Now download the parts we're interested in storing.
1054                 */
1055                for (Part part : viewables) {
1056                    fp.clear();
1057                    fp.add(part);
1058                    // TODO what happens if the network connection dies? We've got partial
1059                    // messages with incorrect status stored.
1060                    remoteFolder.fetch(new Message[] { message }, fp, null);
1061                }
1062                // Store the updated message locally
1063//                localFolder.appendMessages(new Message[] {
1064//                    message
1065//                });
1066
1067//                Message localMessage = localFolder.getMessage(message.getUid());
1068
1069                // Set a flag indicating this message has been fully downloaded and can be
1070                // viewed.
1071//                localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1072            }
1073
1074            // Update the listener with what we've found
1075//            synchronized (mListeners) {
1076//                for (MessagingListener l : mListeners) {
1077//                    l.synchronizeMailboxNewMessage(
1078//                            account,
1079//                            folder,
1080//                            localFolder.getMessage(message.getUid()));
1081//                }
1082//            }
1083        }
1084
1085
1086        /*
1087         * Report successful sync
1088         */
1089        StoreSynchronizer.SyncResults results = new StoreSynchronizer.SyncResults(
1090                remoteFolder.getMessageCount(), newMessages.size());
1091
1092        remoteFolder.close(false);
1093//        localFolder.close(false);
1094
1095        return results;
1096        }
1097
1098        return new StoreSynchronizer.SyncResults(0, 0);
1099
1100    }
1101
1102    /**
1103     * Copy field-by-field from a "store" message to a "provider" message
1104     * @param message The message we've just downloaded
1105     * @param localMessage The message we'd like to write into the DB
1106     * @result true if dirty (changes were made)
1107     */
1108    /* package */ boolean updateMessageFields(EmailContent.Message localMessage, Message message,
1109            long accountId, long mailboxId) throws MessagingException {
1110
1111        Address[] from = message.getFrom();
1112        Address[] to = message.getRecipients(Message.RecipientType.TO);
1113        Address[] cc = message.getRecipients(Message.RecipientType.CC);
1114        Address[] bcc = message.getRecipients(Message.RecipientType.BCC);
1115        Address[] replyTo = message.getReplyTo();
1116        String subject = message.getSubject();
1117        Date sentDate = message.getSentDate();
1118
1119        if (from != null && from.length > 0) {
1120            localMessage.mDisplayName = from[0].toFriendly();
1121        }
1122        if (sentDate != null) {
1123            localMessage.mTimeStamp = sentDate.getTime();
1124        }
1125        if (subject != null) {
1126            localMessage.mSubject = subject;
1127        }
1128//        public String mPreview;
1129//        public boolean mFlagRead = false;
1130        if (localMessage.mFlagLoaded != EmailContent.Message.LOADED) {
1131            localMessage.mFlagLoaded = EmailContent.Message.PARTIALLY_LOADED;
1132        }
1133//        public boolean mFlagFavorite = false;
1134//        public boolean mFlagAttachment = false;
1135//        public int mFlags = 0;
1136//
1137//        public String mTextInfo;
1138//        public String mHtmlInfo;
1139//
1140        localMessage.mServerId = message.getUid();
1141//        public int mServerIntId;
1142//        public String mClientId;
1143//        public String mMessageId;
1144//        public String mThreadId;
1145//
1146//        public long mBodyKey;
1147        localMessage.mMailboxKey = mailboxId;
1148        localMessage.mAccountKey = accountId;
1149//        public long mReferenceKey;
1150//
1151//        public String mSender;
1152        if (from != null && from.length > 0) {
1153            localMessage.mFrom = Address.pack(from);
1154        }
1155
1156        if (to != null && to.length > 0) {
1157            localMessage.mTo = Address.pack(to);
1158        }
1159        if (cc != null && cc.length > 0) {
1160            localMessage.mCc = Address.pack(cc);
1161        }
1162        if (bcc != null && bcc.length > 0) {
1163            localMessage.mBcc = Address.pack(bcc);
1164        }
1165        if (replyTo != null && replyTo.length > 0) {
1166            localMessage.mReplyTo = Address.pack(replyTo);
1167        }
1168//
1169//        public String mServerVersion;
1170//
1171//        public String mText;
1172//        public String mHtml;
1173//
1174//        // Can be used while building messages, but is NOT saved by the Provider
1175//        transient public ArrayList<Attachment> mAttachments = null;
1176//
1177//        public static final int UNREAD = 0;
1178//        public static final int READ = 1;
1179//        public static final int DELETED = 2;
1180//
1181//        public static final int NOT_LOADED = 0;
1182//        public static final int LOADED = 1;
1183//        public static final int PARTIALLY_LOADED = 2;
1184
1185        return true;
1186    }
1187
1188    /**
1189     * Copy body text (plain and/or HTML) from MimeMessage to provider Message
1190     */
1191    /* package */ boolean updateBodyFields(EmailContent.Body body,
1192            EmailContent.Message localMessage, Message message) throws MessagingException {
1193
1194        body.mMessageKey = localMessage.mId;
1195
1196        Part htmlPart = MimeUtility.findFirstPartByMimeType(message, "text/html");
1197        Part textPart = MimeUtility.findFirstPartByMimeType(message, "text/plain");
1198
1199        if (textPart != null) {
1200            String text = MimeUtility.getTextFromPart(textPart);
1201            if (text != null) {
1202                localMessage.mTextInfo = "X;X;8;" + text.length()*2;
1203                body.mTextContent = text;
1204            }
1205        }
1206        if (htmlPart != null) {
1207            String html = MimeUtility.getTextFromPart(htmlPart);
1208            if (html != null) {
1209                localMessage.mHtmlInfo = "X;X;8;" + html.length()*2;
1210                body.mHtmlContent = html;
1211            }
1212        }
1213        return true;
1214    }
1215
1216    private void queuePendingCommand(EmailContent.Account account, PendingCommand command) {
1217        try {
1218            LocalStore localStore = (LocalStore) Store.getInstance(
1219                    account.getLocalStoreUri(mContext), mContext, null);
1220            localStore.addPendingCommand(command);
1221        }
1222        catch (Exception e) {
1223            throw new RuntimeException("Unable to enqueue pending command", e);
1224        }
1225    }
1226
1227    private void processPendingCommands(final EmailContent.Account account) {
1228        put("processPendingCommands", null, new Runnable() {
1229            public void run() {
1230                try {
1231                    processPendingCommandsSynchronous(account);
1232                }
1233                catch (MessagingException me) {
1234                    if (Config.LOGV) {
1235                        Log.v(Email.LOG_TAG, "processPendingCommands", me);
1236                    }
1237                    /*
1238                     * Ignore any exceptions from the commands. Commands will be processed
1239                     * on the next round.
1240                     */
1241                }
1242            }
1243        });
1244    }
1245
1246    private void processPendingCommandsSynchronous(EmailContent.Account account)
1247            throws MessagingException {
1248        LocalStore localStore = (LocalStore) Store.getInstance(
1249                account.getLocalStoreUri(mContext), mContext, null);
1250        ArrayList<PendingCommand> commands = localStore.getPendingCommands();
1251        for (PendingCommand command : commands) {
1252            /*
1253             * We specifically do not catch any exceptions here. If a command fails it is
1254             * most likely due to a server or IO error and it must be retried before any
1255             * other command processes. This maintains the order of the commands.
1256             */
1257            if (PENDING_COMMAND_APPEND.equals(command.command)) {
1258                processPendingAppend(command, account);
1259            }
1260            else if (PENDING_COMMAND_MARK_READ.equals(command.command)) {
1261                processPendingMarkRead(command, account);
1262            }
1263            else if (PENDING_COMMAND_TRASH.equals(command.command)) {
1264                processPendingTrash(command, account);
1265            }
1266            localStore.removePendingCommand(command);
1267        }
1268    }
1269
1270    /**
1271     * Process a pending append message command. This command uploads a local message to the
1272     * server, first checking to be sure that the server message is not newer than
1273     * the local message. Once the local message is successfully processed it is deleted so
1274     * that the server message will be synchronized down without an additional copy being
1275     * created.
1276     * TODO update the local message UID instead of deleteing it
1277     *
1278     * @param command arguments = (String folder, String uid)
1279     * @param account
1280     * @throws MessagingException
1281     */
1282    private void processPendingAppend(PendingCommand command, EmailContent.Account account)
1283            throws MessagingException {
1284        String folder = command.arguments[0];
1285        String uid = command.arguments[1];
1286
1287        LocalStore localStore = (LocalStore) Store.getInstance(
1288                account.getLocalStoreUri(mContext), mContext, null);
1289        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1290        LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid);
1291
1292        if (localMessage == null) {
1293            return;
1294        }
1295
1296        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1297                localStore.getPersistentCallbacks());
1298        Folder remoteFolder = remoteStore.getFolder(folder);
1299        if (!remoteFolder.exists()) {
1300            if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
1301                return;
1302            }
1303        }
1304        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1305        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1306            return;
1307        }
1308
1309        Message remoteMessage = null;
1310        if (!localMessage.getUid().startsWith("Local")
1311                && !localMessage.getUid().contains("-")) {
1312            remoteMessage = remoteFolder.getMessage(localMessage.getUid());
1313        }
1314
1315        if (remoteMessage == null) {
1316            /*
1317             * If the message does not exist remotely we just upload it and then
1318             * update our local copy with the new uid.
1319             */
1320            FetchProfile fp = new FetchProfile();
1321            fp.add(FetchProfile.Item.BODY);
1322            localFolder.fetch(new Message[] { localMessage }, fp, null);
1323            String oldUid = localMessage.getUid();
1324            remoteFolder.appendMessages(new Message[] { localMessage });
1325            localFolder.changeUid(localMessage);
1326            mListeners.messageUidChanged(account, folder, oldUid, localMessage.getUid());
1327        }
1328        else {
1329            /*
1330             * If the remote message exists we need to determine which copy to keep.
1331             */
1332            /*
1333             * See if the remote message is newer than ours.
1334             */
1335            FetchProfile fp = new FetchProfile();
1336            fp.add(FetchProfile.Item.ENVELOPE);
1337            remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1338            Date localDate = localMessage.getInternalDate();
1339            Date remoteDate = remoteMessage.getInternalDate();
1340            if (remoteDate.compareTo(localDate) > 0) {
1341                /*
1342                 * If the remote message is newer than ours we'll just
1343                 * delete ours and move on. A sync will get the server message
1344                 * if we need to be able to see it.
1345                 */
1346                localMessage.setFlag(Flag.DELETED, true);
1347            }
1348            else {
1349                /*
1350                 * Otherwise we'll upload our message and then delete the remote message.
1351                 */
1352                fp.clear();
1353                fp = new FetchProfile();
1354                fp.add(FetchProfile.Item.BODY);
1355                localFolder.fetch(new Message[] { localMessage }, fp, null);
1356                String oldUid = localMessage.getUid();
1357                remoteFolder.appendMessages(new Message[] { localMessage });
1358                localFolder.changeUid(localMessage);
1359                mListeners.messageUidChanged(account, folder, oldUid, localMessage.getUid());
1360                remoteMessage.setFlag(Flag.DELETED, true);
1361            }
1362        }
1363    }
1364
1365    /**
1366     * Process a pending trash message command.
1367     *
1368     * @param command arguments = (String folder, String uid)
1369     * @param account
1370     * @throws MessagingException
1371     */
1372    private void processPendingTrash(PendingCommand command, final EmailContent.Account account)
1373            throws MessagingException {
1374        String folder = command.arguments[0];
1375        String uid = command.arguments[1];
1376
1377        final LocalStore localStore = (LocalStore) Store.getInstance(
1378                account.getLocalStoreUri(mContext), mContext, null);
1379        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1380
1381        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1382                localStore.getPersistentCallbacks());
1383        Folder remoteFolder = remoteStore.getFolder(folder);
1384        if (!remoteFolder.exists()) {
1385            return;
1386        }
1387        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1388        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1389            remoteFolder.close(false);
1390            return;
1391        }
1392
1393        Message remoteMessage = null;
1394        if (!uid.startsWith("Local")) {
1395            remoteMessage = remoteFolder.getMessage(uid);
1396        }
1397        if (remoteMessage == null) {
1398            remoteFolder.close(false);
1399            return;
1400        }
1401
1402        Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName(mContext));
1403        /*
1404         * Attempt to copy the remote message to the remote trash folder.
1405         */
1406        if (!remoteTrashFolder.exists()) {
1407            /*
1408             * If the remote trash folder doesn't exist we try to create it.
1409             */
1410            remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
1411        }
1412
1413        if (remoteTrashFolder.exists()) {
1414            /*
1415             * Because remoteTrashFolder may be new, we need to explicitly open it
1416             * and pass in the persistence callbacks.
1417             */
1418            final LocalFolder localTrashFolder =
1419                (LocalFolder) localStore.getFolder(account.getTrashFolderName(mContext));
1420            remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks());
1421            if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1422                remoteFolder.close(false);
1423                remoteTrashFolder.close(false);
1424                return;
1425            }
1426
1427            remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
1428                    new Folder.MessageUpdateCallbacks() {
1429                public void onMessageUidChange(Message message, String newUid)
1430                        throws MessagingException {
1431                    // update the UID in the local trash folder, because some stores will
1432                    // have to change it when copying to remoteTrashFolder
1433                    LocalMessage localMessage =
1434                        (LocalMessage) localTrashFolder.getMessage(message.getUid());
1435                    if(localMessage != null) {
1436                        localMessage.setUid(newUid);
1437                        localTrashFolder.updateMessage(localMessage);
1438                    }
1439                }
1440
1441                /**
1442                 * This will be called if the deleted message doesn't exist and can't be
1443                 * deleted (e.g. it was already deleted from the server.)  In this case,
1444                 * attempt to delete the local copy as well.
1445                 */
1446                public void onMessageNotFound(Message message) throws MessagingException {
1447                    LocalMessage localMessage =
1448                        (LocalMessage) localTrashFolder.getMessage(message.getUid());
1449                    if (localMessage != null) {
1450                        localMessage.setFlag(Flag.DELETED, true);
1451                    }
1452                }
1453
1454            }
1455            );
1456            remoteTrashFolder.close(false);
1457        }
1458
1459        remoteMessage.setFlag(Flag.DELETED, true);
1460        remoteFolder.expunge();
1461        remoteFolder.close(false);
1462    }
1463
1464    /**
1465     * Processes a pending mark read or unread command.
1466     *
1467     * @param command arguments = (String folder, String uid, boolean read)
1468     * @param account
1469     */
1470    private void processPendingMarkRead(PendingCommand command, EmailContent.Account account)
1471            throws MessagingException {
1472        String folder = command.arguments[0];
1473        String uid = command.arguments[1];
1474        boolean read = Boolean.parseBoolean(command.arguments[2]);
1475
1476        LocalStore localStore = (LocalStore) Store.getInstance(
1477                account.getLocalStoreUri(mContext), mContext, null);
1478        LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1479
1480        Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1481                localStore.getPersistentCallbacks());
1482        Folder remoteFolder = remoteStore.getFolder(folder);
1483        if (!remoteFolder.exists()) {
1484            return;
1485        }
1486        remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1487        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1488            return;
1489        }
1490        Message remoteMessage = null;
1491        if (!uid.startsWith("Local")
1492                && !uid.contains("-")) {
1493            remoteMessage = remoteFolder.getMessage(uid);
1494        }
1495        if (remoteMessage == null) {
1496            return;
1497        }
1498        remoteMessage.setFlag(Flag.SEEN, read);
1499    }
1500
1501    /**
1502     * Mark the message with the given account, folder and uid either Seen or not Seen.
1503     * @param account
1504     * @param folder
1505     * @param uid
1506     * @param seen
1507     */
1508    public void markMessageRead(
1509            final EmailContent.Account account,
1510            final String folder,
1511            final String uid,
1512            final boolean seen) {
1513        try {
1514            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1515                    null);
1516            Folder localFolder = localStore.getFolder(folder);
1517            localFolder.open(OpenMode.READ_WRITE, null);
1518
1519            Message message = localFolder.getMessage(uid);
1520            message.setFlag(Flag.SEEN, seen);
1521            PendingCommand command = new PendingCommand();
1522            command.command = PENDING_COMMAND_MARK_READ;
1523            command.arguments = new String[] { folder, uid, Boolean.toString(seen) };
1524            queuePendingCommand(account, command);
1525            processPendingCommands(account);
1526        }
1527        catch (MessagingException me) {
1528            throw new RuntimeException(me);
1529        }
1530    }
1531
1532    private void loadMessageForViewRemote(final EmailContent.Account account, final String folder,
1533            final String uid, MessagingListener listener) {
1534        put("loadMessageForViewRemote", listener, new Runnable() {
1535            public void run() {
1536                try {
1537                    LocalStore localStore = (LocalStore) Store.getInstance(
1538                            account.getLocalStoreUri(mContext), mContext, null);
1539                    LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1540                    localFolder.open(OpenMode.READ_WRITE, null);
1541
1542                    Message message = localFolder.getMessage(uid);
1543
1544                    if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
1545                        /*
1546                         * If the message has been synchronized since we were called we'll
1547                         * just hand it back cause it's ready to go.
1548                         */
1549                        FetchProfile fp = new FetchProfile();
1550                        fp.add(FetchProfile.Item.ENVELOPE);
1551                        fp.add(FetchProfile.Item.BODY);
1552                        localFolder.fetch(new Message[] { message }, fp, null);
1553
1554                        mListeners.loadMessageForViewBodyAvailable(account, folder, uid, message);
1555                        mListeners.loadMessageForViewFinished(account, folder, uid, message);
1556                        localFolder.close(false);
1557                        return;
1558                    }
1559
1560                    /*
1561                     * At this point the message is not available, so we need to download it
1562                     * fully if possible.
1563                     */
1564
1565                    Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1566                            localStore.getPersistentCallbacks());
1567                    Folder remoteFolder = remoteStore.getFolder(folder);
1568                    remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1569
1570                    // Get the remote message and fully download it (and save into local store)
1571
1572                    if (remoteStore.requireStructurePrefetch()) {
1573                        // For remote stores that require it, prefetch the message structure.
1574                        FetchProfile fp = new FetchProfile();
1575                        fp.add(FetchProfile.Item.STRUCTURE);
1576                        localFolder.fetch(new Message[] { message }, fp, null);
1577
1578                        ArrayList<Part> viewables = new ArrayList<Part>();
1579                        ArrayList<Part> attachments = new ArrayList<Part>();
1580                        MimeUtility.collectParts(message, viewables, attachments);
1581                        fp.clear();
1582                        for (Part part : viewables) {
1583                            fp.add(part);
1584                        }
1585
1586                        remoteFolder.fetch(new Message[] { message }, fp, null);
1587
1588                        // Store the updated message locally
1589                        localFolder.updateMessage((LocalMessage)message);
1590
1591                    } else {
1592                        // Most remote stores can directly obtain the message using only uid
1593                        Message remoteMessage = remoteFolder.getMessage(uid);
1594                        FetchProfile fp = new FetchProfile();
1595                        fp.add(FetchProfile.Item.BODY);
1596                        remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1597
1598                        // Store the message locally
1599                        localFolder.appendMessages(new Message[] { remoteMessage });
1600                    }
1601
1602                    // Now obtain the local copy for further access & manipulation
1603                    message = localFolder.getMessage(uid);
1604                    FetchProfile fp = new FetchProfile();
1605                    fp.add(FetchProfile.Item.BODY);
1606                    localFolder.fetch(new Message[] { message }, fp, null);
1607
1608                    // This is a view message request, so mark it read
1609                    if (!message.isSet(Flag.SEEN)) {
1610                        markMessageRead(account, folder, uid, true);
1611                    }
1612
1613                    // Mark that this message is now fully synched
1614                    message.setFlag(Flag.X_DOWNLOADED_FULL, true);
1615
1616                    mListeners.loadMessageForViewBodyAvailable(account, folder, uid, message);
1617                    mListeners.loadMessageForViewFinished(account, folder, uid, message);
1618                    remoteFolder.close(false);
1619                    localFolder.close(false);
1620                }
1621                catch (Exception e) {
1622                    mListeners.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1623                }
1624            }
1625        });
1626    }
1627
1628    public void loadMessageForView(final EmailContent.Account account, final String folder,
1629            final String uid, MessagingListener listener) {
1630        mListeners.loadMessageForViewStarted(account, folder, uid);
1631        try {
1632            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1633                    null);
1634            LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
1635            localFolder.open(OpenMode.READ_WRITE, null);
1636
1637            Message message = localFolder.getMessage(uid);
1638            mListeners.loadMessageForViewHeadersAvailable(account, folder, uid, message);
1639            if (!message.isSet(Flag.X_DOWNLOADED_FULL)) {
1640                loadMessageForViewRemote(account, folder, uid, listener);
1641                localFolder.close(false);
1642                return;
1643            }
1644
1645            if (!message.isSet(Flag.SEEN)) {
1646                markMessageRead(account, folder, uid, true);
1647            }
1648
1649            FetchProfile fp = new FetchProfile();
1650            fp.add(FetchProfile.Item.ENVELOPE);
1651            fp.add(FetchProfile.Item.BODY);
1652            localFolder.fetch(new Message[] {
1653                message
1654            }, fp, null);
1655
1656            mListeners.loadMessageForViewBodyAvailable(account, folder, uid, message);
1657            mListeners.loadMessageForViewFinished(account, folder, uid, message);
1658            localFolder.close(false);
1659        }
1660        catch (Exception e) {
1661            mListeners.loadMessageForViewFailed(account, folder, uid, e.getMessage());
1662        }
1663    }
1664
1665    /**
1666     * Attempts to load the attachment specified by part from the given account and message.
1667     * @param account
1668     * @param message
1669     * @param part
1670     * @param listener
1671     */
1672    public void loadAttachment(
1673            final EmailContent.Account account,
1674            final Message message,
1675            final Part part,
1676            final Object tag,
1677            MessagingListener listener) {
1678        /*
1679         * Check if the attachment has already been downloaded. If it has there's no reason to
1680         * download it, so we just tell the listener that it's ready to go.
1681         */
1682        try {
1683            if (part.getBody() != null) {
1684                mListeners.loadAttachmentStarted(account, message, part, tag, false);
1685                mListeners.loadAttachmentFinished(account, message, part, tag);
1686                return;
1687            }
1688        }
1689        catch (MessagingException me) {
1690            /*
1691             * If the header isn't there the attachment isn't downloaded yet, so just continue
1692             * on.
1693             */
1694        }
1695
1696        mListeners.loadAttachmentStarted(account, message, part, tag, true);
1697
1698        put("loadAttachment", listener, new Runnable() {
1699            public void run() {
1700                try {
1701                    LocalStore localStore = (LocalStore) Store.getInstance(
1702                            account.getLocalStoreUri(mContext), mContext, null);
1703                    /*
1704                     * We clear out any attachments already cached in the entire store and then
1705                     * we update the passed in message to reflect that there are no cached
1706                     * attachments. This is in support of limiting the account to having one
1707                     * attachment downloaded at a time.
1708                     */
1709                    localStore.pruneCachedAttachments();
1710                    ArrayList<Part> viewables = new ArrayList<Part>();
1711                    ArrayList<Part> attachments = new ArrayList<Part>();
1712                    MimeUtility.collectParts(message, viewables, attachments);
1713                    for (Part attachment : attachments) {
1714                        attachment.setBody(null);
1715                    }
1716                    Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
1717                            localStore.getPersistentCallbacks());
1718                    LocalFolder localFolder =
1719                        (LocalFolder) localStore.getFolder(message.getFolder().getName());
1720                    Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName());
1721                    remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
1722
1723                    FetchProfile fp = new FetchProfile();
1724                    fp.add(part);
1725                    remoteFolder.fetch(new Message[] { message }, fp, null);
1726                    localFolder.updateMessage((LocalMessage)message);
1727                    localFolder.close(false);
1728                    mListeners.loadAttachmentFinished(account, message, part, tag);
1729                }
1730                catch (MessagingException me) {
1731                    if (Config.LOGV) {
1732                        Log.v(Email.LOG_TAG, "", me);
1733                    }
1734                    mListeners.loadAttachmentFailed(account, message, part, tag, me.getMessage());
1735                }
1736            }
1737        });
1738    }
1739
1740    /**
1741     * Stores the given message in the Outbox and starts a sendPendingMessages command to
1742     * attempt to send the message.
1743     * @param account
1744     * @param message
1745     * @param listener
1746     */
1747    public void sendMessage(final EmailContent.Account account, final Message message,
1748            MessagingListener listener) {
1749        try {
1750            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1751                    null);
1752            LocalFolder localFolder =
1753                (LocalFolder) localStore.getFolder(account.getOutboxFolderName(mContext));
1754            localFolder.open(OpenMode.READ_WRITE, null);
1755            localFolder.appendMessages(new Message[] {
1756                message
1757            });
1758            Message localMessage = localFolder.getMessage(message.getUid());
1759            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1760            localFolder.close(false);
1761            sendPendingMessages(account, null);
1762        }
1763        catch (Exception e) {
1764//            synchronized (mListeners) {
1765//                for (MessagingListener l : mListeners) {
1766//                    // TODO general failed
1767//                }
1768//            }
1769        }
1770    }
1771
1772    /**
1773     * Attempt to send any messages that are sitting in the Outbox.
1774     * @param account
1775     * @param listener
1776     */
1777    public void sendPendingMessages(final EmailContent.Account account, MessagingListener listener) {
1778        put("sendPendingMessages", listener, new Runnable() {
1779            public void run() {
1780                sendPendingMessagesSynchronous(account);
1781            }
1782        });
1783    }
1784
1785    /**
1786     * Attempt to send any messages that are sitting in the Outbox.
1787     * @param account
1788     * @param listener
1789     */
1790    public void sendPendingMessagesSynchronous(final EmailContent.Account account) {
1791        try {
1792            LocalStore localStore = (LocalStore) Store.getInstance(
1793                    account.getLocalStoreUri(mContext), mContext, null);
1794            Folder localFolder = localStore.getFolder(account.getOutboxFolderName(mContext));
1795            if (!localFolder.exists()) {
1796                return;
1797            }
1798            localFolder.open(OpenMode.READ_WRITE, null);
1799
1800            Message[] localMessages = localFolder.getMessages(null);
1801
1802            /*
1803             * The profile we will use to pull all of the content
1804             * for a given local message into memory for sending.
1805             */
1806            FetchProfile fp = new FetchProfile();
1807            fp.add(FetchProfile.Item.ENVELOPE);
1808            fp.add(FetchProfile.Item.BODY);
1809
1810            LocalFolder localSentFolder =
1811                (LocalFolder) localStore.getFolder(account.getSentFolderName(mContext));
1812
1813            // Determine if upload to "sent" folder is necessary
1814            Store remoteStore = Store.getInstance(
1815                    account.getStoreUri(mContext), mContext, localStore.getPersistentCallbacks());
1816            boolean requireCopyMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
1817
1818            Sender sender = Sender.getInstance(account.getSenderUri(mContext), mContext);
1819            for (Message message : localMessages) {
1820                try {
1821                    localFolder.fetch(new Message[] { message }, fp, null);
1822                    try {
1823                        // Send message using Sender
1824                        message.setFlag(Flag.X_SEND_IN_PROGRESS, true);
1825                        sender.sendMessage(message);
1826                        message.setFlag(Flag.X_SEND_IN_PROGRESS, false);
1827
1828                        // Upload to "sent" folder if not supported server-side
1829                        if (requireCopyMessageToSentFolder) {
1830                            localFolder.copyMessages(
1831                                    new Message[] { message },localSentFolder, null);
1832                            PendingCommand command = new PendingCommand();
1833                            command.command = PENDING_COMMAND_APPEND;
1834                            command.arguments =
1835                                new String[] { localSentFolder.getName(), message.getUid() };
1836                            queuePendingCommand(account, command);
1837                            processPendingCommands(account);
1838                        }
1839
1840                        // And delete from outbox
1841                        message.setFlag(Flag.X_DESTROYED, true);
1842                    }
1843                    catch (Exception e) {
1844                        message.setFlag(Flag.X_SEND_FAILED, true);
1845                        mListeners.sendPendingMessageFailed(account, message, e);
1846                    }
1847                }
1848                catch (Exception e) {
1849                    mListeners.sendPendingMessageFailed(account, message, e);
1850                }
1851            }
1852            localFolder.expunge();
1853            mListeners.sendPendingMessagesCompleted(account);
1854        }
1855        catch (Exception e) {
1856            mListeners.sendPendingMessagesFailed(account, e);
1857        }
1858    }
1859
1860    /**
1861     * We do the local portion of this synchronously because other activities may have to make
1862     * updates based on what happens here
1863     * @param account
1864     * @param folder
1865     * @param message
1866     * @param listener
1867     */
1868    public void deleteMessage(final EmailContent.Account account, final String folder,
1869            final Message message, MessagingListener listener) {
1870        if (folder.equals(account.getTrashFolderName(mContext))) {
1871            return;
1872        }
1873        try {
1874            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1875                    null);
1876            Folder localFolder = localStore.getFolder(folder);
1877            Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName(mContext));
1878
1879            localFolder.copyMessages(new Message[] { message }, localTrashFolder, null);
1880            message.setFlag(Flag.DELETED, true);
1881
1882            if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) {
1883                PendingCommand command = new PendingCommand();
1884                command.command = PENDING_COMMAND_TRASH;
1885                command.arguments = new String[] { folder, message.getUid() };
1886                queuePendingCommand(account, command);
1887                processPendingCommands(account);
1888            }
1889        }
1890        catch (MessagingException me) {
1891            throw new RuntimeException("Error deleting message from local store.", me);
1892        }
1893    }
1894
1895    public void emptyTrash(final EmailContent.Account account, MessagingListener listener) {
1896        put("emptyTrash", listener, new Runnable() {
1897            public void run() {
1898                // TODO IMAP
1899                try {
1900                    Store localStore = Store.getInstance(
1901                            account.getLocalStoreUri(mContext), mContext, null);
1902                    Folder localFolder = localStore.getFolder(account.getTrashFolderName(mContext));
1903                    localFolder.open(OpenMode.READ_WRITE, null);
1904                    Message[] messages = localFolder.getMessages(null);
1905                    localFolder.setFlags(messages, new Flag[] {
1906                        Flag.DELETED
1907                    }, true);
1908                    localFolder.close(true);
1909                    mListeners.emptyTrashCompleted(account);
1910                }
1911                catch (Exception e) {
1912                    // TODO
1913                    if (Config.LOGV) {
1914                        Log.v(Email.LOG_TAG, "emptyTrash");
1915                    }
1916                }
1917            }
1918        });
1919    }
1920
1921    /**
1922     * Checks mail for one or multiple accounts. If account is null all accounts
1923     * are checked.
1924     *
1925     * TODO:  There is no use case for "check all accounts".  Clean up this API to remove
1926     * that case.  Callers can supply the appropriate list.
1927     *
1928     * TODO:  Better protection against a failure in account n, which should not prevent
1929     * syncing account in accounts n+1 and beyond.
1930     *
1931     * @param context
1932     * @param accounts List of accounts to check, or null to check all accounts
1933     * @param listener
1934     */
1935    public void checkMail(final Context context, EmailContent.Account[] accounts,
1936            final MessagingListener listener) {
1937        /**
1938         * Note:  The somewhat tortured logic here is to guarantee proper ordering of events:
1939         *      listeners: checkMailStarted
1940         *      account 1: list folders
1941         *      account 1: sync messages
1942         *      account 2: list folders
1943         *      account 2: sync messages
1944         *      ...
1945         *      account n: list folders
1946         *      account n: sync messages
1947         *      listeners: checkMailFinished
1948         */
1949        mListeners.checkMailStarted(context, null); // TODO this needs to pass the actual array
1950        if (accounts == null) {
1951            // TODO eliminate this use case, implement, or ...?
1952//            accounts = Preferences.getPreferences(context).getAccounts();
1953        }
1954        for (final EmailContent.Account account : accounts) {
1955            listFolders(account, null);
1956
1957            put("checkMail", listener, new Runnable() {
1958                public void run() {
1959                    sendPendingMessagesSynchronous(account);
1960                    // TODO find mailbox # for inbox and sync it.
1961//                    synchronizeMailboxSynchronous(account, Email.INBOX);
1962                }
1963            });
1964        }
1965        put("checkMailFinished", listener, new Runnable() {
1966            public void run() {
1967                mListeners.checkMailFinished(context, null); // TODO this needs to pass actual array
1968            }
1969        });
1970    }
1971
1972    public void saveDraft(final EmailContent.Account account, final Message message) {
1973        try {
1974            Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext,
1975                    null);
1976            LocalFolder localFolder =
1977                (LocalFolder) localStore.getFolder(account.getDraftsFolderName(mContext));
1978            localFolder.open(OpenMode.READ_WRITE, null);
1979            localFolder.appendMessages(new Message[] {
1980                message
1981            });
1982            Message localMessage = localFolder.getMessage(message.getUid());
1983            localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true);
1984
1985            PendingCommand command = new PendingCommand();
1986            command.command = PENDING_COMMAND_APPEND;
1987            command.arguments = new String[] {
1988                    localFolder.getName(),
1989                    localMessage.getUid() };
1990            queuePendingCommand(account, command);
1991            processPendingCommands(account);
1992        }
1993        catch (MessagingException e) {
1994            Log.e(Email.LOG_TAG, "Unable to save message as draft.", e);
1995        }
1996    }
1997
1998    private static class Command {
1999        public Runnable runnable;
2000
2001        public MessagingListener listener;
2002
2003        public String description;
2004
2005        @Override
2006        public String toString() {
2007            return description;
2008        }
2009    }
2010}
2011