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 17 package com.android.email.service; 18 19 import android.app.Service; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.Cursor; 26 import android.net.TrafficStats; 27 import android.net.Uri; 28 import android.os.IBinder; 29 import android.os.SystemClock; 30 import android.text.TextUtils; 31 import android.text.format.DateUtils; 32 33 import com.android.email.DebugUtils; 34 import com.android.email.LegacyConversions; 35 import com.android.email.NotificationController; 36 import com.android.email.NotificationControllerCreatorHolder; 37 import com.android.email.R; 38 import com.android.email.mail.Store; 39 import com.android.email.provider.Utilities; 40 import com.android.emailcommon.Logging; 41 import com.android.emailcommon.TrafficFlags; 42 import com.android.emailcommon.internet.MimeUtility; 43 import com.android.emailcommon.mail.AuthenticationFailedException; 44 import com.android.emailcommon.mail.FetchProfile; 45 import com.android.emailcommon.mail.Flag; 46 import com.android.emailcommon.mail.Folder; 47 import com.android.emailcommon.mail.Folder.FolderType; 48 import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 49 import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; 50 import com.android.emailcommon.mail.Folder.OpenMode; 51 import com.android.emailcommon.mail.Message; 52 import com.android.emailcommon.mail.MessagingException; 53 import com.android.emailcommon.mail.Part; 54 import com.android.emailcommon.provider.Account; 55 import com.android.emailcommon.provider.EmailContent; 56 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 57 import com.android.emailcommon.provider.EmailContent.MessageColumns; 58 import com.android.emailcommon.provider.EmailContent.SyncColumns; 59 import com.android.emailcommon.provider.Mailbox; 60 import com.android.emailcommon.service.EmailServiceStatus; 61 import com.android.emailcommon.service.SearchParams; 62 import com.android.emailcommon.service.SyncWindow; 63 import com.android.emailcommon.utility.AttachmentUtilities; 64 import com.android.mail.providers.UIProvider; 65 import com.android.mail.utils.LogUtils; 66 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Comparator; 70 import java.util.Date; 71 import java.util.HashMap; 72 import java.util.List; 73 74 public 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 */ getMessageDecodeErrorString()119 public static String getMessageDecodeErrorString() { 120 return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString; 121 } 122 123 @Override onCreate()124 public void onCreate() { 125 super.onCreate(); 126 127 sMessageDecodeErrorString = getString(R.string.message_decode_error); 128 } 129 130 @Override onStartCommand(Intent intent, int flags, int startId)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 onBind(Intent intent)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 */ synchronizeMailboxSynchronous(Context context, final Account account, final Mailbox folder, final boolean loadMore, final boolean uiRefresh)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 LocalMessageInfo(Cursor c)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 */ loadUnsyncedMessages(final Context context, final Account account, Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)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 downloadFlagAndEnvelope(final Context context, final Account account, final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)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 */ synchronizeMailboxGeneric(final Context context, final Account account, Store remoteStore, final Mailbox mailbox, final boolean loadMore, final boolean uiRefresh)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 */ processPendingActionsSynchronous(Context context, Account account, Store remoteStore, boolean manualSync)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 */ getRemoteMailboxForMessage( Context context, EmailContent.Message message)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 */ processPendingDeletesSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs)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 */ processPendingUploadsSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs, boolean manualSync)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 */ processPendingUpdatesSynchronous(Context context, Account account, Store remoteStore, String[] accountIdArgs)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 */ processUploadMessage(Context context, Store remoteStore, Mailbox mailbox, long messageId, boolean manualSync)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 */ processPendingDataChange(final Context context, Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, EmailContent.Message oldMessage, final EmailContent.Message newMessage)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 */ processPendingMoveToTrash(final Context context, Store remoteStore, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage)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 */ processPendingDeleteFromTrash(Store remoteStore, Mailbox oldMailbox, EmailContent.Message oldMessage)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 */ processPendingAppend(Context context, Store remoteStore, Mailbox mailbox, EmailContent.Message message, boolean manualSync)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 SortableMessage(Message message, long uid)1450 SortableMessage(Message message, long uid) { 1451 mMessage = message; 1452 mUid = uid; 1453 } 1454 } 1455 searchMailboxImpl(final Context context, final long accountId, final SearchParams searchParams, final long destMailboxId)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