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