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