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