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 onStartCommand(Intent intent, int flags, int startId)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 onBind(Intent intent)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 */ synchronizeMailboxSynchronous(Context context, final Account account, final Mailbox folder, final boolean loadMore, final boolean uiRefresh)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 LocalMessageInfo(Cursor c)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 */ loadUnsyncedMessages(final Context context, final Account account, Folder remoteFolder, ArrayList<Message> messages, final Mailbox toMailbox)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 downloadFlagAndEnvelope(final Context context, final Account account, final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)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 */ synchronizeMailboxGeneric(final Context context, final Account account, final Mailbox mailbox, final boolean loadMore, final boolean uiRefresh)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 */ processPendingActionsSynchronous(Context context, Account account)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 */ getRemoteMailboxForMessage( Context context, EmailContent.Message message)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 */ processPendingDeletesSynchronous(Context context, Account account, String[] accountIdArgs)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 */ processPendingUploadsSynchronous(Context context, Account account, String[] accountIdArgs)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 */ processPendingUpdatesSynchronous(Context context, Account account, String[] accountIdArgs)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 */ processUploadMessage(Context context, Store remoteStore, Mailbox mailbox, long messageId)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 */ processPendingDataChange(final Context context, Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, EmailContent.Message oldMessage, final EmailContent.Message newMessage)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 */ processPendingMoveToTrash(final Context context, Store remoteStore, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage)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 */ processPendingDeleteFromTrash(Store remoteStore, Mailbox oldMailbox, EmailContent.Message oldMessage)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 */ processPendingAppend(Context context, Store remoteStore, Mailbox mailbox, EmailContent.Message message)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 SortableMessage(Message message, long uid)1379 SortableMessage(Message message, long uid) { 1380 mMessage = message; 1381 mUid = uid; 1382 } 1383 } 1384 searchMailboxImpl(final Context context, final long accountId, final SearchParams searchParams, final long destMailboxId)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