1 /* 2 * Copyright (C) 2008 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; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.net.TrafficStats; 25 import android.net.Uri; 26 import android.os.Process; 27 import android.text.TextUtils; 28 import android.util.Log; 29 30 import com.android.email.mail.Sender; 31 import com.android.email.mail.Store; 32 import com.android.emailcommon.Logging; 33 import com.android.emailcommon.TrafficFlags; 34 import com.android.emailcommon.internet.MimeBodyPart; 35 import com.android.emailcommon.internet.MimeHeader; 36 import com.android.emailcommon.internet.MimeMultipart; 37 import com.android.emailcommon.internet.MimeUtility; 38 import com.android.emailcommon.mail.AuthenticationFailedException; 39 import com.android.emailcommon.mail.FetchProfile; 40 import com.android.emailcommon.mail.Flag; 41 import com.android.emailcommon.mail.Folder; 42 import com.android.emailcommon.mail.Folder.FolderType; 43 import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 44 import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; 45 import com.android.emailcommon.mail.Folder.OpenMode; 46 import com.android.emailcommon.mail.Message; 47 import com.android.emailcommon.mail.MessagingException; 48 import com.android.emailcommon.mail.Part; 49 import com.android.emailcommon.provider.Account; 50 import com.android.emailcommon.provider.EmailContent; 51 import com.android.emailcommon.provider.EmailContent.Attachment; 52 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 53 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 54 import com.android.emailcommon.provider.EmailContent.MessageColumns; 55 import com.android.emailcommon.provider.EmailContent.SyncColumns; 56 import com.android.emailcommon.provider.Mailbox; 57 import com.android.emailcommon.service.SearchParams; 58 import com.android.emailcommon.utility.AttachmentUtilities; 59 import com.android.emailcommon.utility.ConversionUtilities; 60 import com.android.emailcommon.utility.Utility; 61 62 import java.io.IOException; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.Comparator; 66 import java.util.Date; 67 import java.util.HashMap; 68 import java.util.HashSet; 69 import java.util.concurrent.BlockingQueue; 70 import java.util.concurrent.LinkedBlockingQueue; 71 72 /** 73 * Starts a long running (application) Thread that will run through commands 74 * that require remote mailbox access. This class is used to serialize and 75 * prioritize these commands. Each method that will submit a command requires a 76 * MessagingListener instance to be provided. It is expected that that listener 77 * has also been added as a registered listener using addListener(). When a 78 * command is to be executed, if the listener that was provided with the command 79 * is no longer registered the command is skipped. The design idea for the above 80 * is that when an Activity starts it registers as a listener. When it is paused 81 * it removes itself. Thus, any commands that that activity submitted are 82 * removed from the queue once the activity is no longer active. 83 */ 84 public class MessagingController implements Runnable { 85 86 /** 87 * The maximum message size that we'll consider to be "small". A small message is downloaded 88 * in full immediately instead of in pieces. Anything over this size will be downloaded in 89 * pieces with attachments being left off completely and downloaded on demand. 90 * 91 * 92 * 25k for a "small" message was picked by educated trial and error. 93 * http://answers.google.com/answers/threadview?id=312463 claims that the 94 * average size of an email is 59k, which I feel is too large for our 95 * blind download. The following tests were performed on a download of 96 * 25 random messages. 97 * <pre> 98 * 5k - 61 seconds, 99 * 25k - 51 seconds, 100 * 55k - 53 seconds, 101 * </pre> 102 * So 25k gives good performance and a reasonable data footprint. Sounds good to me. 103 */ 104 private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); 105 106 private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; 107 private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; 108 private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; 109 110 /** 111 * We write this into the serverId field of messages that will never be upsynced. 112 */ 113 private static final String LOCAL_SERVERID_PREFIX = "Local-"; 114 115 /** 116 * Cache search results by account; this allows for "load more" support without having to 117 * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory 118 * shouldn't be an issue 119 */ 120 private static final HashMap<Long, SortableMessage[]> sSearchResults = 121 new HashMap<Long, SortableMessage[]>(); 122 123 private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues(); 124 static { 125 PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI); 126 } 127 128 private static MessagingController sInstance = null; 129 private final BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>(); 130 private final Thread mThread; 131 132 /** 133 * All access to mListeners *must* be synchronized 134 */ 135 private final GroupMessagingListener mListeners = new GroupMessagingListener(); 136 private boolean mBusy; 137 private final Context mContext; 138 private final Controller mController; 139 140 /** 141 * Simple cache for last search result mailbox by account and serverId, since the most common 142 * case will be repeated use of the same mailbox 143 */ 144 private long mLastSearchAccountKey = Account.NO_ACCOUNT; 145 private String mLastSearchServerId = null; 146 private Mailbox mLastSearchRemoteMailbox = null; 147 MessagingController(Context _context, Controller _controller)148 protected MessagingController(Context _context, Controller _controller) { 149 mContext = _context.getApplicationContext(); 150 mController = _controller; 151 mThread = new Thread(this); 152 mThread.start(); 153 } 154 155 /** 156 * Gets or creates the singleton instance of MessagingController. Application is used to 157 * provide a Context to classes that need it. 158 */ getInstance(Context _context, Controller _controller)159 public synchronized static MessagingController getInstance(Context _context, 160 Controller _controller) { 161 if (sInstance == null) { 162 sInstance = new MessagingController(_context, _controller); 163 } 164 return sInstance; 165 } 166 167 /** 168 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 169 */ injectMockController(MessagingController mockController)170 public static void injectMockController(MessagingController mockController) { 171 sInstance = mockController; 172 } 173 174 // TODO: seems that this reading of mBusy isn't thread-safe isBusy()175 public boolean isBusy() { 176 return mBusy; 177 } 178 run()179 public void run() { 180 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 181 // TODO: add an end test to this infinite loop 182 while (true) { 183 Command command; 184 try { 185 command = mCommands.take(); 186 } catch (InterruptedException e) { 187 continue; //re-test the condition on the eclosing while 188 } 189 if (command.listener == null || isActiveListener(command.listener)) { 190 mBusy = true; 191 command.runnable.run(); 192 mListeners.controllerCommandCompleted(mCommands.size() > 0); 193 } 194 mBusy = false; 195 } 196 } 197 put(String description, MessagingListener listener, Runnable runnable)198 private void put(String description, MessagingListener listener, Runnable runnable) { 199 try { 200 Command command = new Command(); 201 command.listener = listener; 202 command.runnable = runnable; 203 command.description = description; 204 mCommands.add(command); 205 } 206 catch (IllegalStateException ie) { 207 throw new Error(ie); 208 } 209 } 210 addListener(MessagingListener listener)211 public void addListener(MessagingListener listener) { 212 mListeners.addListener(listener); 213 } 214 removeListener(MessagingListener listener)215 public void removeListener(MessagingListener listener) { 216 mListeners.removeListener(listener); 217 } 218 isActiveListener(MessagingListener listener)219 private boolean isActiveListener(MessagingListener listener) { 220 return mListeners.isActiveListener(listener); 221 } 222 223 private static final int MAILBOX_COLUMN_ID = 0; 224 private static final int MAILBOX_COLUMN_SERVER_ID = 1; 225 private static final int MAILBOX_COLUMN_TYPE = 2; 226 227 /** Small projection for just the columns required for a sync. */ 228 private static final String[] MAILBOX_PROJECTION = new String[] { 229 MailboxColumns.ID, 230 MailboxColumns.SERVER_ID, 231 MailboxColumns.TYPE, 232 }; 233 234 /** 235 * Synchronize the folder list with the remote server. Synchronization occurs in the 236 * background and results are passed through the {@link MessagingListener}. If the 237 * given listener is not {@code null}, it must have been previously added to the set 238 * of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no 239 * actions will be performed. 240 * 241 * TODO this needs to cache the remote folder list 242 * TODO break out an inner listFoldersSynchronized which could simplify checkMail 243 * 244 * @param accountId ID of the account for which to list the folders 245 * @param listener A listener to notify 246 */ listFolders(final long accountId, MessagingListener listener)247 void listFolders(final long accountId, MessagingListener listener) { 248 final Account account = Account.restoreAccountWithId(mContext, accountId); 249 if (account == null) { 250 Log.i(Logging.LOG_TAG, "Could not load account id " + accountId 251 + ". Has it been removed?"); 252 return; 253 } 254 mListeners.listFoldersStarted(accountId); 255 put("listFolders", listener, new Runnable() { 256 // TODO For now, mailbox addition occurs in the server-dependent store implementation, 257 // but, mailbox removal occurs here. Instead, each store should be responsible for 258 // content synchronization (addition AND removal) since each store will likely need 259 // to implement it's own, unique synchronization methodology. 260 public void run() { 261 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 262 Cursor localFolderCursor = null; 263 try { 264 // Step 1: Get remote mailboxes 265 Store store = Store.getInstance(account, mContext); 266 Folder[] remoteFolders = store.updateFolders(); 267 HashSet<String> remoteFolderNames = new HashSet<String>(); 268 for (int i = 0, count = remoteFolders.length; i < count; i++) { 269 remoteFolderNames.add(remoteFolders[i].getName()); 270 } 271 272 // Step 2: Get local mailboxes 273 localFolderCursor = mContext.getContentResolver().query( 274 Mailbox.CONTENT_URI, 275 MAILBOX_PROJECTION, 276 EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", 277 new String[] { String.valueOf(account.mId) }, 278 null); 279 280 // Step 3: Remove any local mailbox not on the remote list 281 while (localFolderCursor.moveToNext()) { 282 String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); 283 // Short circuit if we have a remote mailbox with the same name 284 if (remoteFolderNames.contains(mailboxPath)) { 285 continue; 286 } 287 288 int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); 289 long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); 290 switch (mailboxType) { 291 case Mailbox.TYPE_INBOX: 292 case Mailbox.TYPE_DRAFTS: 293 case Mailbox.TYPE_OUTBOX: 294 case Mailbox.TYPE_SENT: 295 case Mailbox.TYPE_TRASH: 296 case Mailbox.TYPE_SEARCH: 297 // Never, ever delete special mailboxes 298 break; 299 default: 300 // Drop all attachment files related to this mailbox 301 AttachmentUtilities.deleteAllMailboxAttachmentFiles( 302 mContext, accountId, mailboxId); 303 // Delete the mailbox; database triggers take care of related 304 // Message, Body and Attachment records 305 Uri uri = ContentUris.withAppendedId( 306 Mailbox.CONTENT_URI, mailboxId); 307 mContext.getContentResolver().delete(uri, null, null); 308 break; 309 } 310 } 311 mListeners.listFoldersFinished(accountId); 312 } catch (Exception e) { 313 mListeners.listFoldersFailed(accountId, e.toString()); 314 } finally { 315 if (localFolderCursor != null) { 316 localFolderCursor.close(); 317 } 318 } 319 } 320 }); 321 } 322 323 /** 324 * Start background synchronization of the specified folder. 325 * @param account 326 * @param folder 327 * @param listener 328 */ synchronizeMailbox(final Account account, final Mailbox folder, MessagingListener listener)329 public void synchronizeMailbox(final Account account, 330 final Mailbox folder, MessagingListener listener) { 331 /* 332 * We don't ever sync the Outbox. 333 */ 334 if (folder.mType == Mailbox.TYPE_OUTBOX) { 335 return; 336 } 337 mListeners.synchronizeMailboxStarted(account.mId, folder.mId); 338 put("synchronizeMailbox", listener, new Runnable() { 339 public void run() { 340 synchronizeMailboxSynchronous(account, folder); 341 } 342 }); 343 } 344 345 /** 346 * Start foreground synchronization of the specified folder. This is called by 347 * synchronizeMailbox or checkMail. 348 * TODO this should use ID's instead of fully-restored objects 349 * @param account 350 * @param folder 351 */ synchronizeMailboxSynchronous(final Account account, final Mailbox folder)352 private void synchronizeMailboxSynchronous(final Account account, 353 final Mailbox folder) { 354 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 355 mListeners.synchronizeMailboxStarted(account.mId, folder.mId); 356 if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { 357 // We don't hold messages, so, nothing to synchronize 358 mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null); 359 return; 360 } 361 NotificationController nc = NotificationController.getInstance(mContext); 362 try { 363 processPendingActionsSynchronous(account); 364 365 // Select generic sync or store-specific sync 366 SyncResults results = synchronizeMailboxGeneric(account, folder); 367 mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 368 results.mTotalMessages, 369 results.mAddedMessages.size(), 370 results.mAddedMessages); 371 // Clear authentication notification for this account 372 nc.cancelLoginFailedNotification(account.mId); 373 } catch (MessagingException e) { 374 if (Logging.LOGD) { 375 Log.v(Logging.LOG_TAG, "synchronizeMailbox", e); 376 } 377 if (e instanceof AuthenticationFailedException) { 378 // Generate authentication notification 379 nc.showLoginFailedNotification(account.mId); 380 } 381 mListeners.synchronizeMailboxFailed(account.mId, folder.mId, e); 382 } 383 } 384 385 /** 386 * Lightweight record for the first pass of message sync, where I'm just seeing if 387 * the local message requires sync. Later (for messages that need syncing) we'll do a full 388 * readout from the DB. 389 */ 390 private static class LocalMessageInfo { 391 private static final int COLUMN_ID = 0; 392 private static final int COLUMN_FLAG_READ = 1; 393 private static final int COLUMN_FLAG_FAVORITE = 2; 394 private static final int COLUMN_FLAG_LOADED = 3; 395 private static final int COLUMN_SERVER_ID = 4; 396 private static final int COLUMN_FLAGS = 7; 397 private static final String[] PROJECTION = new String[] { 398 EmailContent.RECORD_ID, 399 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED, 400 SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 401 MessageColumns.FLAGS 402 }; 403 404 final long mId; 405 final boolean mFlagRead; 406 final boolean mFlagFavorite; 407 final int mFlagLoaded; 408 final String mServerId; 409 final int mFlags; 410 LocalMessageInfo(Cursor c)411 public LocalMessageInfo(Cursor c) { 412 mId = c.getLong(COLUMN_ID); 413 mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0; 414 mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0; 415 mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); 416 mServerId = c.getString(COLUMN_SERVER_ID); 417 mFlags = c.getInt(COLUMN_FLAGS); 418 // Note: mailbox key and account key not needed - they are projected for the SELECT 419 } 420 } 421 saveOrUpdate(EmailContent content, Context context)422 private void saveOrUpdate(EmailContent content, Context context) { 423 if (content.isSaved()) { 424 content.update(context, content.toContentValues()); 425 } else { 426 content.save(context); 427 } 428 } 429 430 /** 431 * Load the structure and body of messages not yet synced 432 * @param account the account we're syncing 433 * @param remoteFolder the (open) Folder we're working on 434 * @param unsyncedMessages an array of Message's we've got headers for 435 * @param toMailbox the destination mailbox we're syncing 436 * @throws MessagingException 437 */ loadUnsyncedMessages(final Account account, Folder remoteFolder, ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)438 void loadUnsyncedMessages(final Account account, Folder remoteFolder, 439 ArrayList<Message> unsyncedMessages, final Mailbox toMailbox) 440 throws MessagingException { 441 442 // 1. Divide the unsynced messages into small & large (by size) 443 444 // TODO doing this work here (synchronously) is problematic because it prevents the UI 445 // from affecting the order (e.g. download a message because the user requested it.) Much 446 // of this logic should move out to a different sync loop that attempts to update small 447 // groups of messages at a time, as a background task. However, we can't just return 448 // (yet) because POP messages don't have an envelope yet.... 449 450 ArrayList<Message> largeMessages = new ArrayList<Message>(); 451 ArrayList<Message> smallMessages = new ArrayList<Message>(); 452 for (Message message : unsyncedMessages) { 453 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 454 largeMessages.add(message); 455 } else { 456 smallMessages.add(message); 457 } 458 } 459 460 // 2. Download small messages 461 462 // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope, 463 // this is going to be inefficient and duplicate work we've already done. 2. It's going 464 // back to the DB for a local message that we already had (and discarded). 465 466 // For small messages, we specify "body", which returns everything (incl. attachments) 467 FetchProfile fp = new FetchProfile(); 468 fp.add(FetchProfile.Item.BODY); 469 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, 470 new MessageRetrievalListener() { 471 public void messageRetrieved(Message message) { 472 // Store the updated message locally and mark it fully loaded 473 copyOneMessageToProvider(message, account, toMailbox, 474 EmailContent.Message.FLAG_LOADED_COMPLETE); 475 } 476 477 @Override 478 public void loadAttachmentProgress(int progress) { 479 } 480 }); 481 482 // 3. Download large messages. We ask the server to give us the message structure, 483 // but not all of the attachments. 484 fp.clear(); 485 fp.add(FetchProfile.Item.STRUCTURE); 486 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); 487 for (Message message : largeMessages) { 488 if (message.getBody() == null) { 489 // POP doesn't support STRUCTURE mode, so we'll just do a partial download 490 // (hopefully enough to see some/all of the body) and mark the message for 491 // further download. 492 fp.clear(); 493 fp.add(FetchProfile.Item.BODY_SANE); 494 // TODO a good optimization here would be to make sure that all Stores set 495 // the proper size after this fetch and compare the before and after size. If 496 // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 497 remoteFolder.fetch(new Message[] { message }, fp, null); 498 499 // Store the partially-loaded message and mark it partially loaded 500 copyOneMessageToProvider(message, account, toMailbox, 501 EmailContent.Message.FLAG_LOADED_PARTIAL); 502 } else { 503 // We have a structure to deal with, from which 504 // we can pull down the parts we want to actually store. 505 // Build a list of parts we are interested in. Text parts will be downloaded 506 // right now, attachments will be left for later. 507 ArrayList<Part> viewables = new ArrayList<Part>(); 508 ArrayList<Part> attachments = new ArrayList<Part>(); 509 MimeUtility.collectParts(message, viewables, attachments); 510 // Download the viewables immediately 511 for (Part part : viewables) { 512 fp.clear(); 513 fp.add(part); 514 // TODO what happens if the network connection dies? We've got partial 515 // messages with incorrect status stored. 516 remoteFolder.fetch(new Message[] { message }, fp, null); 517 } 518 // Store the updated message locally and mark it fully loaded 519 copyOneMessageToProvider(message, account, toMailbox, 520 EmailContent.Message.FLAG_LOADED_COMPLETE); 521 } 522 } 523 524 } 525 downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages, HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)526 public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, 527 Folder remoteFolder, ArrayList<Message> unsyncedMessages, 528 HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages) 529 throws MessagingException { 530 FetchProfile fp = new FetchProfile(); 531 fp.add(FetchProfile.Item.FLAGS); 532 fp.add(FetchProfile.Item.ENVELOPE); 533 534 final HashMap<String, LocalMessageInfo> localMapCopy; 535 if (localMessageMap != null) 536 localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap); 537 else { 538 localMapCopy = new HashMap<String, LocalMessageInfo>(); 539 } 540 541 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 542 new MessageRetrievalListener() { 543 @Override 544 public void messageRetrieved(Message message) { 545 try { 546 // Determine if the new message was already known (e.g. partial) 547 // And create or reload the full message info 548 LocalMessageInfo localMessageInfo = 549 localMapCopy.get(message.getUid()); 550 EmailContent.Message localMessage = null; 551 if (localMessageInfo == null) { 552 localMessage = new EmailContent.Message(); 553 } else { 554 localMessage = EmailContent.Message.restoreMessageWithId( 555 mContext, localMessageInfo.mId); 556 } 557 558 if (localMessage != null) { 559 try { 560 // Copy the fields that are available into the message 561 LegacyConversions.updateMessageFields(localMessage, 562 message, account.mId, mailbox.mId); 563 // Commit the message to the local store 564 saveOrUpdate(localMessage, mContext); 565 // Track the "new" ness of the downloaded message 566 if (!message.isSet(Flag.SEEN) && unseenMessages != null) { 567 unseenMessages.add(localMessage.mId); 568 } 569 } catch (MessagingException me) { 570 Log.e(Logging.LOG_TAG, 571 "Error while copying downloaded message." + me); 572 } 573 574 } 575 } 576 catch (Exception e) { 577 Log.e(Logging.LOG_TAG, 578 "Error while storing downloaded message." + e.toString()); 579 } 580 } 581 582 @Override 583 public void loadAttachmentProgress(int progress) { 584 } 585 }); 586 587 } 588 589 /** 590 * A message and numeric uid that's easily sortable 591 */ 592 private static class SortableMessage { 593 private final Message mMessage; 594 private final long mUid; 595 SortableMessage(Message message, long uid)596 SortableMessage(Message message, long uid) { 597 mMessage = message; 598 mUid = uid; 599 } 600 } 601 searchMailbox(long accountId, SearchParams searchParams, long destMailboxId)602 public int searchMailbox(long accountId, SearchParams searchParams, long destMailboxId) 603 throws MessagingException { 604 try { 605 return searchMailboxImpl(accountId, searchParams, destMailboxId); 606 } finally { 607 // Tell UI that we're done loading any search results (no harm calling this even if we 608 // encountered an error or never sent a "started" message) 609 mListeners.synchronizeMailboxFinished(accountId, destMailboxId, 0, 0, null); 610 } 611 } 612 searchMailboxImpl(long accountId, SearchParams searchParams, final long destMailboxId)613 private int searchMailboxImpl(long accountId, SearchParams searchParams, 614 final long destMailboxId) throws MessagingException { 615 final Account account = Account.restoreAccountWithId(mContext, accountId); 616 final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); 617 final Mailbox destMailbox = Mailbox.restoreMailboxWithId(mContext, destMailboxId); 618 if (account == null || mailbox == null || destMailbox == null) { 619 Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams 620 + " but account or mailbox information was missing"); 621 return 0; 622 } 623 624 // Tell UI that we're loading messages 625 mListeners.synchronizeMailboxStarted(accountId, destMailbox.mId); 626 627 Store remoteStore = Store.getInstance(account, mContext); 628 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 629 remoteFolder.open(OpenMode.READ_WRITE); 630 631 SortableMessage[] sortableMessages = new SortableMessage[0]; 632 if (searchParams.mOffset == 0) { 633 // Get the "bare" messages (basically uid) 634 Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); 635 int remoteCount = remoteMessages.length; 636 if (remoteCount > 0) { 637 sortableMessages = new SortableMessage[remoteCount]; 638 int i = 0; 639 for (Message msg : remoteMessages) { 640 sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid())); 641 } 642 // Sort the uid's, most recent first 643 // Note: Not all servers will be nice and return results in the order of request; 644 // those that do will see messages arrive from newest to oldest 645 Arrays.sort(sortableMessages, new Comparator<SortableMessage>() { 646 @Override 647 public int compare(SortableMessage lhs, SortableMessage rhs) { 648 return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0; 649 } 650 }); 651 sSearchResults.put(accountId, sortableMessages); 652 } 653 } else { 654 sortableMessages = sSearchResults.get(accountId); 655 } 656 657 final int numSearchResults = sortableMessages.length; 658 final int numToLoad = 659 Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); 660 if (numToLoad <= 0) { 661 return 0; 662 } 663 664 final ArrayList<Message> messageList = new ArrayList<Message>(); 665 for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; i++) { 666 messageList.add(sortableMessages[i].mMessage); 667 } 668 // Get everything in one pass, rather than two (as in sync); this starts getting us 669 // usable results quickly. 670 FetchProfile fp = new FetchProfile(); 671 fp.add(FetchProfile.Item.FLAGS); 672 fp.add(FetchProfile.Item.ENVELOPE); 673 fp.add(FetchProfile.Item.STRUCTURE); 674 fp.add(FetchProfile.Item.BODY_SANE); 675 remoteFolder.fetch(messageList.toArray(new Message[0]), fp, 676 new MessageRetrievalListener() { 677 public void messageRetrieved(Message message) { 678 try { 679 // Determine if the new message was already known (e.g. partial) 680 // And create or reload the full message info 681 EmailContent.Message localMessage = new EmailContent.Message(); 682 try { 683 // Copy the fields that are available into the message 684 LegacyConversions.updateMessageFields(localMessage, 685 message, account.mId, mailbox.mId); 686 // Commit the message to the local store 687 saveOrUpdate(localMessage, mContext); 688 localMessage.mMailboxKey = destMailboxId; 689 // We load 50k or so; maybe it's complete, maybe not... 690 int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; 691 // We store the serverId of the source mailbox into protocolSearchInfo 692 // This will be used by loadMessageForView, etc. to use the proper remote 693 // folder 694 localMessage.mProtocolSearchInfo = mailbox.mServerId; 695 if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { 696 flag = EmailContent.Message.FLAG_LOADED_PARTIAL; 697 } 698 copyOneMessageToProvider(message, localMessage, flag, mContext); 699 } catch (MessagingException me) { 700 Log.e(Logging.LOG_TAG, 701 "Error while copying downloaded message." + me); 702 } 703 } catch (Exception e) { 704 Log.e(Logging.LOG_TAG, 705 "Error while storing downloaded message." + e.toString()); 706 } 707 } 708 709 @Override 710 public void loadAttachmentProgress(int progress) { 711 } 712 }); 713 return numSearchResults; 714 } 715 716 717 /** 718 * Generic synchronizer - used for POP3 and IMAP. 719 * 720 * TODO Break this method up into smaller chunks. 721 * 722 * @param account the account to sync 723 * @param mailbox the mailbox to sync 724 * @return results of the sync pass 725 * @throws MessagingException 726 */ synchronizeMailboxGeneric(final Account account, final Mailbox mailbox)727 private SyncResults synchronizeMailboxGeneric(final Account account, final Mailbox mailbox) 728 throws MessagingException { 729 730 /* 731 * A list of IDs for messages that were downloaded and did not have the seen flag set. 732 * This serves as the "true" new message count reported to the user via notification. 733 */ 734 final ArrayList<Long> unseenMessages = new ArrayList<Long>(); 735 736 Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***"); 737 ContentResolver resolver = mContext.getContentResolver(); 738 739 // 0. We do not ever sync DRAFTS or OUTBOX (down or up) 740 if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { 741 int totalMessages = EmailContent.count(mContext, mailbox.getUri(), null, null); 742 return new SyncResults(totalMessages, unseenMessages); 743 } 744 745 // 1. Get the message list from the local store and create an index of the uids 746 747 Cursor localUidCursor = null; 748 HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>(); 749 750 try { 751 localUidCursor = resolver.query( 752 EmailContent.Message.CONTENT_URI, 753 LocalMessageInfo.PROJECTION, 754 EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + 755 " AND " + MessageColumns.MAILBOX_KEY + "=?", 756 new String[] { 757 String.valueOf(account.mId), 758 String.valueOf(mailbox.mId) 759 }, 760 null); 761 while (localUidCursor.moveToNext()) { 762 LocalMessageInfo info = new LocalMessageInfo(localUidCursor); 763 localMessageMap.put(info.mServerId, info); 764 } 765 } finally { 766 if (localUidCursor != null) { 767 localUidCursor.close(); 768 } 769 } 770 771 // 2. Open the remote folder and create the remote folder if necessary 772 773 Store remoteStore = Store.getInstance(account, mContext); 774 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 775 776 /* 777 * If the folder is a "special" folder we need to see if it exists 778 * on the remote server. It if does not exist we'll try to create it. If we 779 * can't create we'll abort. This will happen on every single Pop3 folder as 780 * designed and on Imap folders during error conditions. This allows us 781 * to treat Pop3 and Imap the same in this code. 782 */ 783 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT 784 || mailbox.mType == Mailbox.TYPE_DRAFTS) { 785 if (!remoteFolder.exists()) { 786 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 787 return new SyncResults(0, unseenMessages); 788 } 789 } 790 } 791 792 // 3, Open the remote folder. This pre-loads certain metadata like message count. 793 remoteFolder.open(OpenMode.READ_WRITE); 794 795 // 4. Trash any remote messages that are marked as trashed locally. 796 // TODO - this comment was here, but no code was here. 797 798 // 5. Get the remote message count. 799 int remoteMessageCount = remoteFolder.getMessageCount(); 800 801 // 6. Determine the limit # of messages to download 802 int visibleLimit = mailbox.mVisibleLimit; 803 if (visibleLimit <= 0) { 804 visibleLimit = Email.VISIBLE_LIMIT_DEFAULT; 805 } 806 807 // 7. Create a list of messages to download 808 Message[] remoteMessages = new Message[0]; 809 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 810 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 811 812 int newMessageCount = 0; 813 if (remoteMessageCount > 0) { 814 /* 815 * Message numbers start at 1. 816 */ 817 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 818 int remoteEnd = remoteMessageCount; 819 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 820 // TODO Why are we running through the list twice? Combine w/ for loop below 821 for (Message message : remoteMessages) { 822 remoteUidMap.put(message.getUid(), message); 823 } 824 825 /* 826 * Get a list of the messages that are in the remote list but not on the 827 * local store, or messages that are in the local store but failed to download 828 * on the last sync. These are the new messages that we will download. 829 * Note, we also skip syncing messages which are flagged as "deleted message" sentinels, 830 * because they are locally deleted and we don't need or want the old message from 831 * the server. 832 */ 833 for (Message message : remoteMessages) { 834 LocalMessageInfo localMessage = localMessageMap.get(message.getUid()); 835 if (localMessage == null) { 836 newMessageCount++; 837 } 838 // localMessage == null -> message has never been created (not even headers) 839 // mFlagLoaded = UNLOADED -> message created, but none of body loaded 840 // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded 841 // mFlagLoaded = COMPLETE -> message body has been completely loaded 842 // mFlagLoaded = DELETED -> message has been deleted 843 // Only the first two of these are "unsynced", so let's retrieve them 844 if (localMessage == null || 845 (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) { 846 unsyncedMessages.add(message); 847 } 848 } 849 } 850 851 // 8. Download basic info about the new/unloaded messages (if any) 852 /* 853 * Fetch the flags and envelope only of the new messages. This is intended to get us 854 * critical data as fast as possible, and then we'll fill in the details. 855 */ 856 if (unsyncedMessages.size() > 0) { 857 downloadFlagAndEnvelope(account, mailbox, remoteFolder, unsyncedMessages, 858 localMessageMap, unseenMessages); 859 } 860 861 // 9. Refresh the flags for any messages in the local store that we didn't just download. 862 FetchProfile fp = new FetchProfile(); 863 fp.add(FetchProfile.Item.FLAGS); 864 remoteFolder.fetch(remoteMessages, fp, null); 865 boolean remoteSupportsSeen = false; 866 boolean remoteSupportsFlagged = false; 867 boolean remoteSupportsAnswered = false; 868 for (Flag flag : remoteFolder.getPermanentFlags()) { 869 if (flag == Flag.SEEN) { 870 remoteSupportsSeen = true; 871 } 872 if (flag == Flag.FLAGGED) { 873 remoteSupportsFlagged = true; 874 } 875 if (flag == Flag.ANSWERED) { 876 remoteSupportsAnswered = true; 877 } 878 } 879 // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3) 880 if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) { 881 for (Message remoteMessage : remoteMessages) { 882 LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid()); 883 if (localMessageInfo == null) { 884 continue; 885 } 886 boolean localSeen = localMessageInfo.mFlagRead; 887 boolean remoteSeen = remoteMessage.isSet(Flag.SEEN); 888 boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen)); 889 boolean localFlagged = localMessageInfo.mFlagFavorite; 890 boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED); 891 boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged)); 892 int localFlags = localMessageInfo.mFlags; 893 boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0; 894 boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED); 895 boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered)); 896 if (newSeen || newFlagged || newAnswered) { 897 Uri uri = ContentUris.withAppendedId( 898 EmailContent.Message.CONTENT_URI, localMessageInfo.mId); 899 ContentValues updateValues = new ContentValues(); 900 updateValues.put(MessageColumns.FLAG_READ, remoteSeen); 901 updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged); 902 if (remoteAnswered) { 903 localFlags |= EmailContent.Message.FLAG_REPLIED_TO; 904 } else { 905 localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO; 906 } 907 updateValues.put(MessageColumns.FLAGS, localFlags); 908 resolver.update(uri, updateValues, null, null); 909 } 910 } 911 } 912 913 // 10. Remove any messages that are in the local store but no longer on the remote store. 914 HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet()); 915 localUidsToDelete.removeAll(remoteUidMap.keySet()); 916 for (String uidToDelete : localUidsToDelete) { 917 LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete); 918 919 // Delete associated data (attachment files) 920 // Attachment & Body records are auto-deleted when we delete the Message record 921 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 922 infoToDelete.mId); 923 924 // Delete the message itself 925 Uri uriToDelete = ContentUris.withAppendedId( 926 EmailContent.Message.CONTENT_URI, infoToDelete.mId); 927 resolver.delete(uriToDelete, null, null); 928 929 // Delete extra rows (e.g. synced or deleted) 930 Uri syncRowToDelete = ContentUris.withAppendedId( 931 EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); 932 resolver.delete(syncRowToDelete, null, null); 933 Uri deletERowToDelete = ContentUris.withAppendedId( 934 EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); 935 resolver.delete(deletERowToDelete, null, null); 936 } 937 938 loadUnsyncedMessages(account, remoteFolder, unsyncedMessages, mailbox); 939 940 // 14. Clean up and report results 941 remoteFolder.close(false); 942 943 return new SyncResults(remoteMessageCount, unseenMessages); 944 } 945 946 /** 947 * Copy one downloaded message (which may have partially-loaded sections) 948 * into a newly created EmailProvider Message, given the account and mailbox 949 * 950 * @param message the remote message we've just downloaded 951 * @param account the account it will be stored into 952 * @param folder the mailbox it will be stored into 953 * @param loadStatus when complete, the message will be marked with this status (e.g. 954 * EmailContent.Message.LOADED) 955 */ copyOneMessageToProvider(Message message, Account account, Mailbox folder, int loadStatus)956 public void copyOneMessageToProvider(Message message, Account account, 957 Mailbox folder, int loadStatus) { 958 EmailContent.Message localMessage = null; 959 Cursor c = null; 960 try { 961 c = mContext.getContentResolver().query( 962 EmailContent.Message.CONTENT_URI, 963 EmailContent.Message.CONTENT_PROJECTION, 964 EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + 965 " AND " + MessageColumns.MAILBOX_KEY + "=?" + 966 " AND " + SyncColumns.SERVER_ID + "=?", 967 new String[] { 968 String.valueOf(account.mId), 969 String.valueOf(folder.mId), 970 String.valueOf(message.getUid()) 971 }, 972 null); 973 if (c.moveToNext()) { 974 localMessage = EmailContent.getContent(c, EmailContent.Message.class); 975 localMessage.mMailboxKey = folder.mId; 976 localMessage.mAccountKey = account.mId; 977 copyOneMessageToProvider(message, localMessage, loadStatus, mContext); 978 } 979 } finally { 980 if (c != null) { 981 c.close(); 982 } 983 } 984 } 985 986 /** 987 * Copy one downloaded message (which may have partially-loaded sections) 988 * into an already-created EmailProvider Message 989 * 990 * @param message the remote message we've just downloaded 991 * @param localMessage the EmailProvider Message, already created 992 * @param loadStatus when complete, the message will be marked with this status (e.g. 993 * EmailContent.Message.LOADED) 994 * @param context the context to be used for EmailProvider 995 */ copyOneMessageToProvider(Message message, EmailContent.Message localMessage, int loadStatus, Context context)996 public void copyOneMessageToProvider(Message message, EmailContent.Message localMessage, 997 int loadStatus, Context context) { 998 try { 999 1000 EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context, 1001 localMessage.mId); 1002 if (body == null) { 1003 body = new EmailContent.Body(); 1004 } 1005 try { 1006 // Copy the fields that are available into the message object 1007 LegacyConversions.updateMessageFields(localMessage, message, 1008 localMessage.mAccountKey, localMessage.mMailboxKey); 1009 1010 // Now process body parts & attachments 1011 ArrayList<Part> viewables = new ArrayList<Part>(); 1012 ArrayList<Part> attachments = new ArrayList<Part>(); 1013 MimeUtility.collectParts(message, viewables, attachments); 1014 1015 ConversionUtilities.updateBodyFields(body, localMessage, viewables); 1016 1017 // Commit the message & body to the local store immediately 1018 saveOrUpdate(localMessage, context); 1019 saveOrUpdate(body, context); 1020 1021 // process (and save) attachments 1022 LegacyConversions.updateAttachments(context, localMessage, attachments); 1023 1024 // One last update of message with two updated flags 1025 localMessage.mFlagLoaded = loadStatus; 1026 1027 ContentValues cv = new ContentValues(); 1028 cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment); 1029 cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded); 1030 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, 1031 localMessage.mId); 1032 context.getContentResolver().update(uri, cv, null, null); 1033 1034 } catch (MessagingException me) { 1035 Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me); 1036 } 1037 1038 } catch (RuntimeException rte) { 1039 Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString()); 1040 } catch (IOException ioe) { 1041 Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); 1042 } 1043 } 1044 processPendingActions(final long accountId)1045 public void processPendingActions(final long accountId) { 1046 put("processPendingActions", null, new Runnable() { 1047 public void run() { 1048 try { 1049 Account account = Account.restoreAccountWithId(mContext, accountId); 1050 if (account == null) { 1051 return; 1052 } 1053 processPendingActionsSynchronous(account); 1054 } 1055 catch (MessagingException me) { 1056 if (Logging.LOGD) { 1057 Log.v(Logging.LOG_TAG, "processPendingActions", me); 1058 } 1059 /* 1060 * Ignore any exceptions from the commands. Commands will be processed 1061 * on the next round. 1062 */ 1063 } 1064 } 1065 }); 1066 } 1067 1068 /** 1069 * Find messages in the updated table that need to be written back to server. 1070 * 1071 * Handles: 1072 * Read/Unread 1073 * Flagged 1074 * Append (upload) 1075 * Move To Trash 1076 * Empty trash 1077 * TODO: 1078 * Move 1079 * 1080 * @param account the account to scan for pending actions 1081 * @throws MessagingException 1082 */ processPendingActionsSynchronous(Account account)1083 private void processPendingActionsSynchronous(Account account) 1084 throws MessagingException { 1085 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 1086 ContentResolver resolver = mContext.getContentResolver(); 1087 String[] accountIdArgs = new String[] { Long.toString(account.mId) }; 1088 1089 // Handle deletes first, it's always better to get rid of things first 1090 processPendingDeletesSynchronous(account, resolver, accountIdArgs); 1091 1092 // Handle uploads (currently, only to sent messages) 1093 processPendingUploadsSynchronous(account, resolver, accountIdArgs); 1094 1095 // Now handle updates / upsyncs 1096 processPendingUpdatesSynchronous(account, resolver, accountIdArgs); 1097 } 1098 1099 /** 1100 * Get the mailbox corresponding to the remote location of a message; this will normally be 1101 * the mailbox whose _id is mailboxKey, except for search results, where we must look it up 1102 * by serverId 1103 * @param message the message in question 1104 * @return the mailbox in which the message resides on the server 1105 */ getRemoteMailboxForMessage(EmailContent.Message message)1106 private Mailbox getRemoteMailboxForMessage(EmailContent.Message message) { 1107 // If this is a search result, use the protocolSearchInfo field to get the server info 1108 if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { 1109 long accountKey = message.mAccountKey; 1110 String protocolSearchInfo = message.mProtocolSearchInfo; 1111 if (accountKey == mLastSearchAccountKey && 1112 protocolSearchInfo.equals(mLastSearchServerId)) { 1113 return mLastSearchRemoteMailbox; 1114 } 1115 Cursor c = mContext.getContentResolver().query(Mailbox.CONTENT_URI, 1116 Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION, 1117 new String[] {protocolSearchInfo, Long.toString(accountKey)}, 1118 null); 1119 try { 1120 if (c.moveToNext()) { 1121 Mailbox mailbox = new Mailbox(); 1122 mailbox.restore(c); 1123 mLastSearchAccountKey = accountKey; 1124 mLastSearchServerId = protocolSearchInfo; 1125 mLastSearchRemoteMailbox = mailbox; 1126 return mailbox; 1127 } else { 1128 return null; 1129 } 1130 } finally { 1131 c.close(); 1132 } 1133 } else { 1134 return Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 1135 } 1136 } 1137 1138 /** 1139 * Scan for messages that are in the Message_Deletes table, look for differences that 1140 * we can deal with, and do the work. 1141 * 1142 * @param account 1143 * @param resolver 1144 * @param accountIdArgs 1145 */ processPendingDeletesSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1146 private void processPendingDeletesSynchronous(Account account, 1147 ContentResolver resolver, String[] accountIdArgs) { 1148 Cursor deletes = resolver.query(EmailContent.Message.DELETED_CONTENT_URI, 1149 EmailContent.Message.CONTENT_PROJECTION, 1150 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, 1151 EmailContent.MessageColumns.MAILBOX_KEY); 1152 long lastMessageId = -1; 1153 try { 1154 // Defer setting up the store until we know we need to access it 1155 Store remoteStore = null; 1156 // loop through messages marked as deleted 1157 while (deletes.moveToNext()) { 1158 boolean deleteFromTrash = false; 1159 1160 EmailContent.Message oldMessage = 1161 EmailContent.getContent(deletes, EmailContent.Message.class); 1162 1163 if (oldMessage != null) { 1164 lastMessageId = oldMessage.mId; 1165 1166 Mailbox mailbox = getRemoteMailboxForMessage(oldMessage); 1167 if (mailbox == null) { 1168 continue; // Mailbox removed. Move to the next message. 1169 } 1170 deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH; 1171 1172 // Load the remote store if it will be needed 1173 if (remoteStore == null && deleteFromTrash) { 1174 remoteStore = Store.getInstance(account, mContext); 1175 } 1176 1177 // Dispatch here for specific change types 1178 if (deleteFromTrash) { 1179 // Move message to trash 1180 processPendingDeleteFromTrash(remoteStore, account, mailbox, oldMessage); 1181 } 1182 } 1183 1184 // Finally, delete the update 1185 Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, 1186 oldMessage.mId); 1187 resolver.delete(uri, null, null); 1188 } 1189 } catch (MessagingException me) { 1190 // Presumably an error here is an account connection failure, so there is 1191 // no point in continuing through the rest of the pending updates. 1192 if (Email.DEBUG) { 1193 Log.d(Logging.LOG_TAG, "Unable to process pending delete for id=" 1194 + lastMessageId + ": " + me); 1195 } 1196 } finally { 1197 deletes.close(); 1198 } 1199 } 1200 1201 /** 1202 * Scan for messages that are in Sent, and are in need of upload, 1203 * and send them to the server. "In need of upload" is defined as: 1204 * serverId == null (no UID has been assigned) 1205 * or 1206 * message is in the updated list 1207 * 1208 * Note we also look for messages that are moving from drafts->outbox->sent. They never 1209 * go through "drafts" or "outbox" on the server, so we hang onto these until they can be 1210 * uploaded directly to the Sent folder. 1211 * 1212 * @param account 1213 * @param resolver 1214 * @param accountIdArgs 1215 */ processPendingUploadsSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1216 private void processPendingUploadsSynchronous(Account account, 1217 ContentResolver resolver, String[] accountIdArgs) { 1218 // Find the Sent folder (since that's all we're uploading for now 1219 Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, 1220 MailboxColumns.ACCOUNT_KEY + "=?" 1221 + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT, 1222 accountIdArgs, null); 1223 long lastMessageId = -1; 1224 try { 1225 // Defer setting up the store until we know we need to access it 1226 Store remoteStore = null; 1227 while (mailboxes.moveToNext()) { 1228 long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN); 1229 String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) }; 1230 // Demand load mailbox 1231 Mailbox mailbox = null; 1232 1233 // First handle the "new" messages (serverId == null) 1234 Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI, 1235 EmailContent.Message.ID_PROJECTION, 1236 EmailContent.Message.MAILBOX_KEY + "=?" 1237 + " and (" + EmailContent.Message.SERVER_ID + " is null" 1238 + " or " + EmailContent.Message.SERVER_ID + "=''" + ")", 1239 mailboxKeyArgs, 1240 null); 1241 try { 1242 while (upsyncs1.moveToNext()) { 1243 // Load the remote store if it will be needed 1244 if (remoteStore == null) { 1245 remoteStore = Store.getInstance(account, mContext); 1246 } 1247 // Load the mailbox if it will be needed 1248 if (mailbox == null) { 1249 mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1250 if (mailbox == null) { 1251 continue; // Mailbox removed. Move to the next message. 1252 } 1253 } 1254 // upsync the message 1255 long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); 1256 lastMessageId = id; 1257 processUploadMessage(resolver, remoteStore, account, mailbox, id); 1258 } 1259 } finally { 1260 if (upsyncs1 != null) { 1261 upsyncs1.close(); 1262 } 1263 } 1264 1265 // Next, handle any updates (e.g. edited in place, although this shouldn't happen) 1266 Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, 1267 EmailContent.Message.ID_PROJECTION, 1268 EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs, 1269 null); 1270 try { 1271 while (upsyncs2.moveToNext()) { 1272 // Load the remote store if it will be needed 1273 if (remoteStore == null) { 1274 remoteStore = Store.getInstance(account, mContext); 1275 } 1276 // Load the mailbox if it will be needed 1277 if (mailbox == null) { 1278 mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1279 if (mailbox == null) { 1280 continue; // Mailbox removed. Move to the next message. 1281 } 1282 } 1283 // upsync the message 1284 long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); 1285 lastMessageId = id; 1286 processUploadMessage(resolver, remoteStore, account, mailbox, id); 1287 } 1288 } finally { 1289 if (upsyncs2 != null) { 1290 upsyncs2.close(); 1291 } 1292 } 1293 } 1294 } catch (MessagingException me) { 1295 // Presumably an error here is an account connection failure, so there is 1296 // no point in continuing through the rest of the pending updates. 1297 if (Email.DEBUG) { 1298 Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id=" 1299 + lastMessageId + ": " + me); 1300 } 1301 } finally { 1302 if (mailboxes != null) { 1303 mailboxes.close(); 1304 } 1305 } 1306 } 1307 1308 /** 1309 * Scan for messages that are in the Message_Updates table, look for differences that 1310 * we can deal with, and do the work. 1311 * 1312 * @param account 1313 * @param resolver 1314 * @param accountIdArgs 1315 */ processPendingUpdatesSynchronous(Account account, ContentResolver resolver, String[] accountIdArgs)1316 private void processPendingUpdatesSynchronous(Account account, 1317 ContentResolver resolver, String[] accountIdArgs) { 1318 Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, 1319 EmailContent.Message.CONTENT_PROJECTION, 1320 EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, 1321 EmailContent.MessageColumns.MAILBOX_KEY); 1322 long lastMessageId = -1; 1323 try { 1324 // Defer setting up the store until we know we need to access it 1325 Store remoteStore = null; 1326 // Demand load mailbox (note order-by to reduce thrashing here) 1327 Mailbox mailbox = null; 1328 // loop through messages marked as needing updates 1329 while (updates.moveToNext()) { 1330 boolean changeMoveToTrash = false; 1331 boolean changeRead = false; 1332 boolean changeFlagged = false; 1333 boolean changeMailbox = false; 1334 boolean changeAnswered = false; 1335 1336 EmailContent.Message oldMessage = 1337 EmailContent.getContent(updates, EmailContent.Message.class); 1338 lastMessageId = oldMessage.mId; 1339 EmailContent.Message newMessage = 1340 EmailContent.Message.restoreMessageWithId(mContext, oldMessage.mId); 1341 if (newMessage != null) { 1342 mailbox = Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey); 1343 if (mailbox == null) { 1344 continue; // Mailbox removed. Move to the next message. 1345 } 1346 if (oldMessage.mMailboxKey != newMessage.mMailboxKey) { 1347 if (mailbox.mType == Mailbox.TYPE_TRASH) { 1348 changeMoveToTrash = true; 1349 } else { 1350 changeMailbox = true; 1351 } 1352 } 1353 changeRead = oldMessage.mFlagRead != newMessage.mFlagRead; 1354 changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite; 1355 changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 1356 (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO); 1357 } 1358 1359 // Load the remote store if it will be needed 1360 if (remoteStore == null && 1361 (changeMoveToTrash || changeRead || changeFlagged || changeMailbox || 1362 changeAnswered)) { 1363 remoteStore = Store.getInstance(account, mContext); 1364 } 1365 1366 // Dispatch here for specific change types 1367 if (changeMoveToTrash) { 1368 // Move message to trash 1369 processPendingMoveToTrash(remoteStore, account, mailbox, oldMessage, 1370 newMessage); 1371 } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) { 1372 processPendingDataChange(remoteStore, mailbox, changeRead, changeFlagged, 1373 changeMailbox, changeAnswered, oldMessage, newMessage); 1374 } 1375 1376 // Finally, delete the update 1377 Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, 1378 oldMessage.mId); 1379 resolver.delete(uri, null, null); 1380 } 1381 1382 } catch (MessagingException me) { 1383 // Presumably an error here is an account connection failure, so there is 1384 // no point in continuing through the rest of the pending updates. 1385 if (Email.DEBUG) { 1386 Log.d(Logging.LOG_TAG, "Unable to process pending update for id=" 1387 + lastMessageId + ": " + me); 1388 } 1389 } finally { 1390 updates.close(); 1391 } 1392 } 1393 1394 /** 1395 * Upsync an entire message. This must also unwind whatever triggered it (either by 1396 * updating the serverId, or by deleting the update record, or it's going to keep happening 1397 * over and over again. 1398 * 1399 * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload. 1400 * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select 1401 * only the Drafts and Sent folders, this can happen when the update record and the current 1402 * record mismatch. In this case, we let the update record remain, because the filters 1403 * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it) 1404 * appropriately. 1405 * 1406 * @param resolver 1407 * @param remoteStore 1408 * @param account 1409 * @param mailbox the actual mailbox 1410 * @param messageId 1411 */ processUploadMessage(ContentResolver resolver, Store remoteStore, Account account, Mailbox mailbox, long messageId)1412 private void processUploadMessage(ContentResolver resolver, Store remoteStore, 1413 Account account, Mailbox mailbox, long messageId) 1414 throws MessagingException { 1415 EmailContent.Message newMessage = 1416 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1417 boolean deleteUpdate = false; 1418 if (newMessage == null) { 1419 deleteUpdate = true; 1420 Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); 1421 } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { 1422 deleteUpdate = false; 1423 Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); 1424 } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 1425 deleteUpdate = false; 1426 Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); 1427 } else if (mailbox.mType == Mailbox.TYPE_TRASH) { 1428 deleteUpdate = false; 1429 Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); 1430 } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) { 1431 deleteUpdate = false; 1432 Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); 1433 } else { 1434 Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId); 1435 deleteUpdate = processPendingAppend(remoteStore, account, mailbox, newMessage); 1436 } 1437 if (deleteUpdate) { 1438 // Finally, delete the update (if any) 1439 Uri uri = ContentUris.withAppendedId( 1440 EmailContent.Message.UPDATED_CONTENT_URI, messageId); 1441 resolver.delete(uri, null, null); 1442 } 1443 } 1444 1445 /** 1446 * Upsync changes to read, flagged, or mailbox 1447 * 1448 * @param remoteStore the remote store for this mailbox 1449 * @param mailbox the mailbox the message is stored in 1450 * @param changeRead whether the message's read state has changed 1451 * @param changeFlagged whether the message's flagged state has changed 1452 * @param changeMailbox whether the message's mailbox has changed 1453 * @param oldMessage the message in it's pre-change state 1454 * @param newMessage the current version of the message 1455 */ processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1456 private void processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead, 1457 boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, 1458 EmailContent.Message oldMessage, final EmailContent.Message newMessage) 1459 throws MessagingException { 1460 // New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't 1461 // being moved 1462 Mailbox newMailbox = mailbox; 1463 // Mailbox is the original remote mailbox (the one we're acting on) 1464 mailbox = getRemoteMailboxForMessage(oldMessage); 1465 1466 // 0. No remote update if the message is local-only 1467 if (newMessage.mServerId == null || newMessage.mServerId.equals("") 1468 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) { 1469 return; 1470 } 1471 1472 // 1. No remote update for DRAFTS or OUTBOX 1473 if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { 1474 return; 1475 } 1476 1477 // 2. Open the remote store & folder 1478 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 1479 if (!remoteFolder.exists()) { 1480 return; 1481 } 1482 remoteFolder.open(OpenMode.READ_WRITE); 1483 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1484 return; 1485 } 1486 1487 // 3. Finally, apply the changes to the message 1488 Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); 1489 if (remoteMessage == null) { 1490 return; 1491 } 1492 if (Email.DEBUG) { 1493 Log.d(Logging.LOG_TAG, 1494 "Update for msg id=" + newMessage.mId 1495 + " read=" + newMessage.mFlagRead 1496 + " flagged=" + newMessage.mFlagFavorite 1497 + " answered=" 1498 + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0) 1499 + " new mailbox=" + newMessage.mMailboxKey); 1500 } 1501 Message[] messages = new Message[] { remoteMessage }; 1502 if (changeRead) { 1503 remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead); 1504 } 1505 if (changeFlagged) { 1506 remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite); 1507 } 1508 if (changeAnswered) { 1509 remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED, 1510 (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0); 1511 } 1512 if (changeMailbox) { 1513 Folder toFolder = remoteStore.getFolder(newMailbox.mServerId); 1514 if (!remoteFolder.exists()) { 1515 return; 1516 } 1517 // We may need the message id to search for the message in the destination folder 1518 remoteMessage.setMessageId(newMessage.mMessageId); 1519 // Copy the message to its new folder 1520 remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() { 1521 @Override 1522 public void onMessageUidChange(Message message, String newUid) { 1523 ContentValues cv = new ContentValues(); 1524 cv.put(EmailContent.Message.SERVER_ID, newUid); 1525 // We only have one message, so, any updates _must_ be for it. Otherwise, 1526 // we'd have to cycle through to find the one with the same server ID. 1527 mContext.getContentResolver().update(ContentUris.withAppendedId( 1528 EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null); 1529 } 1530 @Override 1531 public void onMessageNotFound(Message message) { 1532 } 1533 }); 1534 // Delete the message from the remote source folder 1535 remoteMessage.setFlag(Flag.DELETED, true); 1536 remoteFolder.expunge(); 1537 } 1538 remoteFolder.close(false); 1539 } 1540 1541 /** 1542 * Process a pending trash message command. 1543 * 1544 * @param remoteStore the remote store we're working in 1545 * @param account The account in which we are working 1546 * @param newMailbox The local trash mailbox 1547 * @param oldMessage The message copy that was saved in the updates shadow table 1548 * @param newMessage The message that was moved to the mailbox 1549 */ processPendingMoveToTrash(Store remoteStore, Account account, Mailbox newMailbox, EmailContent.Message oldMessage, final EmailContent.Message newMessage)1550 private void processPendingMoveToTrash(Store remoteStore, 1551 Account account, Mailbox newMailbox, EmailContent.Message oldMessage, 1552 final EmailContent.Message newMessage) throws MessagingException { 1553 1554 // 0. No remote move if the message is local-only 1555 if (newMessage.mServerId == null || newMessage.mServerId.equals("") 1556 || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { 1557 return; 1558 } 1559 1560 // 1. Escape early if we can't find the local mailbox 1561 // TODO smaller projection here 1562 Mailbox oldMailbox = getRemoteMailboxForMessage(oldMessage); 1563 if (oldMailbox == null) { 1564 // can't find old mailbox, it may have been deleted. just return. 1565 return; 1566 } 1567 // 2. We don't support delete-from-trash here 1568 if (oldMailbox.mType == Mailbox.TYPE_TRASH) { 1569 return; 1570 } 1571 1572 // 3. If DELETE_POLICY_NEVER, simply write back the deleted sentinel and return 1573 // 1574 // This sentinel takes the place of the server-side message, and locally "deletes" it 1575 // by inhibiting future sync or display of the message. It will eventually go out of 1576 // scope when it becomes old, or is deleted on the server, and the regular sync code 1577 // will clean it up for us. 1578 if (account.getDeletePolicy() == Account.DELETE_POLICY_NEVER) { 1579 EmailContent.Message sentinel = new EmailContent.Message(); 1580 sentinel.mAccountKey = oldMessage.mAccountKey; 1581 sentinel.mMailboxKey = oldMessage.mMailboxKey; 1582 sentinel.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED; 1583 sentinel.mFlagRead = true; 1584 sentinel.mServerId = oldMessage.mServerId; 1585 sentinel.save(mContext); 1586 1587 return; 1588 } 1589 1590 // The rest of this method handles server-side deletion 1591 1592 // 4. Find the remote mailbox (that we deleted from), and open it 1593 Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId); 1594 if (!remoteFolder.exists()) { 1595 return; 1596 } 1597 1598 remoteFolder.open(OpenMode.READ_WRITE); 1599 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1600 remoteFolder.close(false); 1601 return; 1602 } 1603 1604 // 5. Find the remote original message 1605 Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId); 1606 if (remoteMessage == null) { 1607 remoteFolder.close(false); 1608 return; 1609 } 1610 1611 // 6. Find the remote trash folder, and create it if not found 1612 Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId); 1613 if (!remoteTrashFolder.exists()) { 1614 /* 1615 * If the remote trash folder doesn't exist we try to create it. 1616 */ 1617 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 1618 } 1619 1620 // 7. Try to copy the message into the remote trash folder 1621 // Note, this entire section will be skipped for POP3 because there's no remote trash 1622 if (remoteTrashFolder.exists()) { 1623 /* 1624 * Because remoteTrashFolder may be new, we need to explicitly open it 1625 */ 1626 remoteTrashFolder.open(OpenMode.READ_WRITE); 1627 if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { 1628 remoteFolder.close(false); 1629 remoteTrashFolder.close(false); 1630 return; 1631 } 1632 1633 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, 1634 new Folder.MessageUpdateCallbacks() { 1635 public void onMessageUidChange(Message message, String newUid) { 1636 // update the UID in the local trash folder, because some stores will 1637 // have to change it when copying to remoteTrashFolder 1638 ContentValues cv = new ContentValues(); 1639 cv.put(EmailContent.Message.SERVER_ID, newUid); 1640 mContext.getContentResolver().update(newMessage.getUri(), cv, null, null); 1641 } 1642 1643 /** 1644 * This will be called if the deleted message doesn't exist and can't be 1645 * deleted (e.g. it was already deleted from the server.) In this case, 1646 * attempt to delete the local copy as well. 1647 */ 1648 public void onMessageNotFound(Message message) { 1649 mContext.getContentResolver().delete(newMessage.getUri(), null, null); 1650 } 1651 }); 1652 remoteTrashFolder.close(false); 1653 } 1654 1655 // 8. Delete the message from the remote source folder 1656 remoteMessage.setFlag(Flag.DELETED, true); 1657 remoteFolder.expunge(); 1658 remoteFolder.close(false); 1659 } 1660 1661 /** 1662 * Process a pending trash message command. 1663 * 1664 * @param remoteStore the remote store we're working in 1665 * @param account The account in which we are working 1666 * @param oldMailbox The local trash mailbox 1667 * @param oldMessage The message that was deleted from the trash 1668 */ processPendingDeleteFromTrash(Store remoteStore, Account account, Mailbox oldMailbox, EmailContent.Message oldMessage)1669 private void processPendingDeleteFromTrash(Store remoteStore, 1670 Account account, Mailbox oldMailbox, EmailContent.Message oldMessage) 1671 throws MessagingException { 1672 1673 // 1. We only support delete-from-trash here 1674 if (oldMailbox.mType != Mailbox.TYPE_TRASH) { 1675 return; 1676 } 1677 1678 // 2. Find the remote trash folder (that we are deleting from), and open it 1679 Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId); 1680 if (!remoteTrashFolder.exists()) { 1681 return; 1682 } 1683 1684 remoteTrashFolder.open(OpenMode.READ_WRITE); 1685 if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { 1686 remoteTrashFolder.close(false); 1687 return; 1688 } 1689 1690 // 3. Find the remote original message 1691 Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId); 1692 if (remoteMessage == null) { 1693 remoteTrashFolder.close(false); 1694 return; 1695 } 1696 1697 // 4. Delete the message from the remote trash folder 1698 remoteMessage.setFlag(Flag.DELETED, true); 1699 remoteTrashFolder.expunge(); 1700 remoteTrashFolder.close(false); 1701 } 1702 1703 /** 1704 * Process a pending append message command. This command uploads a local message to the 1705 * server, first checking to be sure that the server message is not newer than 1706 * the local message. 1707 * 1708 * @param remoteStore the remote store we're working in 1709 * @param account The account in which we are working 1710 * @param newMailbox The mailbox we're appending to 1711 * @param message The message we're appending 1712 * @return true if successfully uploaded 1713 */ processPendingAppend(Store remoteStore, Account account, Mailbox newMailbox, EmailContent.Message message)1714 private boolean processPendingAppend(Store remoteStore, Account account, 1715 Mailbox newMailbox, EmailContent.Message message) 1716 throws MessagingException { 1717 1718 boolean updateInternalDate = false; 1719 boolean updateMessage = false; 1720 boolean deleteMessage = false; 1721 1722 // 1. Find the remote folder that we're appending to and create and/or open it 1723 Folder remoteFolder = remoteStore.getFolder(newMailbox.mServerId); 1724 if (!remoteFolder.exists()) { 1725 if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) { 1726 // This is POP3, we cannot actually upload. Instead, we'll update the message 1727 // locally with a fake serverId (so we don't keep trying here) and return. 1728 if (message.mServerId == null || message.mServerId.length() == 0) { 1729 message.mServerId = LOCAL_SERVERID_PREFIX + message.mId; 1730 Uri uri = 1731 ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); 1732 ContentValues cv = new ContentValues(); 1733 cv.put(EmailContent.Message.SERVER_ID, message.mServerId); 1734 mContext.getContentResolver().update(uri, cv, null, null); 1735 } 1736 return true; 1737 } 1738 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 1739 // This is a (hopefully) transient error and we return false to try again later 1740 return false; 1741 } 1742 } 1743 remoteFolder.open(OpenMode.READ_WRITE); 1744 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1745 return false; 1746 } 1747 1748 // 2. If possible, load a remote message with the matching UID 1749 Message remoteMessage = null; 1750 if (message.mServerId != null && message.mServerId.length() > 0) { 1751 remoteMessage = remoteFolder.getMessage(message.mServerId); 1752 } 1753 1754 // 3. If a remote message could not be found, upload our local message 1755 if (remoteMessage == null) { 1756 // 3a. Create a legacy message to upload 1757 Message localMessage = LegacyConversions.makeMessage(mContext, message); 1758 1759 // 3b. Upload it 1760 FetchProfile fp = new FetchProfile(); 1761 fp.add(FetchProfile.Item.BODY); 1762 remoteFolder.appendMessages(new Message[] { localMessage }); 1763 1764 // 3b. And record the UID from the server 1765 message.mServerId = localMessage.getUid(); 1766 updateInternalDate = true; 1767 updateMessage = true; 1768 } else { 1769 // 4. If the remote message exists we need to determine which copy to keep. 1770 FetchProfile fp = new FetchProfile(); 1771 fp.add(FetchProfile.Item.ENVELOPE); 1772 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1773 Date localDate = new Date(message.mServerTimeStamp); 1774 Date remoteDate = remoteMessage.getInternalDate(); 1775 if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { 1776 // 4a. If the remote message is newer than ours we'll just 1777 // delete ours and move on. A sync will get the server message 1778 // if we need to be able to see it. 1779 deleteMessage = true; 1780 } else { 1781 // 4b. Otherwise we'll upload our message and then delete the remote message. 1782 1783 // Create a legacy message to upload 1784 Message localMessage = LegacyConversions.makeMessage(mContext, message); 1785 1786 // 4c. Upload it 1787 fp.clear(); 1788 fp = new FetchProfile(); 1789 fp.add(FetchProfile.Item.BODY); 1790 remoteFolder.appendMessages(new Message[] { localMessage }); 1791 1792 // 4d. Record the UID and new internalDate from the server 1793 message.mServerId = localMessage.getUid(); 1794 updateInternalDate = true; 1795 updateMessage = true; 1796 1797 // 4e. And delete the old copy of the message from the server 1798 remoteMessage.setFlag(Flag.DELETED, true); 1799 } 1800 } 1801 1802 // 5. If requested, Best-effort to capture new "internaldate" from the server 1803 if (updateInternalDate && message.mServerId != null) { 1804 try { 1805 Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); 1806 if (remoteMessage2 != null) { 1807 FetchProfile fp2 = new FetchProfile(); 1808 fp2.add(FetchProfile.Item.ENVELOPE); 1809 remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); 1810 message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime(); 1811 updateMessage = true; 1812 } 1813 } catch (MessagingException me) { 1814 // skip it - we can live without this 1815 } 1816 } 1817 1818 // 6. Perform required edits to local copy of message 1819 if (deleteMessage || updateMessage) { 1820 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); 1821 ContentResolver resolver = mContext.getContentResolver(); 1822 if (deleteMessage) { 1823 resolver.delete(uri, null, null); 1824 } else if (updateMessage) { 1825 ContentValues cv = new ContentValues(); 1826 cv.put(EmailContent.Message.SERVER_ID, message.mServerId); 1827 cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp); 1828 resolver.update(uri, cv, null, null); 1829 } 1830 } 1831 1832 return true; 1833 } 1834 1835 /** 1836 * Finish loading a message that have been partially downloaded. 1837 * 1838 * @param messageId the message to load 1839 * @param listener the callback by which results will be reported 1840 */ loadMessageForView(final long messageId, MessagingListener listener)1841 public void loadMessageForView(final long messageId, MessagingListener listener) { 1842 mListeners.loadMessageForViewStarted(messageId); 1843 put("loadMessageForViewRemote", listener, new Runnable() { 1844 public void run() { 1845 try { 1846 // 1. Resample the message, in case it disappeared or synced while 1847 // this command was in queue 1848 EmailContent.Message message = 1849 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1850 if (message == null) { 1851 mListeners.loadMessageForViewFailed(messageId, "Unknown message"); 1852 return; 1853 } 1854 if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { 1855 mListeners.loadMessageForViewFinished(messageId); 1856 return; 1857 } 1858 1859 // 2. Open the remote folder. 1860 // TODO combine with common code in loadAttachment 1861 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 1862 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 1863 if (account == null || mailbox == null) { 1864 mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); 1865 return; 1866 } 1867 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 1868 1869 Store remoteStore = Store.getInstance(account, mContext); 1870 String remoteServerId = mailbox.mServerId; 1871 // If this is a search result, use the protocolSearchInfo field to get the 1872 // correct remote location 1873 if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { 1874 remoteServerId = message.mProtocolSearchInfo; 1875 } 1876 Folder remoteFolder = remoteStore.getFolder(remoteServerId); 1877 remoteFolder.open(OpenMode.READ_WRITE); 1878 1879 // 3. Set up to download the entire message 1880 Message remoteMessage = remoteFolder.getMessage(message.mServerId); 1881 FetchProfile fp = new FetchProfile(); 1882 fp.add(FetchProfile.Item.BODY); 1883 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1884 1885 // 4. Write to provider 1886 copyOneMessageToProvider(remoteMessage, account, mailbox, 1887 EmailContent.Message.FLAG_LOADED_COMPLETE); 1888 1889 // 5. Notify UI 1890 mListeners.loadMessageForViewFinished(messageId); 1891 1892 } catch (MessagingException me) { 1893 if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); 1894 mListeners.loadMessageForViewFailed(messageId, me.getMessage()); 1895 } catch (RuntimeException rte) { 1896 mListeners.loadMessageForViewFailed(messageId, rte.getMessage()); 1897 } 1898 } 1899 }); 1900 } 1901 1902 /** 1903 * Attempts to load the attachment specified by id from the given account and message. 1904 */ loadAttachment(final long accountId, final long messageId, final long mailboxId, final long attachmentId, MessagingListener listener, final boolean background)1905 public void loadAttachment(final long accountId, final long messageId, final long mailboxId, 1906 final long attachmentId, MessagingListener listener, final boolean background) { 1907 mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true); 1908 1909 put("loadAttachment", listener, new Runnable() { 1910 public void run() { 1911 try { 1912 //1. Check if the attachment is already here and return early in that case 1913 Attachment attachment = 1914 Attachment.restoreAttachmentWithId(mContext, attachmentId); 1915 if (attachment == null) { 1916 mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, 1917 new MessagingException("The attachment is null"), 1918 background); 1919 return; 1920 } 1921 if (Utility.attachmentExists(mContext, attachment)) { 1922 mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); 1923 return; 1924 } 1925 1926 // 2. Open the remote folder. 1927 // TODO all of these could be narrower projections 1928 Account account = Account.restoreAccountWithId(mContext, accountId); 1929 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1930 EmailContent.Message message = 1931 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1932 1933 if (account == null || mailbox == null || message == null) { 1934 mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, 1935 new MessagingException( 1936 "Account, mailbox, message or attachment are null"), 1937 background); 1938 return; 1939 } 1940 TrafficStats.setThreadStatsTag( 1941 TrafficFlags.getAttachmentFlags(mContext, account)); 1942 1943 Store remoteStore = Store.getInstance(account, mContext); 1944 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 1945 remoteFolder.open(OpenMode.READ_WRITE); 1946 1947 // 3. Generate a shell message in which to retrieve the attachment, 1948 // and a shell BodyPart for the attachment. Then glue them together. 1949 Message storeMessage = remoteFolder.createMessage(message.mServerId); 1950 MimeBodyPart storePart = new MimeBodyPart(); 1951 storePart.setSize((int)attachment.mSize); 1952 storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, 1953 attachment.mLocation); 1954 storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, 1955 String.format("%s;\n name=\"%s\"", 1956 attachment.mMimeType, 1957 attachment.mFileName)); 1958 // TODO is this always true for attachments? I think we dropped the 1959 // true encoding along the way 1960 storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 1961 1962 MimeMultipart multipart = new MimeMultipart(); 1963 multipart.setSubType("mixed"); 1964 multipart.addBodyPart(storePart); 1965 1966 storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); 1967 storeMessage.setBody(multipart); 1968 1969 // 4. Now ask for the attachment to be fetched 1970 FetchProfile fp = new FetchProfile(); 1971 fp.add(storePart); 1972 remoteFolder.fetch(new Message[] { storeMessage }, fp, 1973 mController.new MessageRetrievalListenerBridge( 1974 messageId, attachmentId)); 1975 1976 // If we failed to load the attachment, throw an Exception here, so that 1977 // AttachmentDownloadService knows that we failed 1978 if (storePart.getBody() == null) { 1979 throw new MessagingException("Attachment not loaded."); 1980 } 1981 1982 // 5. Save the downloaded file and update the attachment as necessary 1983 LegacyConversions.saveAttachmentBody(mContext, storePart, attachment, 1984 accountId); 1985 1986 // 6. Report success 1987 mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); 1988 } 1989 catch (MessagingException me) { 1990 if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); 1991 mListeners.loadAttachmentFailed( 1992 accountId, messageId, attachmentId, me, background); 1993 } catch (IOException ioe) { 1994 Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); 1995 } 1996 }}); 1997 } 1998 1999 /** 2000 * Attempt to send any messages that are sitting in the Outbox. 2001 * @param account 2002 * @param listener 2003 */ sendPendingMessages(final Account account, final long sentFolderId, MessagingListener listener)2004 public void sendPendingMessages(final Account account, final long sentFolderId, 2005 MessagingListener listener) { 2006 put("sendPendingMessages", listener, new Runnable() { 2007 public void run() { 2008 sendPendingMessagesSynchronous(account, sentFolderId); 2009 } 2010 }); 2011 } 2012 2013 /** 2014 * Attempt to send all messages sitting in the given account's outbox. Optionally, 2015 * if the server requires it, the message will be moved to the given sent folder. 2016 */ sendPendingMessagesSynchronous(final Account account, long sentFolderId)2017 public void sendPendingMessagesSynchronous(final Account account, 2018 long sentFolderId) { 2019 TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account)); 2020 NotificationController nc = NotificationController.getInstance(mContext); 2021 // 1. Loop through all messages in the account's outbox 2022 long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); 2023 if (outboxId == Mailbox.NO_MAILBOX) { 2024 return; 2025 } 2026 ContentResolver resolver = mContext.getContentResolver(); 2027 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 2028 EmailContent.Message.ID_COLUMN_PROJECTION, 2029 EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, 2030 null); 2031 try { 2032 // 2. exit early 2033 if (c.getCount() <= 0) { 2034 return; 2035 } 2036 // 3. do one-time setup of the Sender & other stuff 2037 mListeners.sendPendingMessagesStarted(account.mId, -1); 2038 2039 Sender sender = Sender.getInstance(mContext, account); 2040 Store remoteStore = Store.getInstance(account, mContext); 2041 boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 2042 ContentValues moveToSentValues = null; 2043 if (requireMoveMessageToSentFolder) { 2044 moveToSentValues = new ContentValues(); 2045 moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId); 2046 } 2047 2048 // 4. loop through the available messages and send them 2049 while (c.moveToNext()) { 2050 long messageId = -1; 2051 try { 2052 messageId = c.getLong(0); 2053 mListeners.sendPendingMessagesStarted(account.mId, messageId); 2054 // Don't send messages with unloaded attachments 2055 if (Utility.hasUnloadedAttachments(mContext, messageId)) { 2056 if (Email.DEBUG) { 2057 Log.d(Logging.LOG_TAG, "Can't send #" + messageId + 2058 "; unloaded attachments"); 2059 } 2060 continue; 2061 } 2062 sender.sendMessage(messageId); 2063 } catch (MessagingException me) { 2064 // report error for this message, but keep trying others 2065 if (me instanceof AuthenticationFailedException) { 2066 nc.showLoginFailedNotification(account.mId); 2067 } 2068 mListeners.sendPendingMessagesFailed(account.mId, messageId, me); 2069 continue; 2070 } 2071 // 5. move to sent, or delete 2072 Uri syncedUri = 2073 ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 2074 if (requireMoveMessageToSentFolder) { 2075 // If this is a forwarded message and it has attachments, delete them, as they 2076 // duplicate information found elsewhere (on the server). This saves storage. 2077 EmailContent.Message msg = 2078 EmailContent.Message.restoreMessageWithId(mContext, messageId); 2079 if (msg != null && 2080 ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { 2081 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 2082 messageId); 2083 } 2084 resolver.update(syncedUri, moveToSentValues, null, null); 2085 } else { 2086 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 2087 messageId); 2088 Uri uri = 2089 ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 2090 resolver.delete(uri, null, null); 2091 resolver.delete(syncedUri, null, null); 2092 } 2093 } 2094 // 6. report completion/success 2095 mListeners.sendPendingMessagesCompleted(account.mId); 2096 nc.cancelLoginFailedNotification(account.mId); 2097 } catch (MessagingException me) { 2098 if (me instanceof AuthenticationFailedException) { 2099 nc.showLoginFailedNotification(account.mId); 2100 } 2101 mListeners.sendPendingMessagesFailed(account.mId, -1, me); 2102 } finally { 2103 c.close(); 2104 } 2105 } 2106 2107 /** 2108 * Checks mail for an account. 2109 * This entry point is for use by the mail checking service only, because it 2110 * gives slightly different callbacks (so the service doesn't get confused by callbacks 2111 * triggered by/for the foreground UI. 2112 * 2113 * TODO clean up the execution model which is unnecessarily threaded due to legacy code 2114 * 2115 * @param accountId the account to check 2116 * @param listener 2117 */ checkMail(final long accountId, final long tag, final MessagingListener listener)2118 public void checkMail(final long accountId, final long tag, final MessagingListener listener) { 2119 mListeners.checkMailStarted(mContext, accountId, tag); 2120 2121 // This puts the command on the queue (not synchronous) 2122 listFolders(accountId, null); 2123 2124 // Put this on the queue as well so it follows listFolders 2125 put("checkMail", listener, new Runnable() { 2126 public void run() { 2127 // send any pending outbound messages. note, there is a slight race condition 2128 // here if we somehow don't have a sent folder, but this should never happen 2129 // because the call to sendMessage() would have built one previously. 2130 long inboxId = -1; 2131 Account account = Account.restoreAccountWithId(mContext, accountId); 2132 if (account != null) { 2133 long sentboxId = Mailbox.findMailboxOfType(mContext, accountId, 2134 Mailbox.TYPE_SENT); 2135 if (sentboxId != Mailbox.NO_MAILBOX) { 2136 sendPendingMessagesSynchronous(account, sentboxId); 2137 } 2138 // find mailbox # for inbox and sync it. 2139 // TODO we already know this in Controller, can we pass it in? 2140 inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); 2141 if (inboxId != Mailbox.NO_MAILBOX) { 2142 Mailbox mailbox = 2143 Mailbox.restoreMailboxWithId(mContext, inboxId); 2144 if (mailbox != null) { 2145 synchronizeMailboxSynchronous(account, mailbox); 2146 } 2147 } 2148 } 2149 mListeners.checkMailFinished(mContext, accountId, inboxId, tag); 2150 } 2151 }); 2152 } 2153 2154 private static class Command { 2155 public Runnable runnable; 2156 2157 public MessagingListener listener; 2158 2159 public String description; 2160 2161 @Override toString()2162 public String toString() { 2163 return description; 2164 } 2165 } 2166 2167 /** Results of the latest synchronization. */ 2168 private static class SyncResults { 2169 /** The total # of messages in the folder */ 2170 public final int mTotalMessages; 2171 /** A list of new message IDs; must not be {@code null} */ 2172 public final ArrayList<Long> mAddedMessages; 2173 SyncResults(int totalMessages, ArrayList<Long> addedMessages)2174 public SyncResults(int totalMessages, ArrayList<Long> addedMessages) { 2175 if (addedMessages == null) { 2176 throw new IllegalArgumentException("addedMessages must not be null"); 2177 } 2178 mTotalMessages = totalMessages; 2179 mAddedMessages = addedMessages; 2180 } 2181 } 2182 } 2183