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