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