MessagingController.java revision c84467afe1b5e0a657ed7d6a9fa1e3fe1ff259a0
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 android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.net.TrafficStats;
25import android.net.Uri;
26import android.os.Process;
27import android.text.TextUtils;
28import android.util.Log;
29
30import com.android.email.mail.Sender;
31import com.android.email.mail.Store;
32import com.android.emailcommon.Logging;
33import com.android.emailcommon.TrafficFlags;
34import com.android.emailcommon.internet.MimeBodyPart;
35import com.android.emailcommon.internet.MimeHeader;
36import com.android.emailcommon.internet.MimeMultipart;
37import com.android.emailcommon.internet.MimeUtility;
38import com.android.emailcommon.mail.AuthenticationFailedException;
39import com.android.emailcommon.mail.FetchProfile;
40import com.android.emailcommon.mail.Flag;
41import com.android.emailcommon.mail.Folder;
42import com.android.emailcommon.mail.Folder.FolderType;
43import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
44import com.android.emailcommon.mail.Folder.OpenMode;
45import com.android.emailcommon.mail.Message;
46import com.android.emailcommon.mail.MessagingException;
47import com.android.emailcommon.mail.Part;
48import com.android.emailcommon.provider.Account;
49import com.android.emailcommon.provider.EmailContent;
50import com.android.emailcommon.provider.EmailContent.Attachment;
51import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
52import com.android.emailcommon.provider.EmailContent.MailboxColumns;
53import com.android.emailcommon.provider.EmailContent.MessageColumns;
54import com.android.emailcommon.provider.EmailContent.SyncColumns;
55import com.android.emailcommon.provider.Mailbox;
56import com.android.emailcommon.utility.AttachmentUtilities;
57import com.android.emailcommon.utility.ConversionUtilities;
58import com.android.emailcommon.utility.Utility;
59
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    /**
83     * The maximum message size that we'll consider to be "small". A small message is downloaded
84     * in full immediately instead of in pieces. Anything over this size will be downloaded in
85     * pieces with attachments being left off completely and downloaded on demand.
86     *
87     *
88     * 25k for a "small" message was picked by educated trial and error.
89     * http://answers.google.com/answers/threadview?id=312463 claims that the
90     * average size of an email is 59k, which I feel is too large for our
91     * blind download. The following tests were performed on a download of
92     * 25 random messages.
93     * <pre>
94     * 5k - 61 seconds,
95     * 25k - 51 seconds,
96     * 55k - 53 seconds,
97     * </pre>
98     * So 25k gives good performance and a reasonable data footprint. Sounds good to me.
99     */
100    private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
101
102    /**
103     * We write this into the serverId field of messages that will never be upsynced.
104     */
105    private static final String LOCAL_SERVERID_PREFIX = "Local-";
106
107    private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues();
108    static {
109        PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI);
110    }
111
112    private static MessagingController sInstance = null;
113    private final BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
114    private final Thread mThread;
115
116    /**
117     * All access to mListeners *must* be synchronized
118     */
119    private final GroupMessagingListener mListeners = new GroupMessagingListener();
120    private boolean mBusy;
121    private final Context mContext;
122    private final Controller mController;
123
124    /**
125     * Simple cache for last search result mailbox by account and serverId, since the most common
126     * case will be repeated use of the same mailbox
127     */
128    private long mLastSearchAccountKey = Account.NO_ACCOUNT;
129    private String mLastSearchServerId = null;
130    private Mailbox mLastSearchRemoteMailbox = null;
131
132    protected MessagingController(Context _context, Controller _controller) {
133        mContext = _context.getApplicationContext();
134        mController = _controller;
135        mThread = new Thread(this);
136        mThread.start();
137    }
138
139    /**
140     * Gets or creates the singleton instance of MessagingController. Application is used to
141     * provide a Context to classes that need it.
142     */
143    public synchronized static MessagingController getInstance(Context _context,
144            Controller _controller) {
145        if (sInstance == null) {
146            sInstance = new MessagingController(_context, _controller);
147        }
148        return sInstance;
149    }
150
151    /**
152     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
153     */
154    public static void injectMockController(MessagingController mockController) {
155        sInstance = mockController;
156    }
157
158    // TODO: seems that this reading of mBusy isn't thread-safe
159    public boolean isBusy() {
160        return mBusy;
161    }
162
163    @Override
164    public void run() {
165        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
166        // TODO: add an end test to this infinite loop
167        while (true) {
168            Command command;
169            try {
170                command = mCommands.take();
171            } catch (InterruptedException e) {
172                continue; //re-test the condition on the eclosing while
173            }
174            if (command.listener == null || isActiveListener(command.listener)) {
175                mBusy = true;
176                command.runnable.run();
177                mListeners.controllerCommandCompleted(mCommands.size() > 0);
178            }
179            mBusy = false;
180        }
181    }
182
183    private void put(String description, MessagingListener listener, Runnable runnable) {
184        try {
185            Command command = new Command();
186            command.listener = listener;
187            command.runnable = runnable;
188            command.description = description;
189            mCommands.add(command);
190        }
191        catch (IllegalStateException ie) {
192            throw new Error(ie);
193        }
194    }
195
196    public void addListener(MessagingListener listener) {
197        mListeners.addListener(listener);
198    }
199
200    public void removeListener(MessagingListener listener) {
201        mListeners.removeListener(listener);
202    }
203
204    private boolean isActiveListener(MessagingListener listener) {
205        return mListeners.isActiveListener(listener);
206    }
207
208    private static final int MAILBOX_COLUMN_ID = 0;
209    private static final int MAILBOX_COLUMN_SERVER_ID = 1;
210    private static final int MAILBOX_COLUMN_TYPE = 2;
211
212    /** Small projection for just the columns required for a sync. */
213    private static final String[] MAILBOX_PROJECTION = new String[] {
214        MailboxColumns.ID,
215        MailboxColumns.SERVER_ID,
216        MailboxColumns.TYPE,
217    };
218
219    /**
220     * Synchronize the folder list with the remote server. Synchronization occurs in the
221     * background and results are passed through the {@link MessagingListener}. If the
222     * given listener is not {@code null}, it must have been previously added to the set
223     * of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no
224     * actions will be performed.
225     *
226     * TODO this needs to cache the remote folder list
227     * TODO break out an inner listFoldersSynchronized which could simplify checkMail
228     *
229     * @param accountId ID of the account for which to list the folders
230     * @param listener A listener to notify
231     */
232    void listFolders(final long accountId, MessagingListener listener) {
233        final Account account = Account.restoreAccountWithId(mContext, accountId);
234        if (account == null) {
235            Log.i(Logging.LOG_TAG, "Could not load account id " + accountId
236                    + ". Has it been removed?");
237            return;
238        }
239        mListeners.listFoldersStarted(accountId);
240        put("listFolders", listener, new Runnable() {
241            // TODO For now, mailbox addition occurs in the server-dependent store implementation,
242            // but, mailbox removal occurs here. Instead, each store should be responsible for
243            // content synchronization (addition AND removal) since each store will likely need
244            // to implement it's own, unique synchronization methodology.
245            @Override
246            public void run() {
247                TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
248                Cursor localFolderCursor = null;
249                try {
250                    // Step 1: Get remote mailboxes
251                    Store store = Store.getInstance(account, mContext);
252                    Folder[] remoteFolders = store.updateFolders();
253                    HashSet<String> remoteFolderNames = new HashSet<String>();
254                    for (int i = 0, count = remoteFolders.length; i < count; i++) {
255                        remoteFolderNames.add(remoteFolders[i].getName());
256                    }
257
258                    // Step 2: Get local mailboxes
259                    localFolderCursor = mContext.getContentResolver().query(
260                            Mailbox.CONTENT_URI,
261                            MAILBOX_PROJECTION,
262                            EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
263                            new String[] { String.valueOf(account.mId) },
264                            null);
265
266                    // Step 3: Remove any local mailbox not on the remote list
267                    while (localFolderCursor.moveToNext()) {
268                        String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
269                        // Short circuit if we have a remote mailbox with the same name
270                        if (remoteFolderNames.contains(mailboxPath)) {
271                            continue;
272                        }
273
274                        int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
275                        long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
276                        switch (mailboxType) {
277                            case Mailbox.TYPE_INBOX:
278                            case Mailbox.TYPE_DRAFTS:
279                            case Mailbox.TYPE_OUTBOX:
280                            case Mailbox.TYPE_SENT:
281                            case Mailbox.TYPE_TRASH:
282                            case Mailbox.TYPE_SEARCH:
283                                // Never, ever delete special mailboxes
284                                break;
285                            default:
286                                // Drop all attachment files related to this mailbox
287                                AttachmentUtilities.deleteAllMailboxAttachmentFiles(
288                                        mContext, accountId, mailboxId);
289                                // Delete the mailbox; database triggers take care of related
290                                // Message, Body and Attachment records
291                                Uri uri = ContentUris.withAppendedId(
292                                        Mailbox.CONTENT_URI, mailboxId);
293                                mContext.getContentResolver().delete(uri, null, null);
294                                break;
295                        }
296                    }
297                    mListeners.listFoldersFinished(accountId);
298                } catch (Exception e) {
299                    mListeners.listFoldersFailed(accountId, e.toString());
300                } finally {
301                    if (localFolderCursor != null) {
302                        localFolderCursor.close();
303                    }
304                }
305            }
306        });
307    }
308
309    /**
310     * Start background synchronization of the specified folder.
311     * @param account
312     * @param folder
313     * @param listener
314     */
315    public void synchronizeMailbox(final Account account,
316            final Mailbox folder, MessagingListener listener) {
317        /*
318         * We don't ever sync the Outbox.
319         */
320        if (folder.mType == Mailbox.TYPE_OUTBOX) {
321            return;
322        }
323        mListeners.synchronizeMailboxStarted(account.mId, folder.mId);
324        put("synchronizeMailbox", listener, new Runnable() {
325            @Override
326            public void run() {
327                synchronizeMailboxSynchronous(account, folder);
328            }
329        });
330    }
331
332    /**
333     * Start foreground synchronization of the specified folder. This is called by
334     * synchronizeMailbox or checkMail.
335     * TODO this should use ID's instead of fully-restored objects
336     * @param account
337     * @param folder
338     */
339    private void synchronizeMailboxSynchronous(final Account account,
340            final Mailbox folder) {
341        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
342        mListeners.synchronizeMailboxStarted(account.mId, folder.mId);
343        if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
344            // We don't hold messages, so, nothing to synchronize
345            mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null);
346            return;
347        }
348        NotificationController nc = NotificationController.getInstance(mContext);
349        try {
350
351            // Select generic sync or store-specific sync
352            SyncResults results = synchronizeMailboxGeneric(account, folder);
353            // The account might have been deleted
354            if (results == null) return;
355            mListeners.synchronizeMailboxFinished(account.mId, folder.mId,
356                                                  results.mTotalMessages,
357                                                  results.mAddedMessages.size(),
358                                                  results.mAddedMessages);
359            // Clear authentication notification for this account
360            nc.cancelLoginFailedNotification(account.mId);
361        } catch (MessagingException e) {
362            if (Logging.LOGD) {
363                Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
364            }
365            if (e instanceof AuthenticationFailedException) {
366                // Generate authentication notification
367                nc.showLoginFailedNotification(account.mId);
368            }
369            mListeners.synchronizeMailboxFailed(account.mId, folder.mId, e);
370        }
371    }
372
373    /**
374     * Lightweight record for the first pass of message sync, where I'm just seeing if
375     * the local message requires sync.  Later (for messages that need syncing) we'll do a full
376     * readout from the DB.
377     */
378    private static class LocalMessageInfo {
379        private static final int COLUMN_ID = 0;
380        private static final int COLUMN_FLAG_READ = 1;
381        private static final int COLUMN_FLAG_FAVORITE = 2;
382        private static final int COLUMN_FLAG_LOADED = 3;
383        private static final int COLUMN_SERVER_ID = 4;
384        private static final int COLUMN_FLAGS =  7;
385        private static final String[] PROJECTION = new String[] {
386            EmailContent.RECORD_ID,
387            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
388            SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
389            MessageColumns.FLAGS
390        };
391
392        final long mId;
393        final boolean mFlagRead;
394        final boolean mFlagFavorite;
395        final int mFlagLoaded;
396        final String mServerId;
397        final int mFlags;
398
399        public LocalMessageInfo(Cursor c) {
400            mId = c.getLong(COLUMN_ID);
401            mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
402            mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
403            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
404            mServerId = c.getString(COLUMN_SERVER_ID);
405            mFlags = c.getInt(COLUMN_FLAGS);
406            // Note: mailbox key and account key not needed - they are projected for the SELECT
407        }
408    }
409
410    private void saveOrUpdate(EmailContent content, Context context) {
411        if (content.isSaved()) {
412            content.update(context, content.toContentValues());
413        } else {
414            content.save(context);
415        }
416    }
417
418    /**
419     * Load the structure and body of messages not yet synced
420     * @param account the account we're syncing
421     * @param remoteFolder the (open) Folder we're working on
422     * @param unsyncedMessages an array of Message's we've got headers for
423     * @param toMailbox the destination mailbox we're syncing
424     * @throws MessagingException
425     */
426    void loadUnsyncedMessages(final Account account, Folder remoteFolder,
427            ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)
428            throws MessagingException {
429
430        // 1. Divide the unsynced messages into small & large (by size)
431
432        // TODO doing this work here (synchronously) is problematic because it prevents the UI
433        // from affecting the order (e.g. download a message because the user requested it.)  Much
434        // of this logic should move out to a different sync loop that attempts to update small
435        // groups of messages at a time, as a background task.  However, we can't just return
436        // (yet) because POP messages don't have an envelope yet....
437
438        ArrayList<Message> largeMessages = new ArrayList<Message>();
439        ArrayList<Message> smallMessages = new ArrayList<Message>();
440        for (Message message : unsyncedMessages) {
441            if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
442                largeMessages.add(message);
443            } else {
444                smallMessages.add(message);
445            }
446        }
447
448        // 2. Download small messages
449
450        // TODO Problems with this implementation.  1. For IMAP, where we get a real envelope,
451        // this is going to be inefficient and duplicate work we've already done.  2.  It's going
452        // back to the DB for a local message that we already had (and discarded).
453
454        // For small messages, we specify "body", which returns everything (incl. attachments)
455        FetchProfile fp = new FetchProfile();
456        fp.add(FetchProfile.Item.BODY);
457        remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
458                new MessageRetrievalListener() {
459                    @Override
460                    public void messageRetrieved(Message message) {
461                        // Store the updated message locally and mark it fully loaded
462                        copyOneMessageToProvider(message, account, toMailbox,
463                                EmailContent.Message.FLAG_LOADED_COMPLETE);
464                    }
465
466                    @Override
467                    public void loadAttachmentProgress(int progress) {
468                    }
469        });
470
471        // 3. Download large messages.  We ask the server to give us the message structure,
472        // but not all of the attachments.
473        fp.clear();
474        fp.add(FetchProfile.Item.STRUCTURE);
475        remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
476        for (Message message : largeMessages) {
477            if (message.getBody() == null) {
478                // POP doesn't support STRUCTURE mode, so we'll just do a partial download
479                // (hopefully enough to see some/all of the body) and mark the message for
480                // further download.
481                fp.clear();
482                fp.add(FetchProfile.Item.BODY_SANE);
483                //  TODO a good optimization here would be to make sure that all Stores set
484                //  the proper size after this fetch and compare the before and after size. If
485                //  they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
486                remoteFolder.fetch(new Message[] { message }, fp, null);
487
488                // Store the partially-loaded message and mark it partially loaded
489                copyOneMessageToProvider(message, account, toMailbox,
490                        EmailContent.Message.FLAG_LOADED_PARTIAL);
491            } else {
492                // We have a structure to deal with, from which
493                // we can pull down the parts we want to actually store.
494                // Build a list of parts we are interested in. Text parts will be downloaded
495                // right now, attachments will be left for later.
496                ArrayList<Part> viewables = new ArrayList<Part>();
497                ArrayList<Part> attachments = new ArrayList<Part>();
498                MimeUtility.collectParts(message, viewables, attachments);
499                // Download the viewables immediately
500                for (Part part : viewables) {
501                    fp.clear();
502                    fp.add(part);
503                    // TODO what happens if the network connection dies? We've got partial
504                    // messages with incorrect status stored.
505                    remoteFolder.fetch(new Message[] { message }, fp, null);
506                }
507                // Store the updated message locally and mark it fully loaded
508                copyOneMessageToProvider(message, account, toMailbox,
509                        EmailContent.Message.FLAG_LOADED_COMPLETE);
510            }
511        }
512
513    }
514
515    public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox,
516            Folder remoteFolder, ArrayList<Message> unsyncedMessages,
517            HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
518            throws MessagingException {
519        FetchProfile fp = new FetchProfile();
520        fp.add(FetchProfile.Item.FLAGS);
521        fp.add(FetchProfile.Item.ENVELOPE);
522
523        final HashMap<String, LocalMessageInfo> localMapCopy;
524        if (localMessageMap != null)
525            localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
526        else {
527            localMapCopy = new HashMap<String, LocalMessageInfo>();
528        }
529
530        remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
531                new MessageRetrievalListener() {
532                    @Override
533                    public void messageRetrieved(Message message) {
534                        try {
535                            // Determine if the new message was already known (e.g. partial)
536                            // And create or reload the full message info
537                            LocalMessageInfo localMessageInfo =
538                                localMapCopy.get(message.getUid());
539                            EmailContent.Message localMessage = null;
540                            if (localMessageInfo == null) {
541                                localMessage = new EmailContent.Message();
542                            } else {
543                                localMessage = EmailContent.Message.restoreMessageWithId(
544                                        mContext, localMessageInfo.mId);
545                            }
546
547                            if (localMessage != null) {
548                                try {
549                                    // Copy the fields that are available into the message
550                                    LegacyConversions.updateMessageFields(localMessage,
551                                            message, account.mId, mailbox.mId);
552                                    // Commit the message to the local store
553                                    saveOrUpdate(localMessage, mContext);
554                                    // Track the "new" ness of the downloaded message
555                                    if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
556                                        unseenMessages.add(localMessage.mId);
557                                    }
558                                } catch (MessagingException me) {
559                                    Log.e(Logging.LOG_TAG,
560                                            "Error while copying downloaded message." + me);
561                                }
562
563                            }
564                        }
565                        catch (Exception e) {
566                            Log.e(Logging.LOG_TAG,
567                                    "Error while storing downloaded message." + e.toString());
568                        }
569                    }
570
571                    @Override
572                    public void loadAttachmentProgress(int progress) {
573                    }
574                });
575
576    }
577
578    /**
579     * Generic synchronizer - used for POP3 and IMAP.
580     *
581     * TODO Break this method up into smaller chunks.
582     *
583     * @param account the account to sync
584     * @param mailbox the mailbox to sync
585     * @return results of the sync pass
586     * @throws MessagingException
587     */
588    private SyncResults synchronizeMailboxGeneric(final Account account, final Mailbox mailbox)
589            throws MessagingException {
590
591        /*
592         * A list of IDs for messages that were downloaded and did not have the seen flag set.
593         * This serves as the "true" new message count reported to the user via notification.
594         */
595        final ArrayList<Long> unseenMessages = new ArrayList<Long>();
596
597        if (Email.DEBUG) {
598            Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***");
599        }
600        ContentResolver resolver = mContext.getContentResolver();
601
602        // 0.  We do not ever sync DRAFTS or OUTBOX (down or up)
603        if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
604            int totalMessages = EmailContent.count(mContext, mailbox.getUri(), null, null);
605            return new SyncResults(totalMessages, unseenMessages);
606        }
607
608        // 1.  Get the message list from the local store and create an index of the uids
609
610        Cursor localUidCursor = null;
611        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
612
613        try {
614            localUidCursor = resolver.query(
615                    EmailContent.Message.CONTENT_URI,
616                    LocalMessageInfo.PROJECTION,
617                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
618                    " AND " + MessageColumns.MAILBOX_KEY + "=?",
619                    new String[] {
620                            String.valueOf(account.mId),
621                            String.valueOf(mailbox.mId)
622                    },
623                    null);
624            while (localUidCursor.moveToNext()) {
625                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
626                localMessageMap.put(info.mServerId, info);
627            }
628        } finally {
629            if (localUidCursor != null) {
630                localUidCursor.close();
631            }
632        }
633
634        // 2.  Open the remote folder and create the remote folder if necessary
635
636        Store remoteStore = Store.getInstance(account, mContext);
637        // The account might have been deleted
638        if (remoteStore == null) return null;
639        Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
640
641        /*
642         * If the folder is a "special" folder we need to see if it exists
643         * on the remote server. It if does not exist we'll try to create it. If we
644         * can't create we'll abort. This will happen on every single Pop3 folder as
645         * designed and on Imap folders during error conditions. This allows us
646         * to treat Pop3 and Imap the same in this code.
647         */
648        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
649                || mailbox.mType == Mailbox.TYPE_DRAFTS) {
650            if (!remoteFolder.exists()) {
651                if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
652                    return new SyncResults(0, unseenMessages);
653                }
654            }
655        }
656
657        // 3, Open the remote folder. This pre-loads certain metadata like message count.
658        remoteFolder.open(OpenMode.READ_WRITE);
659
660        // 4. Trash any remote messages that are marked as trashed locally.
661        // TODO - this comment was here, but no code was here.
662
663        // 5. Get the remote message count.
664        int remoteMessageCount = remoteFolder.getMessageCount();
665
666        // 6. Determine the limit # of messages to download
667        int visibleLimit = mailbox.mVisibleLimit;
668        if (visibleLimit <= 0) {
669            visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
670        }
671
672        // 7.  Create a list of messages to download
673        Message[] remoteMessages = new Message[0];
674        final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
675        HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
676
677        int newMessageCount = 0;
678        if (remoteMessageCount > 0) {
679            /*
680             * Message numbers start at 1.
681             */
682            int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
683            int remoteEnd = remoteMessageCount;
684            remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
685            // TODO Why are we running through the list twice? Combine w/ for loop below
686            for (Message message : remoteMessages) {
687                remoteUidMap.put(message.getUid(), message);
688            }
689
690            /*
691             * Get a list of the messages that are in the remote list but not on the
692             * local store, or messages that are in the local store but failed to download
693             * on the last sync. These are the new messages that we will download.
694             * Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
695             * because they are locally deleted and we don't need or want the old message from
696             * the server.
697             */
698            for (Message message : remoteMessages) {
699                LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
700                if (localMessage == null) {
701                    newMessageCount++;
702                }
703                // localMessage == null -> message has never been created (not even headers)
704                // mFlagLoaded = UNLOADED -> message created, but none of body loaded
705                // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
706                // mFlagLoaded = COMPLETE -> message body has been completely loaded
707                // mFlagLoaded = DELETED -> message has been deleted
708                // Only the first two of these are "unsynced", so let's retrieve them
709                if (localMessage == null ||
710                        (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
711                    unsyncedMessages.add(message);
712                }
713            }
714        }
715
716        // 8.  Download basic info about the new/unloaded messages (if any)
717        /*
718         * Fetch the flags and envelope only of the new messages. This is intended to get us
719         * critical data as fast as possible, and then we'll fill in the details.
720         */
721        if (unsyncedMessages.size() > 0) {
722            downloadFlagAndEnvelope(account, mailbox, remoteFolder, unsyncedMessages,
723                    localMessageMap, unseenMessages);
724        }
725
726        // 9. Refresh the flags for any messages in the local store that we didn't just download.
727        FetchProfile fp = new FetchProfile();
728        fp.add(FetchProfile.Item.FLAGS);
729        remoteFolder.fetch(remoteMessages, fp, null);
730        boolean remoteSupportsSeen = false;
731        boolean remoteSupportsFlagged = false;
732        boolean remoteSupportsAnswered = false;
733        for (Flag flag : remoteFolder.getPermanentFlags()) {
734            if (flag == Flag.SEEN) {
735                remoteSupportsSeen = true;
736            }
737            if (flag == Flag.FLAGGED) {
738                remoteSupportsFlagged = true;
739            }
740            if (flag == Flag.ANSWERED) {
741                remoteSupportsAnswered = true;
742            }
743        }
744        // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
745        if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
746            for (Message remoteMessage : remoteMessages) {
747                LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
748                if (localMessageInfo == null) {
749                    continue;
750                }
751                boolean localSeen = localMessageInfo.mFlagRead;
752                boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
753                boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
754                boolean localFlagged = localMessageInfo.mFlagFavorite;
755                boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
756                boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
757                int localFlags = localMessageInfo.mFlags;
758                boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
759                boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
760                boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
761                if (newSeen || newFlagged || newAnswered) {
762                    Uri uri = ContentUris.withAppendedId(
763                            EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
764                    ContentValues updateValues = new ContentValues();
765                    updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
766                    updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
767                    if (remoteAnswered) {
768                        localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
769                    } else {
770                        localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
771                    }
772                    updateValues.put(MessageColumns.FLAGS, localFlags);
773                    resolver.update(uri, updateValues, null, null);
774                }
775            }
776        }
777
778        // 10. Remove any messages that are in the local store but no longer on the remote store.
779        HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
780        localUidsToDelete.removeAll(remoteUidMap.keySet());
781        for (String uidToDelete : localUidsToDelete) {
782            LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
783
784            // Delete associated data (attachment files)
785            // Attachment & Body records are auto-deleted when we delete the Message record
786            AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
787                    infoToDelete.mId);
788
789            // Delete the message itself
790            Uri uriToDelete = ContentUris.withAppendedId(
791                    EmailContent.Message.CONTENT_URI, infoToDelete.mId);
792            resolver.delete(uriToDelete, null, null);
793
794            // Delete extra rows (e.g. synced or deleted)
795            Uri syncRowToDelete = ContentUris.withAppendedId(
796                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
797            resolver.delete(syncRowToDelete, null, null);
798            Uri deletERowToDelete = ContentUris.withAppendedId(
799                    EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
800            resolver.delete(deletERowToDelete, null, null);
801        }
802
803        loadUnsyncedMessages(account, remoteFolder, unsyncedMessages, mailbox);
804
805        // 14. Clean up and report results
806        remoteFolder.close(false);
807
808        return new SyncResults(remoteMessageCount, unseenMessages);
809    }
810
811    /**
812     * Copy one downloaded message (which may have partially-loaded sections)
813     * into a newly created EmailProvider Message, given the account and mailbox
814     *
815     * @param message the remote message we've just downloaded
816     * @param account the account it will be stored into
817     * @param folder the mailbox it will be stored into
818     * @param loadStatus when complete, the message will be marked with this status (e.g.
819     *        EmailContent.Message.LOADED)
820     */
821    public void copyOneMessageToProvider(Message message, Account account,
822            Mailbox folder, int loadStatus) {
823        EmailContent.Message localMessage = null;
824        Cursor c = null;
825        try {
826            c = mContext.getContentResolver().query(
827                    EmailContent.Message.CONTENT_URI,
828                    EmailContent.Message.CONTENT_PROJECTION,
829                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
830                    " AND " + MessageColumns.MAILBOX_KEY + "=?" +
831                    " AND " + SyncColumns.SERVER_ID + "=?",
832                    new String[] {
833                            String.valueOf(account.mId),
834                            String.valueOf(folder.mId),
835                            String.valueOf(message.getUid())
836                    },
837                    null);
838            if (c.moveToNext()) {
839                localMessage = EmailContent.getContent(c, EmailContent.Message.class);
840                localMessage.mMailboxKey = folder.mId;
841                localMessage.mAccountKey = account.mId;
842                copyOneMessageToProvider(message, localMessage, loadStatus, mContext);
843            }
844        } finally {
845            if (c != null) {
846                c.close();
847            }
848        }
849    }
850
851    /**
852     * Copy one downloaded message (which may have partially-loaded sections)
853     * into an already-created EmailProvider Message
854     *
855     * @param message the remote message we've just downloaded
856     * @param localMessage the EmailProvider Message, already created
857     * @param loadStatus when complete, the message will be marked with this status (e.g.
858     *        EmailContent.Message.LOADED)
859     * @param context the context to be used for EmailProvider
860     */
861    public void copyOneMessageToProvider(Message message, EmailContent.Message localMessage,
862            int loadStatus, Context context) {
863        try {
864
865            EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
866                    localMessage.mId);
867            if (body == null) {
868                body = new EmailContent.Body();
869            }
870            try {
871                // Copy the fields that are available into the message object
872                LegacyConversions.updateMessageFields(localMessage, message,
873                        localMessage.mAccountKey, localMessage.mMailboxKey);
874
875                // Now process body parts & attachments
876                ArrayList<Part> viewables = new ArrayList<Part>();
877                ArrayList<Part> attachments = new ArrayList<Part>();
878                MimeUtility.collectParts(message, viewables, attachments);
879
880                ConversionUtilities.updateBodyFields(body, localMessage, viewables);
881
882                // Commit the message & body to the local store immediately
883                saveOrUpdate(localMessage, context);
884                saveOrUpdate(body, context);
885
886                // process (and save) attachments
887                LegacyConversions.updateAttachments(context, localMessage, attachments);
888
889                // One last update of message with two updated flags
890                localMessage.mFlagLoaded = loadStatus;
891
892                ContentValues cv = new ContentValues();
893                cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
894                cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
895                Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
896                        localMessage.mId);
897                context.getContentResolver().update(uri, cv, null, null);
898
899            } catch (MessagingException me) {
900                Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
901            }
902
903        } catch (RuntimeException rte) {
904            Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
905        } catch (IOException ioe) {
906            Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
907        }
908    }
909
910    /**
911     * Process a pending append message command. This command uploads a local message to the
912     * server, first checking to be sure that the server message is not newer than
913     * the local message.
914     *
915     * @param remoteStore the remote store we're working in
916     * @param account The account in which we are working
917     * @param newMailbox The mailbox we're appending to
918     * @param message The message we're appending
919     * @return true if successfully uploaded
920     */
921    private boolean processPendingAppend(Store remoteStore, Account account,
922            Mailbox newMailbox, EmailContent.Message message)
923            throws MessagingException {
924
925        boolean updateInternalDate = false;
926        boolean updateMessage = false;
927        boolean deleteMessage = false;
928
929        // 1. Find the remote folder that we're appending to and create and/or open it
930        Folder remoteFolder = remoteStore.getFolder(newMailbox.mServerId);
931        if (!remoteFolder.exists()) {
932            if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) {
933                // This is POP3, we cannot actually upload.  Instead, we'll update the message
934                // locally with a fake serverId (so we don't keep trying here) and return.
935                if (message.mServerId == null || message.mServerId.length() == 0) {
936                    message.mServerId = LOCAL_SERVERID_PREFIX + message.mId;
937                    Uri uri =
938                        ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
939                    ContentValues cv = new ContentValues();
940                    cv.put(EmailContent.Message.SERVER_ID, message.mServerId);
941                    mContext.getContentResolver().update(uri, cv, null, null);
942                }
943                return true;
944            }
945            if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
946                // This is a (hopefully) transient error and we return false to try again later
947                return false;
948            }
949        }
950        remoteFolder.open(OpenMode.READ_WRITE);
951        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
952            return false;
953        }
954
955        // 2. If possible, load a remote message with the matching UID
956        Message remoteMessage = null;
957        if (message.mServerId != null && message.mServerId.length() > 0) {
958            remoteMessage = remoteFolder.getMessage(message.mServerId);
959        }
960
961        // 3. If a remote message could not be found, upload our local message
962        if (remoteMessage == null) {
963            // 3a. Create a legacy message to upload
964            Message localMessage = LegacyConversions.makeMessage(mContext, message);
965
966            // 3b. Upload it
967            FetchProfile fp = new FetchProfile();
968            fp.add(FetchProfile.Item.BODY);
969            remoteFolder.appendMessages(new Message[] { localMessage });
970
971            // 3b. And record the UID from the server
972            message.mServerId = localMessage.getUid();
973            updateInternalDate = true;
974            updateMessage = true;
975        } else {
976            // 4. If the remote message exists we need to determine which copy to keep.
977            FetchProfile fp = new FetchProfile();
978            fp.add(FetchProfile.Item.ENVELOPE);
979            remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
980            Date localDate = new Date(message.mServerTimeStamp);
981            Date remoteDate = remoteMessage.getInternalDate();
982            if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
983                // 4a. If the remote message is newer than ours we'll just
984                // delete ours and move on. A sync will get the server message
985                // if we need to be able to see it.
986                deleteMessage = true;
987            } else {
988                // 4b. Otherwise we'll upload our message and then delete the remote message.
989
990                // Create a legacy message to upload
991                Message localMessage = LegacyConversions.makeMessage(mContext, message);
992
993                // 4c. Upload it
994                fp.clear();
995                fp = new FetchProfile();
996                fp.add(FetchProfile.Item.BODY);
997                remoteFolder.appendMessages(new Message[] { localMessage });
998
999                // 4d. Record the UID and new internalDate from the server
1000                message.mServerId = localMessage.getUid();
1001                updateInternalDate = true;
1002                updateMessage = true;
1003
1004                // 4e. And delete the old copy of the message from the server
1005                remoteMessage.setFlag(Flag.DELETED, true);
1006            }
1007        }
1008
1009        // 5. If requested, Best-effort to capture new "internaldate" from the server
1010        if (updateInternalDate && message.mServerId != null) {
1011            try {
1012                Message remoteMessage2 = remoteFolder.getMessage(message.mServerId);
1013                if (remoteMessage2 != null) {
1014                    FetchProfile fp2 = new FetchProfile();
1015                    fp2.add(FetchProfile.Item.ENVELOPE);
1016                    remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null);
1017                    message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime();
1018                    updateMessage = true;
1019                }
1020            } catch (MessagingException me) {
1021                // skip it - we can live without this
1022            }
1023        }
1024
1025        // 6. Perform required edits to local copy of message
1026        if (deleteMessage || updateMessage) {
1027            Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
1028            ContentResolver resolver = mContext.getContentResolver();
1029            if (deleteMessage) {
1030                resolver.delete(uri, null, null);
1031            } else if (updateMessage) {
1032                ContentValues cv = new ContentValues();
1033                cv.put(EmailContent.Message.SERVER_ID, message.mServerId);
1034                cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp);
1035                resolver.update(uri, cv, null, null);
1036            }
1037        }
1038
1039        return true;
1040    }
1041
1042    /**
1043     * Finish loading a message that have been partially downloaded.
1044     *
1045     * @param messageId the message to load
1046     * @param listener the callback by which results will be reported
1047     */
1048    public void loadMessageForView(final long messageId, MessagingListener listener) {
1049        mListeners.loadMessageForViewStarted(messageId);
1050        put("loadMessageForViewRemote", listener, new Runnable() {
1051            @Override
1052            public void run() {
1053                try {
1054                    // 1. Resample the message, in case it disappeared or synced while
1055                    // this command was in queue
1056                    EmailContent.Message message =
1057                        EmailContent.Message.restoreMessageWithId(mContext, messageId);
1058                    if (message == null) {
1059                        mListeners.loadMessageForViewFailed(messageId, "Unknown message");
1060                        return;
1061                    }
1062                    if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
1063                        mListeners.loadMessageForViewFinished(messageId);
1064                        return;
1065                    }
1066
1067                    // 2. Open the remote folder.
1068                    // TODO combine with common code in loadAttachment
1069                    Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
1070                    Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
1071                    if (account == null || mailbox == null) {
1072                        mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
1073                        return;
1074                    }
1075                    TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
1076
1077                    Store remoteStore = Store.getInstance(account, mContext);
1078                    String remoteServerId = mailbox.mServerId;
1079                    // If this is a search result, use the protocolSearchInfo field to get the
1080                    // correct remote location
1081                    if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
1082                        remoteServerId = message.mProtocolSearchInfo;
1083                    }
1084                    Folder remoteFolder = remoteStore.getFolder(remoteServerId);
1085                    remoteFolder.open(OpenMode.READ_WRITE);
1086
1087                    // 3. Set up to download the entire message
1088                    Message remoteMessage = remoteFolder.getMessage(message.mServerId);
1089                    FetchProfile fp = new FetchProfile();
1090                    fp.add(FetchProfile.Item.BODY);
1091                    remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1092
1093                    // 4. Write to provider
1094                    copyOneMessageToProvider(remoteMessage, account, mailbox,
1095                            EmailContent.Message.FLAG_LOADED_COMPLETE);
1096
1097                    // 5. Notify UI
1098                    mListeners.loadMessageForViewFinished(messageId);
1099
1100                } catch (MessagingException me) {
1101                    if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
1102                    mListeners.loadMessageForViewFailed(messageId, me.getMessage());
1103                } catch (RuntimeException rte) {
1104                    mListeners.loadMessageForViewFailed(messageId, rte.getMessage());
1105                }
1106            }
1107        });
1108    }
1109
1110    /**
1111     * Attempts to load the attachment specified by id from the given account and message.
1112     */
1113    public void loadAttachment(final long accountId, final long messageId, final long mailboxId,
1114            final long attachmentId, MessagingListener listener, final boolean background) {
1115        mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true);
1116
1117        put("loadAttachment", listener, new Runnable() {
1118            @Override
1119            public void run() {
1120                try {
1121                    //1. Check if the attachment is already here and return early in that case
1122                    Attachment attachment =
1123                        Attachment.restoreAttachmentWithId(mContext, attachmentId);
1124                    if (attachment == null) {
1125                        mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
1126                                   new MessagingException("The attachment is null"),
1127                                   background);
1128                        return;
1129                    }
1130                    if (Utility.attachmentExists(mContext, attachment)) {
1131                        mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
1132                        return;
1133                    }
1134
1135                    // 2. Open the remote folder.
1136                    // TODO all of these could be narrower projections
1137                    Account account = Account.restoreAccountWithId(mContext, accountId);
1138                    Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1139                    EmailContent.Message message =
1140                        EmailContent.Message.restoreMessageWithId(mContext, messageId);
1141
1142                    if (account == null || mailbox == null || message == null) {
1143                        mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
1144                                new MessagingException(
1145                                        "Account, mailbox, message or attachment are null"),
1146                                background);
1147                        return;
1148                    }
1149                    TrafficStats.setThreadStatsTag(
1150                            TrafficFlags.getAttachmentFlags(mContext, account));
1151
1152                    Store remoteStore = Store.getInstance(account, mContext);
1153                    Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1154                    remoteFolder.open(OpenMode.READ_WRITE);
1155
1156                    // 3. Generate a shell message in which to retrieve the attachment,
1157                    // and a shell BodyPart for the attachment.  Then glue them together.
1158                    Message storeMessage = remoteFolder.createMessage(message.mServerId);
1159                    MimeBodyPart storePart = new MimeBodyPart();
1160                    storePart.setSize((int)attachment.mSize);
1161                    storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
1162                            attachment.mLocation);
1163                    storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
1164                            String.format("%s;\n name=\"%s\"",
1165                            attachment.mMimeType,
1166                            attachment.mFileName));
1167                    // TODO is this always true for attachments?  I think we dropped the
1168                    // true encoding along the way
1169                    storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
1170
1171                    MimeMultipart multipart = new MimeMultipart();
1172                    multipart.setSubType("mixed");
1173                    multipart.addBodyPart(storePart);
1174
1175                    storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
1176                    storeMessage.setBody(multipart);
1177
1178                    // 4. Now ask for the attachment to be fetched
1179                    FetchProfile fp = new FetchProfile();
1180                    fp.add(storePart);
1181                    remoteFolder.fetch(new Message[] { storeMessage }, fp,
1182                            mController.new MessageRetrievalListenerBridge(
1183                                    messageId, attachmentId));
1184
1185                    // If we failed to load the attachment, throw an Exception here, so that
1186                    // AttachmentDownloadService knows that we failed
1187                    if (storePart.getBody() == null) {
1188                        throw new MessagingException("Attachment not loaded.");
1189                    }
1190
1191                    // 5. Save the downloaded file and update the attachment as necessary
1192                    LegacyConversions.saveAttachmentBody(mContext, storePart, attachment,
1193                            accountId);
1194
1195                    // 6. Report success
1196                    mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
1197                }
1198                catch (MessagingException me) {
1199                    if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
1200                    mListeners.loadAttachmentFailed(
1201                            accountId, messageId, attachmentId, me, background);
1202                } catch (IOException ioe) {
1203                    Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
1204                }
1205            }});
1206    }
1207
1208    /**
1209     * Attempt to send any messages that are sitting in the Outbox.
1210     * @param account
1211     * @param listener
1212     */
1213    public void sendPendingMessages(final Account account, final long sentFolderId,
1214            MessagingListener listener) {
1215        put("sendPendingMessages", listener, new Runnable() {
1216            @Override
1217            public void run() {
1218                sendPendingMessagesSynchronous(account, sentFolderId);
1219            }
1220        });
1221    }
1222
1223    /**
1224     * Attempt to send all messages sitting in the given account's outbox. Optionally,
1225     * if the server requires it, the message will be moved to the given sent folder.
1226     */
1227    public void sendPendingMessagesSynchronous(final Account account,
1228            long sentFolderId) {
1229        TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account));
1230        NotificationController nc = NotificationController.getInstance(mContext);
1231        // 1.  Loop through all messages in the account's outbox
1232        long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
1233        if (outboxId == Mailbox.NO_MAILBOX) {
1234            return;
1235        }
1236        ContentResolver resolver = mContext.getContentResolver();
1237        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
1238                EmailContent.Message.ID_COLUMN_PROJECTION,
1239                EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
1240                null);
1241        try {
1242            // 2.  exit early
1243            if (c.getCount() <= 0) {
1244                return;
1245            }
1246            // 3. do one-time setup of the Sender & other stuff
1247            mListeners.sendPendingMessagesStarted(account.mId, -1);
1248
1249            Sender sender = Sender.getInstance(mContext, account);
1250            Store remoteStore = Store.getInstance(account, mContext);
1251            boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
1252            ContentValues moveToSentValues = null;
1253            if (requireMoveMessageToSentFolder) {
1254                moveToSentValues = new ContentValues();
1255                moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId);
1256            }
1257
1258            // 4.  loop through the available messages and send them
1259            while (c.moveToNext()) {
1260                long messageId = -1;
1261                try {
1262                    messageId = c.getLong(0);
1263                    mListeners.sendPendingMessagesStarted(account.mId, messageId);
1264                    // Don't send messages with unloaded attachments
1265                    if (Utility.hasUnloadedAttachments(mContext, messageId)) {
1266                        if (Email.DEBUG) {
1267                            Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
1268                                    "; unloaded attachments");
1269                        }
1270                        continue;
1271                    }
1272                    sender.sendMessage(messageId);
1273                } catch (MessagingException me) {
1274                    // report error for this message, but keep trying others
1275                    if (me instanceof AuthenticationFailedException) {
1276                        nc.showLoginFailedNotification(account.mId);
1277                    }
1278                    mListeners.sendPendingMessagesFailed(account.mId, messageId, me);
1279                    continue;
1280                }
1281                // 5. move to sent, or delete
1282                Uri syncedUri =
1283                    ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
1284                if (requireMoveMessageToSentFolder) {
1285                    // If this is a forwarded message and it has attachments, delete them, as they
1286                    // duplicate information found elsewhere (on the server).  This saves storage.
1287                    EmailContent.Message msg =
1288                        EmailContent.Message.restoreMessageWithId(mContext, messageId);
1289                    if (msg != null &&
1290                            ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
1291                        AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
1292                                messageId);
1293                    }
1294                    resolver.update(syncedUri, moveToSentValues, null, null);
1295                } else {
1296                    AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
1297                            messageId);
1298                    Uri uri =
1299                        ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
1300                    resolver.delete(uri, null, null);
1301                    resolver.delete(syncedUri, null, null);
1302                }
1303            }
1304            // 6. report completion/success
1305            mListeners.sendPendingMessagesCompleted(account.mId);
1306            nc.cancelLoginFailedNotification(account.mId);
1307        } catch (MessagingException me) {
1308            if (me instanceof AuthenticationFailedException) {
1309                nc.showLoginFailedNotification(account.mId);
1310            }
1311            mListeners.sendPendingMessagesFailed(account.mId, -1, me);
1312        } finally {
1313            c.close();
1314        }
1315    }
1316
1317    /**
1318     * Checks mail for an account.
1319     * This entry point is for use by the mail checking service only, because it
1320     * gives slightly different callbacks (so the service doesn't get confused by callbacks
1321     * triggered by/for the foreground UI.
1322     *
1323     * TODO clean up the execution model which is unnecessarily threaded due to legacy code
1324     *
1325     * @param accountId the account to check
1326     * @param listener
1327     */
1328    public void checkMail(final long accountId, final long tag, final MessagingListener listener) {
1329        mListeners.checkMailStarted(mContext, accountId, tag);
1330
1331        // This puts the command on the queue (not synchronous)
1332        listFolders(accountId, null);
1333
1334        // Put this on the queue as well so it follows listFolders
1335        put("checkMail", listener, new Runnable() {
1336            @Override
1337            public void run() {
1338                // send any pending outbound messages.  note, there is a slight race condition
1339                // here if we somehow don't have a sent folder, but this should never happen
1340                // because the call to sendMessage() would have built one previously.
1341                long inboxId = -1;
1342                Account account = Account.restoreAccountWithId(mContext, accountId);
1343                if (account != null) {
1344                    long sentboxId = Mailbox.findMailboxOfType(mContext, accountId,
1345                            Mailbox.TYPE_SENT);
1346                    if (sentboxId != Mailbox.NO_MAILBOX) {
1347                        sendPendingMessagesSynchronous(account, sentboxId);
1348                    }
1349                    // find mailbox # for inbox and sync it.
1350                    // TODO we already know this in Controller, can we pass it in?
1351                    inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
1352                    if (inboxId != Mailbox.NO_MAILBOX) {
1353                        Mailbox mailbox =
1354                            Mailbox.restoreMailboxWithId(mContext, inboxId);
1355                        if (mailbox != null) {
1356                            synchronizeMailboxSynchronous(account, mailbox);
1357                        }
1358                    }
1359                }
1360                mListeners.checkMailFinished(mContext, accountId, inboxId, tag);
1361            }
1362        });
1363    }
1364
1365    private static class Command {
1366        public Runnable runnable;
1367
1368        public MessagingListener listener;
1369
1370        public String description;
1371
1372        @Override
1373        public String toString() {
1374            return description;
1375        }
1376    }
1377
1378    /** Results of the latest synchronization. */
1379    private static class SyncResults {
1380        /** The total # of messages in the folder */
1381        public final int mTotalMessages;
1382        /** A list of new message IDs; must not be {@code null} */
1383        public final ArrayList<Long> mAddedMessages;
1384
1385        public SyncResults(int totalMessages, ArrayList<Long> addedMessages) {
1386            if (addedMessages == null) {
1387                throw new IllegalArgumentException("addedMessages must not be null");
1388            }
1389            mTotalMessages = totalMessages;
1390            mAddedMessages = addedMessages;
1391        }
1392    }
1393}
1394