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