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