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