1/*
2 * Copyright (C) 2012 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.service;
18
19import android.app.Service;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.database.Cursor;
26import android.net.TrafficStats;
27import android.net.Uri;
28import android.os.IBinder;
29import android.os.SystemClock;
30import android.text.TextUtils;
31import android.text.format.DateUtils;
32
33import com.android.email.DebugUtils;
34import com.android.email.LegacyConversions;
35import com.android.email.NotificationController;
36import com.android.email.NotificationControllerCreatorHolder;
37import com.android.email.R;
38import com.android.email.mail.Store;
39import com.android.email.provider.Utilities;
40import com.android.emailcommon.Logging;
41import com.android.emailcommon.TrafficFlags;
42import com.android.emailcommon.internet.MimeUtility;
43import com.android.emailcommon.mail.AuthenticationFailedException;
44import com.android.emailcommon.mail.FetchProfile;
45import com.android.emailcommon.mail.Flag;
46import com.android.emailcommon.mail.Folder;
47import com.android.emailcommon.mail.Folder.FolderType;
48import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
49import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
50import com.android.emailcommon.mail.Folder.OpenMode;
51import com.android.emailcommon.mail.Message;
52import com.android.emailcommon.mail.MessagingException;
53import com.android.emailcommon.mail.Part;
54import com.android.emailcommon.provider.Account;
55import com.android.emailcommon.provider.EmailContent;
56import com.android.emailcommon.provider.EmailContent.MailboxColumns;
57import com.android.emailcommon.provider.EmailContent.MessageColumns;
58import com.android.emailcommon.provider.EmailContent.SyncColumns;
59import com.android.emailcommon.provider.Mailbox;
60import com.android.emailcommon.service.EmailServiceStatus;
61import com.android.emailcommon.service.SearchParams;
62import com.android.emailcommon.service.SyncWindow;
63import com.android.emailcommon.utility.AttachmentUtilities;
64import com.android.mail.providers.UIProvider;
65import com.android.mail.utils.LogUtils;
66
67import java.util.ArrayList;
68import java.util.Arrays;
69import java.util.Comparator;
70import java.util.Date;
71import java.util.HashMap;
72import java.util.List;
73
74public class ImapService extends Service {
75    // TODO get these from configurations or settings.
76    private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS;
77    private static final long FULL_SYNC_WINDOW_MILLIS = 7 * DateUtils.DAY_IN_MILLIS;
78    private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS;
79
80    // The maximum number of messages to fetch in a single command.
81    private static final int MAX_MESSAGES_TO_FETCH = 500;
82    private static final int MINIMUM_MESSAGES_TO_SYNC = 10;
83    private static final int LOAD_MORE_MIN_INCREMENT = 10;
84    private static final int LOAD_MORE_MAX_INCREMENT = 20;
85    private static final long INITIAL_WINDOW_SIZE_INCREASE = 24 * 60 * 60 * 1000;
86
87    private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN };
88    private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
89    private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
90
91    /**
92     * Simple cache for last search result mailbox by account and serverId, since the most common
93     * case will be repeated use of the same mailbox
94     */
95    private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
96    private static String mLastSearchServerId = null;
97    private static Mailbox mLastSearchRemoteMailbox = null;
98
99    /**
100     * Cache search results by account; this allows for "load more" support without having to
101     * redo the search (which can be quite slow).  SortableMessage is a smallish class, so memory
102     * shouldn't be an issue
103     */
104    private static final HashMap<Long, SortableMessage[]> sSearchResults =
105            new HashMap<Long, SortableMessage[]>();
106
107    /**
108     * We write this into the serverId field of messages that will never be upsynced.
109     */
110    private static final String LOCAL_SERVERID_PREFIX = "Local-";
111
112    private static String sMessageDecodeErrorString;
113
114    /**
115     * Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access
116     * to a Context object.
117     * @return Error string or empty string
118     */
119    public static String getMessageDecodeErrorString() {
120        return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString;
121    }
122
123    @Override
124    public void onCreate() {
125        super.onCreate();
126
127        sMessageDecodeErrorString = getString(R.string.message_decode_error);
128    }
129
130    @Override
131    public int onStartCommand(Intent intent, int flags, int startId) {
132        return Service.START_STICKY;
133    }
134
135    /**
136     * Create our EmailService implementation here.
137     */
138    private final EmailServiceStub mBinder = new EmailServiceStub() {
139        @Override
140        public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
141            try {
142                return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
143                        destMailboxId);
144            } catch (MessagingException e) {
145                // Ignore
146            }
147            return 0;
148        }
149    };
150
151    @Override
152    public IBinder onBind(Intent intent) {
153        mBinder.init(this);
154        return mBinder;
155    }
156
157    /**
158     * Start foreground synchronization of the specified folder. This is called by
159     * synchronizeMailbox or checkMail.
160     * TODO this should use ID's instead of fully-restored objects
161     * @return The status code for whether this operation succeeded.
162     * @throws MessagingException
163     */
164    public static synchronized int synchronizeMailboxSynchronous(Context context,
165            final Account account, final Mailbox folder, final boolean loadMore,
166            final boolean uiRefresh) throws MessagingException {
167        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
168        final NotificationController nc =
169                NotificationControllerCreatorHolder.getInstance(context);
170        Store remoteStore = null;
171        try {
172            remoteStore = Store.getInstance(account, context);
173            processPendingActionsSynchronous(context, account, remoteStore, uiRefresh);
174            synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh);
175            // Clear authentication notification for this account
176            nc.cancelLoginFailedNotification(account.mId);
177        } catch (MessagingException e) {
178            if (Logging.LOGD) {
179                LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxSynchronous", e);
180            }
181            if (e instanceof AuthenticationFailedException) {
182                // Generate authentication notification
183                nc.showLoginFailedNotificationSynchronous(account.mId, true /* incoming */);
184            }
185            throw e;
186        } finally {
187            if (remoteStore != null) {
188                remoteStore.closeConnections();
189            }
190        }
191        // TODO: Rather than use exceptions as logic above, return the status and handle it
192        // correctly in caller.
193        return EmailServiceStatus.SUCCESS;
194    }
195
196    /**
197     * Lightweight record for the first pass of message sync, where I'm just seeing if
198     * the local message requires sync.  Later (for messages that need syncing) we'll do a full
199     * readout from the DB.
200     */
201    private static class LocalMessageInfo {
202        private static final int COLUMN_ID = 0;
203        private static final int COLUMN_FLAG_READ = 1;
204        private static final int COLUMN_FLAG_FAVORITE = 2;
205        private static final int COLUMN_FLAG_LOADED = 3;
206        private static final int COLUMN_SERVER_ID = 4;
207        private static final int COLUMN_FLAGS =  5;
208        private static final int COLUMN_TIMESTAMP =  6;
209        private static final String[] PROJECTION = {
210                MessageColumns._ID,
211                MessageColumns.FLAG_READ,
212                MessageColumns.FLAG_FAVORITE,
213                MessageColumns.FLAG_LOADED,
214                SyncColumns.SERVER_ID,
215                MessageColumns.FLAGS,
216                MessageColumns.TIMESTAMP
217        };
218
219        final long mId;
220        final boolean mFlagRead;
221        final boolean mFlagFavorite;
222        final int mFlagLoaded;
223        final String mServerId;
224        final int mFlags;
225        final long mTimestamp;
226
227        public LocalMessageInfo(Cursor c) {
228            mId = c.getLong(COLUMN_ID);
229            mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
230            mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
231            mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
232            mServerId = c.getString(COLUMN_SERVER_ID);
233            mFlags = c.getInt(COLUMN_FLAGS);
234            mTimestamp = c.getLong(COLUMN_TIMESTAMP);
235            // Note: mailbox key and account key not needed - they are projected for the SELECT
236        }
237    }
238
239    private static class OldestTimestampInfo {
240        private static final int COLUMN_OLDEST_TIMESTAMP = 0;
241        private static final String[] PROJECTION = new String[] {
242            "MIN(" + MessageColumns.TIMESTAMP + ")"
243        };
244    }
245
246    /**
247     * Load the structure and body of messages not yet synced
248     * @param account the account we're syncing
249     * @param remoteFolder the (open) Folder we're working on
250     * @param messages an array of Messages we've got headers for
251     * @param toMailbox the destination mailbox we're syncing
252     * @throws MessagingException
253     */
254    static void loadUnsyncedMessages(final Context context, final Account account,
255            Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)
256            throws MessagingException {
257
258        FetchProfile fp = new FetchProfile();
259        fp.add(FetchProfile.Item.STRUCTURE);
260        remoteFolder.fetch(messages.toArray(new Message[messages.size()]), fp, null);
261        Message [] oneMessageArray = new Message[1];
262        for (Message message : messages) {
263            // Build a list of parts we are interested in. Text parts will be downloaded
264            // right now, attachments will be left for later.
265            ArrayList<Part> viewables = new ArrayList<Part>();
266            ArrayList<Part> attachments = new ArrayList<Part>();
267            MimeUtility.collectParts(message, viewables, attachments);
268            // Download the viewables immediately
269            oneMessageArray[0] = message;
270            for (Part part : viewables) {
271                fp.clear();
272                fp.add(part);
273                remoteFolder.fetch(oneMessageArray, fp, null);
274            }
275            // Store the updated message locally and mark it fully loaded
276            Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
277                    EmailContent.Message.FLAG_LOADED_COMPLETE);
278        }
279    }
280
281    public static void downloadFlagAndEnvelope(final Context context, final Account account,
282            final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
283            HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
284            throws MessagingException {
285        FetchProfile fp = new FetchProfile();
286        fp.add(FetchProfile.Item.FLAGS);
287        fp.add(FetchProfile.Item.ENVELOPE);
288
289        final HashMap<String, LocalMessageInfo> localMapCopy;
290        if (localMessageMap != null)
291            localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
292        else {
293            localMapCopy = new HashMap<String, LocalMessageInfo>();
294        }
295
296        remoteFolder.fetch(unsyncedMessages.toArray(new Message[unsyncedMessages.size()]), fp,
297                new MessageRetrievalListener() {
298                    @Override
299                    public void messageRetrieved(Message message) {
300                        try {
301                            // Determine if the new message was already known (e.g. partial)
302                            // And create or reload the full message info
303                            final LocalMessageInfo localMessageInfo =
304                                    localMapCopy.get(message.getUid());
305                            final boolean localExists = localMessageInfo != null;
306
307                            if (!localExists && message.isSet(Flag.DELETED)) {
308                                // This is a deleted message that we don't have locally, so don't
309                                // create it
310                                return;
311                            }
312
313                            final EmailContent.Message localMessage;
314                            if (!localExists) {
315                                localMessage = new EmailContent.Message();
316                            } else {
317                                localMessage = EmailContent.Message.restoreMessageWithId(
318                                        context, localMessageInfo.mId);
319                            }
320
321                            if (localMessage != null) {
322                                try {
323                                    // Copy the fields that are available into the message
324                                    LegacyConversions.updateMessageFields(localMessage,
325                                            message, account.mId, mailbox.mId);
326                                    // Commit the message to the local store
327                                    Utilities.saveOrUpdate(localMessage, context);
328                                    // Track the "new" ness of the downloaded message
329                                    if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
330                                        unseenMessages.add(localMessage.mId);
331                                    }
332                                } catch (MessagingException me) {
333                                    LogUtils.e(Logging.LOG_TAG,
334                                            "Error while copying downloaded message." + me);
335                                }
336                            }
337                        }
338                        catch (Exception e) {
339                            LogUtils.e(Logging.LOG_TAG,
340                                    "Error while storing downloaded message." + e.toString());
341                        }
342                    }
343
344                    @Override
345                    public void loadAttachmentProgress(int progress) {
346                    }
347                });
348
349    }
350
351    /**
352     * Synchronizer for IMAP.
353     *
354     * TODO Break this method up into smaller chunks.
355     *
356     * @param account the account to sync
357     * @param mailbox the mailbox to sync
358     * @param loadMore whether we should be loading more older messages
359     * @param uiRefresh whether this request is in response to a user action
360     * @throws MessagingException
361     */
362    private synchronized static void synchronizeMailboxGeneric(final Context context,
363            final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore,
364            final boolean uiRefresh)
365            throws MessagingException {
366
367        LogUtils.d(Logging.LOG_TAG, "synchronizeMailboxGeneric " + account + " " + mailbox + " "
368                + loadMore + " " + uiRefresh);
369
370        final ArrayList<Long> unseenMessages = new ArrayList<Long>();
371
372        ContentResolver resolver = context.getContentResolver();
373
374        // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
375        if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
376            return;
377        }
378
379        // 1. Figure out what our sync window should be.
380        long endDate;
381
382        // We will do a full sync if the user has actively requested a sync, or if it has been
383        // too long since the last full sync.
384        // If we have rebooted since the last full sync, then we may get a negative
385        // timeSinceLastFullSync. In this case, we don't know how long it's been since the last
386        // full sync so we should perform the full sync.
387        final long timeSinceLastFullSync = SystemClock.elapsedRealtime() -
388                mailbox.mLastFullSyncTime;
389        final boolean fullSync = (uiRefresh || loadMore ||
390                timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS || timeSinceLastFullSync < 0);
391
392        if (account.mSyncLookback == SyncWindow.SYNC_WINDOW_ALL) {
393            // This is really for testing. There is no UI that allows setting the sync window for
394            // IMAP, but it can be set by sending a special intent to AccountSetupFinal activity.
395            endDate = 0;
396        } else if (fullSync) {
397            // Find the oldest message in the local store. We need our time window to include
398            // all messages that are currently present locally.
399            endDate = System.currentTimeMillis() - FULL_SYNC_WINDOW_MILLIS;
400            Cursor localOldestCursor = null;
401            try {
402                // b/11520812 Ignore message with timestamp = 0 (which includes NULL)
403                localOldestCursor = resolver.query(EmailContent.Message.CONTENT_URI,
404                        OldestTimestampInfo.PROJECTION,
405                        EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + " AND " +
406                                MessageColumns.MAILBOX_KEY + "=? AND " +
407                                MessageColumns.TIMESTAMP + "!=0",
408                        new String[] {String.valueOf(account.mId), String.valueOf(mailbox.mId)},
409                        null);
410                if (localOldestCursor != null && localOldestCursor.moveToFirst()) {
411                    long oldestLocalMessageDate = localOldestCursor.getLong(
412                            OldestTimestampInfo.COLUMN_OLDEST_TIMESTAMP);
413                    if (oldestLocalMessageDate > 0) {
414                        endDate = Math.min(endDate, oldestLocalMessageDate);
415                        LogUtils.d(
416                                Logging.LOG_TAG, "oldest local message " + oldestLocalMessageDate);
417                    }
418                }
419            } finally {
420                if (localOldestCursor != null) {
421                    localOldestCursor.close();
422                }
423            }
424            LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate);
425        } else {
426            // We are doing a frequent, quick sync. This only syncs a small time window, so that
427            // we wil get any new messages, but not spend a lot of bandwidth downloading
428            // messageIds that we most likely already have.
429            endDate = System.currentTimeMillis() - QUICK_SYNC_WINDOW_MILLIS;
430            LogUtils.d(Logging.LOG_TAG, "quick sync: original window: now - " + endDate);
431        }
432
433        // 2. Open the remote folder and create the remote folder if necessary
434        // The account might have been deleted
435        if (remoteStore == null) {
436            LogUtils.d(Logging.LOG_TAG, "account is apparently deleted");
437            return;
438        }
439        final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
440
441        // If the folder is a "special" folder we need to see if it exists
442        // on the remote server. It if does not exist we'll try to create it. If we
443        // can't create we'll abort. This will happen on every single Pop3 folder as
444        // designed and on Imap folders during error conditions. This allows us
445        // to treat Pop3 and Imap the same in this code.
446        if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT) {
447            if (!remoteFolder.exists()) {
448                if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
449                    LogUtils.w(Logging.LOG_TAG, "could not create remote folder type %d",
450                        mailbox.mType);
451                    return;
452                }
453            }
454        }
455        remoteFolder.open(OpenMode.READ_WRITE);
456
457        // 3. Trash any remote messages that are marked as trashed locally.
458        // TODO - this comment was here, but no code was here.
459
460        // 4. Get the number of messages on the server.
461        // TODO: this value includes deleted but unpurged messages, and so slightly mismatches
462        // the contents of our DB since we drop deleted messages. Figure out what to do about this.
463        final int remoteMessageCount = remoteFolder.getMessageCount();
464
465        // 5. Save folder message count locally.
466        mailbox.updateMessageCount(context, remoteMessageCount);
467
468        // 6. Get all message Ids in our sync window:
469        Message[] remoteMessages;
470        remoteMessages = remoteFolder.getMessages(0, endDate, null);
471        LogUtils.d(Logging.LOG_TAG, "received " + remoteMessages.length + " messages");
472
473        // 7. See if we need any additional messages beyond our date query range results.
474        // If we do, keep increasing the size of our query window until we have
475        // enough, or until we have all messages in the mailbox.
476        int totalCountNeeded;
477        if (loadMore) {
478            totalCountNeeded = remoteMessages.length + LOAD_MORE_MIN_INCREMENT;
479        } else {
480            totalCountNeeded = remoteMessages.length;
481            if (fullSync && totalCountNeeded < MINIMUM_MESSAGES_TO_SYNC) {
482                totalCountNeeded = MINIMUM_MESSAGES_TO_SYNC;
483            }
484        }
485        LogUtils.d(Logging.LOG_TAG, "need " + totalCountNeeded + " total");
486
487        final int additionalMessagesNeeded = totalCountNeeded - remoteMessages.length;
488        if (additionalMessagesNeeded > 0) {
489            LogUtils.d(Logging.LOG_TAG, "trying to get " + additionalMessagesNeeded + " more");
490            long startDate = endDate - 1;
491            Message[] additionalMessages = new Message[0];
492            long windowIncreaseSize = INITIAL_WINDOW_SIZE_INCREASE;
493            while (additionalMessages.length < additionalMessagesNeeded && endDate > 0) {
494                endDate = endDate - windowIncreaseSize;
495                if (endDate < 0) {
496                    LogUtils.d(Logging.LOG_TAG, "window size too large, this is the last attempt");
497                    endDate = 0;
498                }
499                LogUtils.d(Logging.LOG_TAG,
500                        "requesting additional messages from range " + startDate + " - " + endDate);
501                additionalMessages = remoteFolder.getMessages(startDate, endDate, null);
502
503                // If don't get enough messages with the first window size expansion,
504                // we need to accelerate rate at which the window expands. Otherwise,
505                // if there were no messages for several weeks, we'd always end up
506                // performing dozens of queries.
507                windowIncreaseSize *= 2;
508            }
509
510            LogUtils.d(Logging.LOG_TAG, "additionalMessages " + additionalMessages.length);
511            if (additionalMessages.length < additionalMessagesNeeded) {
512                // We have attempted to load a window that goes all the way back to time zero,
513                // but we still don't have as many messages as the server says are in the inbox.
514                // This is not expected to happen.
515                LogUtils.e(Logging.LOG_TAG, "expected to find " + additionalMessagesNeeded
516                        + " more messages, only got " + additionalMessages.length);
517            }
518            int additionalToKeep = additionalMessages.length;
519            if (additionalMessages.length > LOAD_MORE_MAX_INCREMENT) {
520                // We have way more additional messages than intended, drop some of them.
521                // The last messages are the most recent, so those are the ones we need to keep.
522                additionalToKeep = LOAD_MORE_MAX_INCREMENT;
523            }
524
525            // Copy the messages into one array.
526            Message[] allMessages = new Message[remoteMessages.length + additionalToKeep];
527            System.arraycopy(remoteMessages, 0, allMessages, 0, remoteMessages.length);
528            // additionalMessages may have more than we need, only copy the last
529            // several. These are the most recent messages in that set because
530            // of the way IMAP server returns messages.
531            System.arraycopy(additionalMessages, additionalMessages.length - additionalToKeep,
532                    allMessages, remoteMessages.length, additionalToKeep);
533            remoteMessages = allMessages;
534        }
535
536        // 8. Get the all of the local messages within the sync window, and create
537        // an index of the uids.
538        // The IMAP query for messages ignores time, and only looks at the date part of the endDate.
539        // So if we query for messages since Aug 11 at 3:00 PM, we can get messages from any time
540        // on Aug 11. Our IMAP query results can include messages up to 24 hours older than endDate,
541        // or up to 25 hours older at a daylight savings transition.
542        // It is important that we have the Id of any local message that could potentially be
543        // returned by the IMAP query, or we will create duplicate copies of the same messages.
544        // So we will increase our local query range by this much.
545        // Note that this complicates deletion: It's not okay to delete anything that is in the
546        // localMessageMap but not in the remote result, because we know that we may be getting
547        // Ids of local messages that are outside the IMAP query window.
548        Cursor localUidCursor = null;
549        HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
550        try {
551            // FLAG: There is a problem that causes us to store the wrong date on some messages,
552            // so messages get a date of zero. If we filter these messages out and don't put them
553            // in our localMessageMap, then we'll end up loading the same message again.
554            // See b/10508861
555//            final long queryEndDate = endDate - DateUtils.DAY_IN_MILLIS - DateUtils.HOUR_IN_MILLIS;
556            final long queryEndDate = 0;
557            localUidCursor = resolver.query(
558                    EmailContent.Message.CONTENT_URI,
559                    LocalMessageInfo.PROJECTION,
560                    EmailContent.MessageColumns.ACCOUNT_KEY + "=?"
561                            + " AND " + MessageColumns.MAILBOX_KEY + "=?"
562                            + " AND " + MessageColumns.TIMESTAMP + ">=?",
563                    new String[] {
564                            String.valueOf(account.mId),
565                            String.valueOf(mailbox.mId),
566                            String.valueOf(queryEndDate) },
567                    null);
568            while (localUidCursor.moveToNext()) {
569                LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
570                // If the message has no server id, it's local only. This should only happen for
571                // mail created on the client that has failed to upsync. We want to ignore such
572                // mail during synchronization (i.e. leave it as-is and let the next sync try again
573                // to upsync).
574                if (!TextUtils.isEmpty(info.mServerId)) {
575                    localMessageMap.put(info.mServerId, info);
576                }
577            }
578        } finally {
579            if (localUidCursor != null) {
580                localUidCursor.close();
581            }
582        }
583
584        // 9. Get a list of the messages that are in the remote list but not on the
585        // local store, or messages that are in the local store but failed to download
586        // on the last sync. These are the new messages that we will download.
587        // Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
588        // because they are locally deleted and we don't need or want the old message from
589        // the server.
590        final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
591        final HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
592        // Process the messages in the reverse order we received them in. This means that
593        // we load the most recent one first, which gives a better user experience.
594        for (int i = remoteMessages.length - 1; i >= 0; i--) {
595            Message message = remoteMessages[i];
596            LogUtils.d(Logging.LOG_TAG, "remote message " + message.getUid());
597            remoteUidMap.put(message.getUid(), message);
598
599            LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
600
601            // localMessage == null -> message has never been created (not even headers)
602            // mFlagLoaded = UNLOADED -> message created, but none of body loaded
603            // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
604            // mFlagLoaded = COMPLETE -> message body has been completely loaded
605            // mFlagLoaded = DELETED -> message has been deleted
606            // Only the first two of these are "unsynced", so let's retrieve them
607            if (localMessage == null ||
608                    (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
609                    (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
610                unsyncedMessages.add(message);
611            }
612        }
613
614        // 10. Download basic info about the new/unloaded messages (if any)
615        /*
616         * Fetch the flags and envelope only of the new messages. This is intended to get us
617         * critical data as fast as possible, and then we'll fill in the details.
618         */
619        if (unsyncedMessages.size() > 0) {
620            downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
621                    localMessageMap, unseenMessages);
622        }
623
624        // 11. Refresh the flags for any messages in the local store that we didn't just download.
625        // TODO This is a bit wasteful because we're also updating any messages we already did get
626        // the flags and envelope for previously.
627        // TODO: the fetch() function, and others, should take List<>s of messages, not
628        // arrays of messages.
629        FetchProfile fp = new FetchProfile();
630        fp.add(FetchProfile.Item.FLAGS);
631        if (remoteMessages.length > MAX_MESSAGES_TO_FETCH) {
632            List<Message> remoteMessageList = Arrays.asList(remoteMessages);
633            for (int start = 0; start < remoteMessageList.size(); start += MAX_MESSAGES_TO_FETCH) {
634                int end = start + MAX_MESSAGES_TO_FETCH;
635                if (end >= remoteMessageList.size()) {
636                    end = remoteMessageList.size() - 1;
637                }
638                List<Message> chunk = remoteMessageList.subList(start, end);
639                final Message[] partialArray = chunk.toArray(new Message[chunk.size()]);
640                // Fetch this one chunk of messages
641                remoteFolder.fetch(partialArray, fp, null);
642            }
643        } else {
644            remoteFolder.fetch(remoteMessages, fp, null);
645        }
646        boolean remoteSupportsSeen = false;
647        boolean remoteSupportsFlagged = false;
648        boolean remoteSupportsAnswered = false;
649        for (Flag flag : remoteFolder.getPermanentFlags()) {
650            if (flag == Flag.SEEN) {
651                remoteSupportsSeen = true;
652            }
653            if (flag == Flag.FLAGGED) {
654                remoteSupportsFlagged = true;
655            }
656            if (flag == Flag.ANSWERED) {
657                remoteSupportsAnswered = true;
658            }
659        }
660
661        // 12. Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
662        if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
663            for (Message remoteMessage : remoteMessages) {
664                LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
665                if (localMessageInfo == null) {
666                    continue;
667                }
668                boolean localSeen = localMessageInfo.mFlagRead;
669                boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
670                boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
671                boolean localFlagged = localMessageInfo.mFlagFavorite;
672                boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
673                boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
674                int localFlags = localMessageInfo.mFlags;
675                boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
676                boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
677                boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
678                if (newSeen || newFlagged || newAnswered) {
679                    Uri uri = ContentUris.withAppendedId(
680                            EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
681                    ContentValues updateValues = new ContentValues();
682                    updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
683                    updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
684                    if (remoteAnswered) {
685                        localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
686                    } else {
687                        localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
688                    }
689                    updateValues.put(MessageColumns.FLAGS, localFlags);
690                    resolver.update(uri, updateValues, null, null);
691                }
692            }
693        }
694
695        // 12.5 Remove messages that are marked as deleted so that we drop them from the DB in the
696        // next step
697        for (final Message remoteMessage : remoteMessages) {
698            if (remoteMessage.isSet(Flag.DELETED)) {
699                remoteUidMap.remove(remoteMessage.getUid());
700                unsyncedMessages.remove(remoteMessage);
701            }
702        }
703
704        // 13. Remove messages that are in the local store and in the current sync window,
705        // but no longer on the remote store. Note that localMessageMap can contain messages
706        // that are not actually in our sync window. We need to check the timestamp to ensure
707        // that it is before deleting.
708        for (final LocalMessageInfo info : localMessageMap.values()) {
709            // If this message is inside our sync window, and we cannot find it in our list
710            // of remote messages, then we know it's been deleted from the server.
711            if (info.mTimestamp >= endDate && !remoteUidMap.containsKey(info.mServerId)) {
712                // Delete associated data (attachment files)
713                // Attachment & Body records are auto-deleted when we delete the Message record
714                AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, info.mId);
715
716                // Delete the message itself
717                final Uri uriToDelete = ContentUris.withAppendedId(
718                        EmailContent.Message.CONTENT_URI, info.mId);
719                resolver.delete(uriToDelete, null, null);
720
721                // Delete extra rows (e.g. updated or deleted)
722                final Uri updateRowToDelete = ContentUris.withAppendedId(
723                        EmailContent.Message.UPDATED_CONTENT_URI, info.mId);
724                resolver.delete(updateRowToDelete, null, null);
725                final Uri deleteRowToDelete = ContentUris.withAppendedId(
726                        EmailContent.Message.DELETED_CONTENT_URI, info.mId);
727                resolver.delete(deleteRowToDelete, null, null);
728            }
729        }
730
731        loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
732
733        if (fullSync) {
734            mailbox.updateLastFullSyncTime(context, SystemClock.elapsedRealtime());
735        }
736
737        // 14. Clean up and report results
738        remoteFolder.close(false);
739    }
740
741    /**
742     * Find messages in the updated table that need to be written back to server.
743     *
744     * Handles:
745     *   Read/Unread
746     *   Flagged
747     *   Append (upload)
748     *   Move To Trash
749     *   Empty trash
750     * TODO:
751     *   Move
752     *
753     * @param account the account to scan for pending actions
754     * @throws MessagingException
755     */
756    private static void processPendingActionsSynchronous(Context context, Account account,
757            Store remoteStore, boolean manualSync)
758            throws MessagingException {
759        TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
760        String[] accountIdArgs = new String[] { Long.toString(account.mId) };
761
762        // Handle deletes first, it's always better to get rid of things first
763        processPendingDeletesSynchronous(context, account, remoteStore, accountIdArgs);
764
765        // Handle uploads (currently, only to sent messages)
766        processPendingUploadsSynchronous(context, account, remoteStore, accountIdArgs, manualSync);
767
768        // Now handle updates / upsyncs
769        processPendingUpdatesSynchronous(context, account, remoteStore, accountIdArgs);
770    }
771
772    /**
773     * Get the mailbox corresponding to the remote location of a message; this will normally be
774     * the mailbox whose _id is mailboxKey, except for search results, where we must look it up
775     * by serverId.
776     *
777     * @param message the message in question
778     * @return the mailbox in which the message resides on the server
779     */
780    private static Mailbox getRemoteMailboxForMessage(
781            Context context, EmailContent.Message message) {
782        // If this is a search result, use the protocolSearchInfo field to get the server info
783        if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
784            long accountKey = message.mAccountKey;
785            String protocolSearchInfo = message.mProtocolSearchInfo;
786            if (accountKey == mLastSearchAccountKey &&
787                    protocolSearchInfo.equals(mLastSearchServerId)) {
788                return mLastSearchRemoteMailbox;
789            }
790            Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
791                    Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
792                    new String[] {protocolSearchInfo, Long.toString(accountKey) },
793                    null);
794            try {
795                if (c.moveToNext()) {
796                    Mailbox mailbox = new Mailbox();
797                    mailbox.restore(c);
798                    mLastSearchAccountKey = accountKey;
799                    mLastSearchServerId = protocolSearchInfo;
800                    mLastSearchRemoteMailbox = mailbox;
801                    return mailbox;
802                } else {
803                    return null;
804                }
805            } finally {
806                c.close();
807            }
808        } else {
809            return Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
810        }
811    }
812
813    /**
814     * Scan for messages that are in the Message_Deletes table, look for differences that
815     * we can deal with, and do the work.
816     */
817    private static void processPendingDeletesSynchronous(Context context, Account account,
818            Store remoteStore, String[] accountIdArgs) {
819        Cursor deletes = context.getContentResolver().query(
820                EmailContent.Message.DELETED_CONTENT_URI,
821                EmailContent.Message.CONTENT_PROJECTION,
822                EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
823                EmailContent.MessageColumns.MAILBOX_KEY);
824        long lastMessageId = -1;
825        try {
826            // loop through messages marked as deleted
827            while (deletes.moveToNext()) {
828                EmailContent.Message oldMessage =
829                        EmailContent.getContent(context, deletes, EmailContent.Message.class);
830
831                if (oldMessage != null) {
832                    lastMessageId = oldMessage.mId;
833
834                    Mailbox mailbox = getRemoteMailboxForMessage(context, oldMessage);
835                    if (mailbox == null) {
836                        continue; // Mailbox removed. Move to the next message.
837                    }
838                    final boolean deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
839
840                    // Dispatch here for specific change types
841                    if (deleteFromTrash) {
842                        // Move message to trash
843                        processPendingDeleteFromTrash(remoteStore, mailbox, oldMessage);
844                    }
845
846                    // Finally, delete the update
847                    Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
848                            oldMessage.mId);
849                    context.getContentResolver().delete(uri, null, null);
850                }
851            }
852        } catch (MessagingException me) {
853            // Presumably an error here is an account connection failure, so there is
854            // no point in continuing through the rest of the pending updates.
855            if (DebugUtils.DEBUG) {
856                LogUtils.d(Logging.LOG_TAG, "Unable to process pending delete for id="
857                        + lastMessageId + ": " + me);
858            }
859        } finally {
860            deletes.close();
861        }
862    }
863
864    /**
865     * Scan for messages that are in Sent, and are in need of upload,
866     * and send them to the server. "In need of upload" is defined as:
867     *  serverId == null (no UID has been assigned)
868     * or
869     *  message is in the updated list
870     *
871     * Note we also look for messages that are moving from drafts->outbox->sent. They never
872     * go through "drafts" or "outbox" on the server, so we hang onto these until they can be
873     * uploaded directly to the Sent folder.
874     */
875    private static void processPendingUploadsSynchronous(Context context, Account account,
876            Store remoteStore, String[] accountIdArgs, boolean manualSync) {
877        ContentResolver resolver = context.getContentResolver();
878        // Find the Sent folder (since that's all we're uploading for now
879        // TODO: Upsync for all folders? (In case a user moves mail from Sent before it is
880        // handled. Also, this would generically solve allowing drafts to upload.)
881        Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION,
882                MailboxColumns.ACCOUNT_KEY + "=?"
883                + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT,
884                accountIdArgs, null);
885        long lastMessageId = -1;
886        try {
887            while (mailboxes.moveToNext()) {
888                long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN);
889                String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) };
890                // Demand load mailbox
891                Mailbox mailbox = null;
892
893                // First handle the "new" messages (serverId == null)
894                Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI,
895                        EmailContent.Message.ID_PROJECTION,
896                        MessageColumns.MAILBOX_KEY + "=?"
897                        + " and (" + MessageColumns.SERVER_ID + " is null"
898                        + " or " + MessageColumns.SERVER_ID + "=''" + ")",
899                        mailboxKeyArgs,
900                        null);
901                try {
902                    while (upsyncs1.moveToNext()) {
903                        // Load the remote store if it will be needed
904                        if (remoteStore == null) {
905                            remoteStore = Store.getInstance(account, context);
906                        }
907                        // Load the mailbox if it will be needed
908                        if (mailbox == null) {
909                            mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
910                            if (mailbox == null) {
911                                continue; // Mailbox removed. Move to the next message.
912                            }
913                        }
914                        // upsync the message
915                        long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN);
916                        lastMessageId = id;
917                        processUploadMessage(context, remoteStore, mailbox, id, manualSync);
918                    }
919                } finally {
920                    if (upsyncs1 != null) {
921                        upsyncs1.close();
922                    }
923                    if (remoteStore != null) {
924                        remoteStore.closeConnections();
925                    }
926                }
927            }
928        } catch (MessagingException me) {
929            // Presumably an error here is an account connection failure, so there is
930            // no point in continuing through the rest of the pending updates.
931            if (DebugUtils.DEBUG) {
932                LogUtils.d(Logging.LOG_TAG, "Unable to process pending upsync for id="
933                        + lastMessageId + ": " + me);
934            }
935        } finally {
936            if (mailboxes != null) {
937                mailboxes.close();
938            }
939        }
940    }
941
942    /**
943     * Scan for messages that are in the Message_Updates table, look for differences that
944     * we can deal with, and do the work.
945     */
946    private static void processPendingUpdatesSynchronous(Context context, Account account,
947            Store remoteStore, String[] accountIdArgs) {
948        ContentResolver resolver = context.getContentResolver();
949        Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI,
950                EmailContent.Message.CONTENT_PROJECTION,
951                EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
952                EmailContent.MessageColumns.MAILBOX_KEY);
953        long lastMessageId = -1;
954        try {
955            // Demand load mailbox (note order-by to reduce thrashing here)
956            Mailbox mailbox = null;
957            // loop through messages marked as needing updates
958            while (updates.moveToNext()) {
959                boolean changeMoveToTrash = false;
960                boolean changeRead = false;
961                boolean changeFlagged = false;
962                boolean changeMailbox = false;
963                boolean changeAnswered = false;
964
965                EmailContent.Message oldMessage =
966                        EmailContent.getContent(context, updates, EmailContent.Message.class);
967                lastMessageId = oldMessage.mId;
968                EmailContent.Message newMessage =
969                        EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
970                if (newMessage != null) {
971                    mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey);
972                    if (mailbox == null) {
973                        continue; // Mailbox removed. Move to the next message.
974                    }
975                    if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
976                        if (mailbox.mType == Mailbox.TYPE_TRASH) {
977                            changeMoveToTrash = true;
978                        } else {
979                            changeMailbox = true;
980                        }
981                    }
982                    changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
983                    changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
984                    changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) !=
985                            (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO);
986                }
987
988                // Load the remote store if it will be needed
989                if (remoteStore == null &&
990                        (changeMoveToTrash || changeRead || changeFlagged || changeMailbox ||
991                                changeAnswered)) {
992                    remoteStore = Store.getInstance(account, context);
993                }
994
995                // Dispatch here for specific change types
996                if (changeMoveToTrash) {
997                    // Move message to trash
998                    processPendingMoveToTrash(context, remoteStore, mailbox, oldMessage,
999                            newMessage);
1000                } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) {
1001                    processPendingDataChange(context, remoteStore, mailbox, changeRead,
1002                            changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage);
1003                }
1004
1005                // Finally, delete the update
1006                Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI,
1007                        oldMessage.mId);
1008                resolver.delete(uri, null, null);
1009            }
1010
1011        } catch (MessagingException me) {
1012            // Presumably an error here is an account connection failure, so there is
1013            // no point in continuing through the rest of the pending updates.
1014            if (DebugUtils.DEBUG) {
1015                LogUtils.d(Logging.LOG_TAG, "Unable to process pending update for id="
1016                        + lastMessageId + ": " + me);
1017            }
1018        } finally {
1019            updates.close();
1020        }
1021    }
1022
1023    /**
1024     * Upsync an entire message. This must also unwind whatever triggered it (either by
1025     * updating the serverId, or by deleting the update record, or it's going to keep happening
1026     * over and over again.
1027     *
1028     * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload.
1029     * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select
1030     * only the Drafts and Sent folders, this can happen when the update record and the current
1031     * record mismatch. In this case, we let the update record remain, because the filters
1032     * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it)
1033     * appropriately.
1034     *
1035     * @param mailbox the actual mailbox
1036     */
1037    private static void processUploadMessage(Context context, Store remoteStore, Mailbox mailbox,
1038            long messageId, boolean manualSync)
1039            throws MessagingException {
1040        EmailContent.Message newMessage =
1041                EmailContent.Message.restoreMessageWithId(context, messageId);
1042        final boolean deleteUpdate;
1043        if (newMessage == null) {
1044            deleteUpdate = true;
1045            LogUtils.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId);
1046        } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
1047            deleteUpdate = false;
1048            LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId);
1049        } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
1050            deleteUpdate = false;
1051            LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId);
1052        } else if (mailbox.mType == Mailbox.TYPE_TRASH) {
1053            deleteUpdate = false;
1054            LogUtils.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId);
1055        } else if (newMessage.mMailboxKey != mailbox.mId) {
1056            deleteUpdate = false;
1057            LogUtils.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId);
1058        } else {
1059            LogUtils.d(Logging.LOG_TAG, "Upsync triggered for message id=" + messageId);
1060            deleteUpdate =
1061                    processPendingAppend(context, remoteStore, mailbox, newMessage, manualSync);
1062        }
1063        if (deleteUpdate) {
1064            // Finally, delete the update (if any)
1065            Uri uri = ContentUris.withAppendedId(
1066                    EmailContent.Message.UPDATED_CONTENT_URI, messageId);
1067            context.getContentResolver().delete(uri, null, null);
1068        }
1069    }
1070
1071    /**
1072     * Upsync changes to read, flagged, or mailbox
1073     *
1074     * @param remoteStore the remote store for this mailbox
1075     * @param mailbox the mailbox the message is stored in
1076     * @param changeRead whether the message's read state has changed
1077     * @param changeFlagged whether the message's flagged state has changed
1078     * @param changeMailbox whether the message's mailbox has changed
1079     * @param oldMessage the message in it's pre-change state
1080     * @param newMessage the current version of the message
1081     */
1082    private static void processPendingDataChange(final Context context, Store remoteStore,
1083            Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox,
1084            boolean changeAnswered, EmailContent.Message oldMessage,
1085            final EmailContent.Message newMessage) throws MessagingException {
1086        // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
1087        // being moved
1088        Mailbox newMailbox = mailbox;
1089        // Mailbox is the original remote mailbox (the one we're acting on)
1090        mailbox = getRemoteMailboxForMessage(context, oldMessage);
1091
1092        // 0. No remote update if the message is local-only
1093        if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1094                || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) {
1095            return;
1096        }
1097
1098        // 1. No remote update for DRAFTS or OUTBOX
1099        if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
1100            return;
1101        }
1102
1103        // 2. Open the remote store & folder
1104        Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1105        if (!remoteFolder.exists()) {
1106            return;
1107        }
1108        remoteFolder.open(OpenMode.READ_WRITE);
1109        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1110            return;
1111        }
1112
1113        // 3. Finally, apply the changes to the message
1114        Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId);
1115        if (remoteMessage == null) {
1116            return;
1117        }
1118        if (DebugUtils.DEBUG) {
1119            LogUtils.d(Logging.LOG_TAG,
1120                    "Update for msg id=" + newMessage.mId
1121                    + " read=" + newMessage.mFlagRead
1122                    + " flagged=" + newMessage.mFlagFavorite
1123                    + " answered="
1124                    + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0)
1125                    + " new mailbox=" + newMessage.mMailboxKey);
1126        }
1127        Message[] messages = new Message[] { remoteMessage };
1128        if (changeRead) {
1129            remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead);
1130        }
1131        if (changeFlagged) {
1132            remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite);
1133        }
1134        if (changeAnswered) {
1135            remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED,
1136                    (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0);
1137        }
1138        if (changeMailbox) {
1139            Folder toFolder = remoteStore.getFolder(newMailbox.mServerId);
1140            if (!remoteFolder.exists()) {
1141                return;
1142            }
1143            // We may need the message id to search for the message in the destination folder
1144            remoteMessage.setMessageId(newMessage.mMessageId);
1145            // Copy the message to its new folder
1146            remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
1147                @Override
1148                public void onMessageUidChange(Message message, String newUid) {
1149                    ContentValues cv = new ContentValues();
1150                    cv.put(MessageColumns.SERVER_ID, newUid);
1151                    // We only have one message, so, any updates _must_ be for it. Otherwise,
1152                    // we'd have to cycle through to find the one with the same server ID.
1153                    context.getContentResolver().update(ContentUris.withAppendedId(
1154                            EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
1155                }
1156
1157                @Override
1158                public void onMessageNotFound(Message message) {
1159                }
1160            });
1161            // Delete the message from the remote source folder
1162            remoteMessage.setFlag(Flag.DELETED, true);
1163            remoteFolder.expunge();
1164        }
1165        remoteFolder.close(false);
1166    }
1167
1168    /**
1169     * Process a pending trash message command.
1170     *
1171     * @param remoteStore the remote store we're working in
1172     * @param newMailbox The local trash mailbox
1173     * @param oldMessage The message copy that was saved in the updates shadow table
1174     * @param newMessage The message that was moved to the mailbox
1175     */
1176    private static void processPendingMoveToTrash(final Context context, Store remoteStore,
1177            Mailbox newMailbox, EmailContent.Message oldMessage,
1178            final EmailContent.Message newMessage) throws MessagingException {
1179
1180        // 0. No remote move if the message is local-only
1181        if (newMessage.mServerId == null || newMessage.mServerId.equals("")
1182                || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) {
1183            return;
1184        }
1185
1186        // 1. Escape early if we can't find the local mailbox
1187        // TODO smaller projection here
1188        Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage);
1189        if (oldMailbox == null) {
1190            // can't find old mailbox, it may have been deleted.  just return.
1191            return;
1192        }
1193        // 2. We don't support delete-from-trash here
1194        if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
1195            return;
1196        }
1197
1198        // The rest of this method handles server-side deletion
1199
1200        // 4.  Find the remote mailbox (that we deleted from), and open it
1201        Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId);
1202        if (!remoteFolder.exists()) {
1203            return;
1204        }
1205
1206        remoteFolder.open(OpenMode.READ_WRITE);
1207        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1208            remoteFolder.close(false);
1209            return;
1210        }
1211
1212        // 5. Find the remote original message
1213        Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
1214        if (remoteMessage == null) {
1215            remoteFolder.close(false);
1216            return;
1217        }
1218
1219        // 6. Find the remote trash folder, and create it if not found
1220        Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId);
1221        if (!remoteTrashFolder.exists()) {
1222            /*
1223             * If the remote trash folder doesn't exist we try to create it.
1224             */
1225            remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
1226        }
1227
1228        // 7. Try to copy the message into the remote trash folder
1229        // Note, this entire section will be skipped for POP3 because there's no remote trash
1230        if (remoteTrashFolder.exists()) {
1231            /*
1232             * Because remoteTrashFolder may be new, we need to explicitly open it
1233             */
1234            remoteTrashFolder.open(OpenMode.READ_WRITE);
1235            if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1236                remoteFolder.close(false);
1237                remoteTrashFolder.close(false);
1238                return;
1239            }
1240
1241            remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
1242                    new Folder.MessageUpdateCallbacks() {
1243                @Override
1244                public void onMessageUidChange(Message message, String newUid) {
1245                    // update the UID in the local trash folder, because some stores will
1246                    // have to change it when copying to remoteTrashFolder
1247                    ContentValues cv = new ContentValues();
1248                    cv.put(MessageColumns.SERVER_ID, newUid);
1249                    context.getContentResolver().update(newMessage.getUri(), cv, null, null);
1250                }
1251
1252                /**
1253                 * This will be called if the deleted message doesn't exist and can't be
1254                 * deleted (e.g. it was already deleted from the server.)  In this case,
1255                 * attempt to delete the local copy as well.
1256                 */
1257                @Override
1258                public void onMessageNotFound(Message message) {
1259                    context.getContentResolver().delete(newMessage.getUri(), null, null);
1260                }
1261            });
1262            remoteTrashFolder.close(false);
1263        }
1264
1265        // 8. Delete the message from the remote source folder
1266        remoteMessage.setFlag(Flag.DELETED, true);
1267        remoteFolder.expunge();
1268        remoteFolder.close(false);
1269    }
1270
1271    /**
1272     * Process a pending trash message command.
1273     *
1274     * @param remoteStore the remote store we're working in
1275     * @param oldMailbox The local trash mailbox
1276     * @param oldMessage The message that was deleted from the trash
1277     */
1278    private static void processPendingDeleteFromTrash(Store remoteStore,
1279            Mailbox oldMailbox, EmailContent.Message oldMessage)
1280            throws MessagingException {
1281
1282        // 1. We only support delete-from-trash here
1283        if (oldMailbox.mType != Mailbox.TYPE_TRASH) {
1284            return;
1285        }
1286
1287        // 2.  Find the remote trash folder (that we are deleting from), and open it
1288        Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId);
1289        if (!remoteTrashFolder.exists()) {
1290            return;
1291        }
1292
1293        remoteTrashFolder.open(OpenMode.READ_WRITE);
1294        if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
1295            remoteTrashFolder.close(false);
1296            return;
1297        }
1298
1299        // 3. Find the remote original message
1300        Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId);
1301        if (remoteMessage == null) {
1302            remoteTrashFolder.close(false);
1303            return;
1304        }
1305
1306        // 4. Delete the message from the remote trash folder
1307        remoteMessage.setFlag(Flag.DELETED, true);
1308        remoteTrashFolder.expunge();
1309        remoteTrashFolder.close(false);
1310    }
1311
1312    /**
1313     * Process a pending append message command. This command uploads a local message to the
1314     * server, first checking to be sure that the server message is not newer than
1315     * the local message.
1316     *
1317     * @param remoteStore the remote store we're working in
1318     * @param mailbox The mailbox we're appending to
1319     * @param message The message we're appending
1320     * @param manualSync True if this is a manual sync (changes upsync behavior)
1321     * @return true if successfully uploaded
1322     */
1323    private static boolean processPendingAppend(Context context, Store remoteStore, Mailbox mailbox,
1324            EmailContent.Message message, boolean manualSync)
1325            throws MessagingException {
1326        boolean updateInternalDate = false;
1327        boolean updateMessage = false;
1328        boolean deleteMessage = false;
1329
1330        // 1. Find the remote folder that we're appending to and create and/or open it
1331        Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1332        if (!remoteFolder.exists()) {
1333            if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
1334                // This is a (hopefully) transient error and we return false to try again later
1335                return false;
1336            }
1337        }
1338        remoteFolder.open(OpenMode.READ_WRITE);
1339        if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
1340            return false;
1341        }
1342
1343        // 2. If possible, load a remote message with the matching UID
1344        Message remoteMessage = null;
1345        if (message.mServerId != null && message.mServerId.length() > 0) {
1346            remoteMessage = remoteFolder.getMessage(message.mServerId);
1347        }
1348
1349        // 3. If a remote message could not be found, upload our local message
1350        if (remoteMessage == null) {
1351            // TODO:
1352            // if we have a serverId and remoteMessage is still null, then probably the message
1353            // has been deleted and we should delete locally.
1354            // 3a. Create a legacy message to upload
1355            Message localMessage = LegacyConversions.makeMessage(context, message);
1356            // 3b. Upload it
1357            //FetchProfile fp = new FetchProfile();
1358            //fp.add(FetchProfile.Item.BODY);
1359            // Note that this operation will assign the Uid to localMessage
1360            remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
1361
1362            // 3b. And record the UID from the server
1363            message.mServerId = localMessage.getUid();
1364            updateInternalDate = true;
1365            updateMessage = true;
1366        } else {
1367            // 4. If the remote message exists we need to determine which copy to keep.
1368            // TODO:
1369            // I don't see a good reason we should be here. If the message already has a serverId,
1370            // then we should be handling it in processPendingUpdates(),
1371            // not processPendingUploads()
1372            FetchProfile fp = new FetchProfile();
1373            fp.add(FetchProfile.Item.ENVELOPE);
1374            remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
1375            Date localDate = new Date(message.mServerTimeStamp);
1376            Date remoteDate = remoteMessage.getInternalDate();
1377            if (remoteDate != null && remoteDate.compareTo(localDate) > 0) {
1378                // 4a. If the remote message is newer than ours we'll just
1379                // delete ours and move on. A sync will get the server message
1380                // if we need to be able to see it.
1381                deleteMessage = true;
1382            } else {
1383                // 4b. Otherwise we'll upload our message and then delete the remote message.
1384
1385                // Create a legacy message to upload
1386                // TODO: This strategy has a problem: This will create a second message,
1387                // so that at least temporarily, we will have two messages for what the
1388                // user would think of as one.
1389                Message localMessage = LegacyConversions.makeMessage(context, message);
1390
1391                // 4c. Upload it
1392                fp.clear();
1393                fp = new FetchProfile();
1394                fp.add(FetchProfile.Item.BODY);
1395                remoteFolder.appendMessage(context, localMessage, manualSync /* no timeout */);
1396
1397                // 4d. Record the UID and new internalDate from the server
1398                message.mServerId = localMessage.getUid();
1399                updateInternalDate = true;
1400                updateMessage = true;
1401
1402                // 4e. And delete the old copy of the message from the server.
1403                remoteMessage.setFlag(Flag.DELETED, true);
1404            }
1405        }
1406
1407        // 5. If requested, Best-effort to capture new "internaldate" from the server
1408        if (updateInternalDate && message.mServerId != null) {
1409            try {
1410                Message remoteMessage2 = remoteFolder.getMessage(message.mServerId);
1411                if (remoteMessage2 != null) {
1412                    FetchProfile fp2 = new FetchProfile();
1413                    fp2.add(FetchProfile.Item.ENVELOPE);
1414                    remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null);
1415                    final Date remoteDate = remoteMessage2.getInternalDate();
1416                    if (remoteDate != null) {
1417                        message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime();
1418                        updateMessage = true;
1419                    }
1420                }
1421            } catch (MessagingException me) {
1422                // skip it - we can live without this
1423            }
1424        }
1425
1426        // 6. Perform required edits to local copy of message
1427        if (deleteMessage || updateMessage) {
1428            Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId);
1429            ContentResolver resolver = context.getContentResolver();
1430            if (deleteMessage) {
1431                resolver.delete(uri, null, null);
1432            } else if (updateMessage) {
1433                ContentValues cv = new ContentValues();
1434                cv.put(MessageColumns.SERVER_ID, message.mServerId);
1435                cv.put(MessageColumns.SERVER_TIMESTAMP, message.mServerTimeStamp);
1436                resolver.update(uri, cv, null, null);
1437            }
1438        }
1439
1440        return true;
1441    }
1442
1443    /**
1444     * A message and numeric uid that's easily sortable
1445     */
1446    private static class SortableMessage {
1447        private final Message mMessage;
1448        private final long mUid;
1449
1450        SortableMessage(Message message, long uid) {
1451            mMessage = message;
1452            mUid = uid;
1453        }
1454    }
1455
1456    private static int searchMailboxImpl(final Context context, final long accountId,
1457            final SearchParams searchParams, final long destMailboxId) throws MessagingException {
1458        final Account account = Account.restoreAccountWithId(context, accountId);
1459        final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId);
1460        final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
1461        if (account == null || mailbox == null || destMailbox == null) {
1462            LogUtils.d(Logging.LOG_TAG, "Attempted search for %s "
1463                    + "but account or mailbox information was missing", searchParams);
1464            return 0;
1465        }
1466
1467        // Tell UI that we're loading messages
1468        final ContentValues statusValues = new ContentValues(2);
1469        statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.LIVE_QUERY);
1470        destMailbox.update(context, statusValues);
1471
1472        Store remoteStore = null;
1473        int numSearchResults = 0;
1474        try {
1475            remoteStore = Store.getInstance(account, context);
1476            final Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
1477            remoteFolder.open(OpenMode.READ_WRITE);
1478
1479            SortableMessage[] sortableMessages = new SortableMessage[0];
1480            if (searchParams.mOffset == 0) {
1481                // Get the "bare" messages (basically uid)
1482                final Message[] remoteMessages = remoteFolder.getMessages(searchParams, null);
1483                final int remoteCount = remoteMessages.length;
1484                if (remoteCount > 0) {
1485                    sortableMessages = new SortableMessage[remoteCount];
1486                    int i = 0;
1487                    for (Message msg : remoteMessages) {
1488                        sortableMessages[i++] = new SortableMessage(msg,
1489                                Long.parseLong(msg.getUid()));
1490                    }
1491                    // Sort the uid's, most recent first
1492                    // Note: Not all servers will be nice and return results in the order of
1493                    // request; those that do will see messages arrive from newest to oldest
1494                    Arrays.sort(sortableMessages, new Comparator<SortableMessage>() {
1495                        @Override
1496                        public int compare(SortableMessage lhs, SortableMessage rhs) {
1497                            return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
1498                        }
1499                    });
1500                    sSearchResults.put(accountId, sortableMessages);
1501                }
1502            } else {
1503                // It seems odd for this to happen, but if the previous query returned zero results,
1504                // but the UI somehow still attempted to load more, then sSearchResults will have
1505                // a null value for this account. We need to handle this below.
1506                sortableMessages = sSearchResults.get(accountId);
1507            }
1508
1509            numSearchResults = (sortableMessages != null ? sortableMessages.length : 0);
1510            final int numToLoad =
1511                    Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit);
1512            destMailbox.updateMessageCount(context, numSearchResults);
1513            if (numToLoad <= 0) {
1514                return 0;
1515            }
1516
1517            final ArrayList<Message> messageList = new ArrayList<>();
1518            for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) {
1519                messageList.add(sortableMessages[i].mMessage);
1520            }
1521            // First fetch FLAGS and ENVELOPE. In a second pass, we'll fetch STRUCTURE and
1522            // the first body part.
1523            final FetchProfile fp = new FetchProfile();
1524            fp.add(FetchProfile.Item.FLAGS);
1525            fp.add(FetchProfile.Item.ENVELOPE);
1526
1527            Message[] messageArray = messageList.toArray(new Message[messageList.size()]);
1528
1529            // TODO: We are purposely processing messages with a MessageRetrievalListener here,
1530            // rather than just walking the messageArray after the operation completes. This is so
1531            // that we can immediately update the database so the user can see something useful
1532            // happening, even if the message body has not yet been fetched.
1533            // There are some issues with this approach:
1534            // 1. It means that we have a single thread doing both network and database operations,
1535            // and either can block the other. The database updates could slow down the network
1536            // reads, keeping our network connection open longer than is really necessary.
1537            // 2. We still load all of this data into messageArray, even though it's not used.
1538            // It would be nicer if we had one thread doing the network operation, and a separate
1539            // thread consuming that data and performing the appropriate database work, then
1540            // discarding the data as soon as it is no longer needed. This would reduce our memory
1541            // footprint and potentially allow our network operation to complete faster.
1542            remoteFolder.fetch(messageArray, fp, new MessageRetrievalListener() {
1543                @Override
1544                public void messageRetrieved(Message message) {
1545                    try {
1546                        EmailContent.Message localMessage = new EmailContent.Message();
1547
1548                        // Copy the fields that are available into the message
1549                        LegacyConversions.updateMessageFields(localMessage,
1550                                message, account.mId, mailbox.mId);
1551                        // Save off the mailbox that this message *really* belongs in.
1552                        // We need this information if we need to do more lookups
1553                        // (like loading attachments) for this message. See b/11294681
1554                        localMessage.mMainMailboxKey = localMessage.mMailboxKey;
1555                        localMessage.mMailboxKey = destMailboxId;
1556                        // We load 50k or so; maybe it's complete, maybe not...
1557                        int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
1558                        // We store the serverId of the source mailbox into protocolSearchInfo
1559                        // This will be used by loadMessageForView, etc. to use the proper remote
1560                        // folder
1561                        localMessage.mProtocolSearchInfo = mailbox.mServerId;
1562                        // Commit the message to the local store
1563                        Utilities.saveOrUpdate(localMessage, context);
1564                    } catch (MessagingException me) {
1565                        LogUtils.e(Logging.LOG_TAG, me,
1566                                "Error while copying downloaded message.");
1567                    } catch (Exception e) {
1568                        LogUtils.e(Logging.LOG_TAG, e,
1569                                "Error while storing downloaded message.");
1570                    }
1571                }
1572
1573                @Override
1574                public void loadAttachmentProgress(int progress) {
1575                }
1576            });
1577
1578            // Now load the structure for all of the messages:
1579            fp.clear();
1580            fp.add(FetchProfile.Item.STRUCTURE);
1581            remoteFolder.fetch(messageArray, fp, null);
1582
1583            // Finally, load the first body part (i.e. message text).
1584            // This means attachment contents are not yet loaded, but that's okay,
1585            // we'll load them as needed, same as in synced messages.
1586            Message[] oneMessageArray = new Message[1];
1587            for (Message message : messageArray) {
1588                // Build a list of parts we are interested in. Text parts will be downloaded
1589                // right now, attachments will be left for later.
1590                ArrayList<Part> viewables = new ArrayList<>();
1591                ArrayList<Part> attachments = new ArrayList<>();
1592                MimeUtility.collectParts(message, viewables, attachments);
1593                // Download the viewables immediately
1594                oneMessageArray[0] = message;
1595                for (Part part : viewables) {
1596                    fp.clear();
1597                    fp.add(part);
1598                    remoteFolder.fetch(oneMessageArray, fp, null);
1599                }
1600                // Store the updated message locally and mark it fully loaded
1601                Utilities.copyOneMessageToProvider(context, message, account, destMailbox,
1602                        EmailContent.Message.FLAG_LOADED_COMPLETE);
1603            }
1604
1605        } finally {
1606            if (remoteStore != null) {
1607                remoteStore.closeConnections();
1608            }
1609            // Tell UI that we're done loading messages
1610            statusValues.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1611            statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
1612            destMailbox.update(context, statusValues);
1613        }
1614
1615        return numSearchResults;
1616    }
1617}
1618