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