MessagingController.java revision d2b0efa2f591f883eaad3d9f8996bbacdcec5c34
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 17package com.android.email; 18 19import com.android.email.mail.FetchProfile; 20import com.android.email.mail.Flag; 21import com.android.email.mail.Folder; 22import com.android.email.mail.Message; 23import com.android.email.mail.MessageRetrievalListener; 24import com.android.email.mail.MessagingException; 25import com.android.email.mail.Part; 26import com.android.email.mail.Sender; 27import com.android.email.mail.Store; 28import com.android.email.mail.StoreSynchronizer; 29import com.android.email.mail.Folder.FolderType; 30import com.android.email.mail.Folder.OpenMode; 31import com.android.email.mail.internet.MimeUtility; 32import com.android.email.mail.store.LocalStore; 33import com.android.email.mail.store.LocalStore.LocalFolder; 34import com.android.email.mail.store.LocalStore.LocalMessage; 35import com.android.email.mail.store.LocalStore.PendingCommand; 36import com.android.email.provider.EmailStore; 37 38import android.content.Context; 39import android.os.Process; 40import android.util.Config; 41import android.util.Log; 42 43import java.util.ArrayList; 44import java.util.Collections; 45import java.util.Date; 46import java.util.HashMap; 47import java.util.HashSet; 48import java.util.concurrent.BlockingQueue; 49import java.util.concurrent.LinkedBlockingQueue; 50 51/** 52 * Starts a long running (application) Thread that will run through commands 53 * that require remote mailbox access. This class is used to serialize and 54 * prioritize these commands. Each method that will submit a command requires a 55 * MessagingListener instance to be provided. It is expected that that listener 56 * has also been added as a registered listener using addListener(). When a 57 * command is to be executed, if the listener that was provided with the command 58 * is no longer registered the command is skipped. The design idea for the above 59 * is that when an Activity starts it registers as a listener. When it is paused 60 * it removes itself. Thus, any commands that that activity submitted are 61 * removed from the queue once the activity is no longer active. 62 */ 63public class MessagingController implements Runnable { 64 /** 65 * The maximum message size that we'll consider to be "small". A small message is downloaded 66 * in full immediately instead of in pieces. Anything over this size will be downloaded in 67 * pieces with attachments being left off completely and downloaded on demand. 68 * 69 * 70 * 25k for a "small" message was picked by educated trial and error. 71 * http://answers.google.com/answers/threadview?id=312463 claims that the 72 * average size of an email is 59k, which I feel is too large for our 73 * blind download. The following tests were performed on a download of 74 * 25 random messages. 75 * <pre> 76 * 5k - 61 seconds, 77 * 25k - 51 seconds, 78 * 55k - 53 seconds, 79 * </pre> 80 * So 25k gives good performance and a reasonable data footprint. Sounds good to me. 81 */ 82 private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); 83 84 private static final String PENDING_COMMAND_TRASH = 85 "com.android.email.MessagingController.trash"; 86 private static final String PENDING_COMMAND_MARK_READ = 87 "com.android.email.MessagingController.markRead"; 88 private static final String PENDING_COMMAND_APPEND = 89 "com.android.email.MessagingController.append"; 90 91 private static MessagingController inst = null; 92 private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>(); 93 private Thread mThread; 94 95 /** 96 * All access to mListeners *must* be synchronized 97 */ 98 private HashSet<MessagingListener> mListeners = new HashSet<MessagingListener>(); 99 private boolean mBusy; 100 private Context mContext; 101 102 protected MessagingController(Context _context) { 103 mContext = _context; 104 mThread = new Thread(this); 105 mThread.start(); 106 } 107 108 /** 109 * Gets or creates the singleton instance of MessagingController. Application is used to 110 * provide a Context to classes that need it. 111 * @param application 112 * @return 113 */ 114 public synchronized static MessagingController getInstance(Context _context) { 115 if (inst == null) { 116 inst = new MessagingController(_context); 117 } 118 return inst; 119 } 120 121 /** 122 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 123 */ 124 public static void injectMockController(MessagingController mockController) { 125 inst = mockController; 126 } 127 128 public boolean isBusy() { 129 return mBusy; 130 } 131 132 public void run() { 133 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 134 while (true) { 135 try { 136 Command command = mCommands.take(); 137 if (command.listener == null || isActiveListener(command.listener)) { 138 mBusy = true; 139 command.runnable.run(); 140 synchronized (mListeners) { 141 for (MessagingListener l : mListeners) { 142 l.controllerCommandCompleted(mCommands.size() > 0); 143 } 144 } 145 } 146 } 147 catch (Exception e) { 148 if (Config.LOGD) { 149 Log.d(Email.LOG_TAG, "Error running command", e); 150 } 151 } 152 mBusy = false; 153 } 154 } 155 156 private void put(String description, MessagingListener listener, Runnable runnable) { 157 try { 158 Command command = new Command(); 159 command.listener = listener; 160 command.runnable = runnable; 161 command.description = description; 162 mCommands.add(command); 163 } 164 catch (IllegalStateException ie) { 165 throw new Error(ie); 166 } 167 } 168 169 public void addListener(MessagingListener listener) { 170 synchronized (mListeners) { 171 mListeners.add(listener); 172 } 173 } 174 175 public void removeListener(MessagingListener listener) { 176 synchronized (mListeners) { 177 mListeners.remove(listener); 178 } 179 } 180 181 private boolean isActiveListener(MessagingListener listener) { 182 synchronized (mListeners) { 183 return mListeners.contains(listener); 184 } 185 } 186 187 /** 188 * Lists folders that are available locally and remotely. This method calls 189 * listFoldersCallback for local folders before it returns, and then for 190 * remote folders at some later point. If there are no local folders 191 * includeRemote is forced by this method. This method should be called from 192 * a Thread as it may take several seconds to list the local folders. 193 * 194 * TODO this needs to cache the remote folder list 195 * TODO break out an inner listFoldersSynchronized which could simplify checkMail 196 * 197 * @param account 198 * @param includeRemote 199 * @param listener 200 * @throws MessagingException 201 */ 202 public void listFolders(final EmailStore.Account account, boolean refreshRemote, 203 MessagingListener listener) { 204 synchronized (mListeners) { 205 for (MessagingListener l : mListeners) { 206 l.listFoldersStarted(account); 207 } 208 } 209 try { 210 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 211 null); 212 Folder[] localFolders = localStore.getPersonalNamespaces(); 213 214 if (localFolders == null || localFolders.length == 0) { 215 refreshRemote = true; 216 } else { 217 synchronized (mListeners) { 218 for (MessagingListener l : mListeners) { 219 l.listFolders(account, localFolders); 220 } 221 } 222 } 223 } 224 catch (Exception e) { 225 synchronized (mListeners) { 226 for (MessagingListener l : mListeners) { 227 l.listFoldersFailed(account, e.getMessage()); 228 return; 229 } 230 } 231 } 232 if (refreshRemote) { 233 put("listFolders", listener, new Runnable() { 234 public void run() { 235 try { 236 LocalStore localStore = (LocalStore) Store.getInstance( 237 account.getLocalStoreUri(mContext), mContext, null); 238 Store store = Store.getInstance(account.getStoreUri(mContext), mContext, 239 localStore.getPersistentCallbacks()); 240 241 Folder[] remoteFolders = store.getPersonalNamespaces(); 242 updateAccountFolderNames(account, remoteFolders); 243 244 HashSet<String> remoteFolderNames = new HashSet<String>(); 245 for (int i = 0, count = remoteFolders.length; i < count; i++) { 246 Folder localFolder = localStore.getFolder(remoteFolders[i].getName()); 247 if (!localFolder.exists()) { 248 localFolder.create(FolderType.HOLDS_MESSAGES); 249 } 250 remoteFolderNames.add(remoteFolders[i].getName()); 251 } 252 253 Folder[] localFolders = localStore.getPersonalNamespaces(); 254 255 /* 256 * Clear out any folders that are no longer on the remote store. 257 */ 258 for (Folder localFolder : localFolders) { 259 String localFolderName = localFolder.getName(); 260 if (localFolderName.equalsIgnoreCase(Email.INBOX) || 261 localFolderName.equals(account.getTrashFolderName(mContext)) || 262 localFolderName.equals(account.getOutboxFolderName(mContext)) || 263 localFolderName.equals(account.getDraftsFolderName(mContext)) || 264 localFolderName.equals(account.getSentFolderName(mContext))) { 265 continue; 266 } 267 if (!remoteFolderNames.contains(localFolder.getName())) { 268 localFolder.delete(false); 269 } 270 } 271 272 // Signal the remote store so it can used folder-based callbacks 273 for (Folder remoteFolder : remoteFolders) { 274 Folder localFolder = localStore.getFolder(remoteFolder.getName()); 275 remoteFolder.localFolderSetupComplete(localFolder); 276 } 277 278 localFolders = localStore.getPersonalNamespaces(); 279 280 synchronized (mListeners) { 281 for (MessagingListener l : mListeners) { 282 l.listFolders(account, localFolders); 283 } 284 for (MessagingListener l : mListeners) { 285 l.listFoldersFinished(account); 286 } 287 } 288 } 289 catch (Exception e) { 290 synchronized (mListeners) { 291 for (MessagingListener l : mListeners) { 292 l.listFoldersFailed(account, ""); 293 } 294 } 295 } 296 } 297 }); 298 } else { 299 synchronized (mListeners) { 300 for (MessagingListener l : mListeners) { 301 l.listFoldersFinished(account); 302 } 303 } 304 } 305 } 306 307 /** 308 * Asks the store for a list of server-specific folder names and, if provided, updates 309 * the account record for future getFolder() operations. 310 * 311 * NOTE: Inbox is not queried, because we require it to be INBOX, and outbox is not 312 * queried, because outbox is local-only. 313 * 314 * TODO: Rewrite this to use simple folder tagging and none of this account nonsense 315 */ 316 /* package */ void updateAccountFolderNames(EmailStore.Account account, 317 Folder[] remoteFolders) { 318 String trash = null; 319 String sent = null; 320 String drafts = null; 321 322 for (Folder folder : remoteFolders) { 323 Folder.FolderRole role = folder.getRole(); 324 if (role == Folder.FolderRole.TRASH) { 325 trash = folder.getName(); 326 } else if (role == Folder.FolderRole.SENT) { 327 sent = folder.getName(); 328 } else if (role == Folder.FolderRole.DRAFTS) { 329 drafts = folder.getName(); 330 } 331 } 332/* 333 // Do not update when null (defaults are already in place) 334 boolean commit = false; 335 if (trash != null && !trash.equals(account.getTrashFolderName(mContext))) { 336 account.setTrashFolderName(trash); 337 commit = true; 338 } 339 if (sent != null && !sent.equals(account.getSentFolderName(mContext))) { 340 account.setSentFolderName(sent); 341 commit = true; 342 } 343 if (drafts != null && !drafts.equals(account.getDraftsFolderName(mContext))) { 344 account.setDraftsFolderName(drafts); 345 commit = true; 346 } 347 if (commit) { 348 account.saveOrUpdate(mContext); 349 } 350*/ 351 } 352 353 /** 354 * List the local message store for the given folder. This work is done 355 * synchronously. 356 * 357 * @param account 358 * @param folder 359 * @param listener 360 * @throws MessagingException 361 */ 362 public void listLocalMessages(final EmailStore.Account account, final String folder, 363 MessagingListener listener) { 364 synchronized (mListeners) { 365 for (MessagingListener l : mListeners) { 366 l.listLocalMessagesStarted(account, folder); 367 } 368 } 369 370 try { 371 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 372 null); 373 Folder localFolder = localStore.getFolder(folder); 374 localFolder.open(OpenMode.READ_WRITE, null); 375 Message[] localMessages = localFolder.getMessages(null); 376 ArrayList<Message> messages = new ArrayList<Message>(); 377 for (Message message : localMessages) { 378 if (!message.isSet(Flag.DELETED)) { 379 messages.add(message); 380 } 381 } 382 synchronized (mListeners) { 383 for (MessagingListener l : mListeners) { 384 l.listLocalMessages(account, folder, messages.toArray(new Message[0])); 385 } 386 for (MessagingListener l : mListeners) { 387 l.listLocalMessagesFinished(account, folder); 388 } 389 } 390 } 391 catch (Exception e) { 392 synchronized (mListeners) { 393 for (MessagingListener l : mListeners) { 394 l.listLocalMessagesFailed(account, folder, e.getMessage()); 395 } 396 } 397 } 398 } 399 400 public void loadMoreMessages(EmailStore.Account account, String folder, 401 MessagingListener listener) { 402 try { 403 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), 404 mContext); 405 LocalStore localStore = (LocalStore) Store.getInstance( 406 account.getLocalStoreUri(mContext), mContext, null); 407 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 408 int oldLimit = localFolder.getVisibleLimit(); 409 if (oldLimit <= 0) { 410 oldLimit = info.mVisibleLimitDefault; 411 } 412 localFolder.setVisibleLimit(oldLimit + info.mVisibleLimitIncrement); 413 synchronizeMailbox(account, folder, listener); 414 } 415 catch (MessagingException me) { 416 throw new RuntimeException("Unable to set visible limit on folder", me); 417 } 418 } 419 420 public void resetVisibleLimits(EmailStore.Account account) { 421 try { 422 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), 423 mContext); 424 if (info != null) { 425 LocalStore localStore = (LocalStore) Store.getInstance( 426 account.getLocalStoreUri(mContext), mContext, null); 427 localStore.resetVisibleLimits(info.mVisibleLimitDefault); 428 } 429 } 430 catch (MessagingException e) { 431 Log.e(Email.LOG_TAG, "Unable to reset visible limits", e); 432 } 433 } 434 435 /** 436 * Start background synchronization of the specified folder. 437 * @param account 438 * @param folder 439 * @param listener 440 */ 441 public void synchronizeMailbox(final EmailStore.Account account, final String folder, 442 MessagingListener listener) { 443 /* 444 * We don't ever sync the Outbox. 445 */ 446 if (folder.equals(account.getOutboxFolderName(mContext))) { 447 return; 448 } 449 synchronized (mListeners) { 450 for (MessagingListener l : mListeners) { 451 l.synchronizeMailboxStarted(account, folder); 452 } 453 } 454 put("synchronizeMailbox", listener, new Runnable() { 455 public void run() { 456 synchronizeMailboxSynchronous(account, folder); 457 } 458 }); 459 } 460 461 /** 462 * Start foreground synchronization of the specified folder. This is called by 463 * synchronizeMailbox or checkMail. 464 * @param account 465 * @param folder 466 * @param listener 467 */ 468 private void synchronizeMailboxSynchronous(final EmailStore.Account account, 469 final String folder) { 470 synchronized (mListeners) { 471 for (MessagingListener l : mListeners) { 472 l.synchronizeMailboxStarted(account, folder); 473 } 474 } 475 try { 476 processPendingCommandsSynchronous(account); 477 478 StoreSynchronizer.SyncResults results; 479 480 // Select generic sync or store-specific sync 481 final LocalStore localStore = 482 (LocalStore) Store.getInstance(account.getLocalStoreUri(mContext), mContext, null); 483 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 484 localStore.getPersistentCallbacks()); 485 StoreSynchronizer customSync = remoteStore.getMessageSynchronizer(); 486 if (customSync == null) { 487 results = synchronizeMailboxGeneric(account, folder); 488 } else { 489 results = customSync.SynchronizeMessagesSynchronous( 490 account, folder, mListeners, mContext); 491 } 492 493 synchronized (mListeners) { 494 for (MessagingListener l : mListeners) { 495 l.synchronizeMailboxFinished( 496 account, 497 folder, 498 results.mTotalMessages, results.mNewMessages); 499 } 500 } 501 502 } catch (Exception e) { 503 if (Config.LOGV) { 504 Log.v(Email.LOG_TAG, "synchronizeMailbox", e); 505 } 506 synchronized (mListeners) { 507 for (MessagingListener l : mListeners) { 508 l.synchronizeMailboxFailed( 509 account, 510 folder, 511 e); 512 } 513 } 514 } 515 } 516 517 /** 518 * Generic synchronizer - used for POP3 and IMAP. 519 * 520 * TODO Break this method up into smaller chunks. 521 * 522 * @param account 523 * @param folder 524 * @return 525 * @throws MessagingException 526 */ 527 private StoreSynchronizer.SyncResults synchronizeMailboxGeneric( 528 final EmailStore.Account account, final String folder) throws MessagingException { 529 /* 530 * Get the message list from the local store and create an index of 531 * the uids within the list. 532 */ 533 final LocalStore localStore = 534 (LocalStore) Store.getInstance(account.getLocalStoreUri(mContext), mContext, null); 535 final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 536 localFolder.open(OpenMode.READ_WRITE, null); 537 Message[] localMessages = localFolder.getMessages(null); 538 HashMap<String, Message> localUidMap = new HashMap<String, Message>(); 539 int localUnreadCount = 0; 540 for (Message message : localMessages) { 541 localUidMap.put(message.getUid(), message); 542 if (!message.isSet(Flag.SEEN)) { 543 localUnreadCount++; 544 } 545 } 546 547 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 548 localStore.getPersistentCallbacks()); 549 Folder remoteFolder = remoteStore.getFolder(folder); 550 551 /* 552 * If the folder is a "special" folder we need to see if it exists 553 * on the remote server. It if does not exist we'll try to create it. If we 554 * can't create we'll abort. This will happen on every single Pop3 folder as 555 * designed and on Imap folders during error conditions. This allows us 556 * to treat Pop3 and Imap the same in this code. 557 */ 558 if (folder.equals(account.getTrashFolderName(mContext)) || 559 folder.equals(account.getSentFolderName(mContext)) || 560 folder.equals(account.getDraftsFolderName(mContext))) { 561 if (!remoteFolder.exists()) { 562 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 563 return new StoreSynchronizer.SyncResults(0, 0); 564 } 565 } 566 } 567 568 /* 569 * Synchronization process: 570 Open the folder 571 Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) 572 Get the message count 573 Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages 574 getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount) 575 See if we have each message locally, if not fetch it's flags and envelope 576 Get and update the unread count for the folder 577 Update the remote flags of any messages we have locally with an internal date 578 newer than the remote message. 579 Get the current flags for any messages we have locally but did not just download 580 Update local flags 581 For any message we have locally but not remotely, delete the local message to keep 582 cache clean. 583 Download larger parts of any new messages. 584 (Optional) Download small attachments in the background. 585 */ 586 587 /* 588 * Open the remote folder. This pre-loads certain metadata like message count. 589 */ 590 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 591 592 /* 593 * Trash any remote messages that are marked as trashed locally. 594 */ 595 596 /* 597 * Get the remote message count. 598 */ 599 int remoteMessageCount = remoteFolder.getMessageCount(); 600 601 int visibleLimit = localFolder.getVisibleLimit(); 602 if (visibleLimit <= 0) { 603 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), 604 mContext); 605 visibleLimit = info.mVisibleLimitDefault; 606 localFolder.setVisibleLimit(visibleLimit); 607 } 608 609 Message[] remoteMessages = new Message[0]; 610 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 611 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 612 613 int newMessageCount = 0; 614 if (remoteMessageCount > 0) { 615 /* 616 * Message numbers start at 1. 617 */ 618 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 619 int remoteEnd = remoteMessageCount; 620 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 621 for (Message message : remoteMessages) { 622 remoteUidMap.put(message.getUid(), message); 623 } 624 625 /* 626 * Get a list of the messages that are in the remote list but not on the 627 * local store, or messages that are in the local store but failed to download 628 * on the last sync. These are the new messages that we will download. 629 */ 630 for (Message message : remoteMessages) { 631 Message localMessage = localUidMap.get(message.getUid()); 632 if (localMessage == null) { 633 newMessageCount++; 634 } 635 if (localMessage == null || 636 (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && 637 !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { 638 unsyncedMessages.add(message); 639 } 640 } 641 } 642 643 /* 644 * A list of messages that were downloaded and which did not have the Seen flag set. 645 * This will serve to indicate the true "new" message count that will be reported to 646 * the user via notification. 647 */ 648 final ArrayList<Message> newMessages = new ArrayList<Message>(); 649 650 /* 651 * Fetch the flags and envelope only of the new messages. This is intended to get us 652 * critical data as fast as possible, and then we'll fill in the details. 653 */ 654 if (unsyncedMessages.size() > 0) { 655 656 /* 657 * Reverse the order of the messages. Depending on the server this may get us 658 * fetch results for newest to oldest. If not, no harm done. 659 */ 660 Collections.reverse(unsyncedMessages); 661 662 FetchProfile fp = new FetchProfile(); 663 fp.add(FetchProfile.Item.FLAGS); 664 fp.add(FetchProfile.Item.ENVELOPE); 665 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 666 new MessageRetrievalListener() { 667 public void messageFinished(Message message, int number, int ofTotal) { 668 try { 669 // Store the new message locally 670 localFolder.appendMessages(new Message[] { 671 message 672 }); 673 674 // And include it in the view 675 if (message.getSubject() != null && 676 message.getFrom() != null) { 677 /* 678 * We check to make sure that we got something worth 679 * showing (subject and from) because some protocols 680 * (POP) may not be able to give us headers for 681 * ENVELOPE, only size. 682 */ 683 synchronized (mListeners) { 684 for (MessagingListener l : mListeners) { 685 l.synchronizeMailboxNewMessage(account, folder, 686 localFolder.getMessage(message.getUid())); 687 } 688 } 689 } 690 691 if (!message.isSet(Flag.SEEN)) { 692 newMessages.add(message); 693 } 694 } 695 catch (Exception e) { 696 Log.e(Email.LOG_TAG, 697 "Error while storing downloaded message.", 698 e); 699 } 700 } 701 702 public void messageStarted(String uid, int number, int ofTotal) { 703 } 704 }); 705 } 706 707 /* 708 * Refresh the flags for any messages in the local store that we didn't just 709 * download. 710 */ 711 FetchProfile fp = new FetchProfile(); 712 fp.add(FetchProfile.Item.FLAGS); 713 remoteFolder.fetch(remoteMessages, fp, null); 714 boolean remoteSupportsSeenFlag = false; 715 for (Flag flag : remoteFolder.getPermanentFlags()) { 716 if (flag == Flag.SEEN) { 717 remoteSupportsSeenFlag = true; 718 } 719 } 720 for (Message remoteMessage : remoteMessages) { 721 Message localMessage = localFolder.getMessage(remoteMessage.getUid()); 722 if (localMessage == null) { 723 continue; 724 } 725 if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN) 726 && remoteSupportsSeenFlag) { 727 localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); 728 synchronized (mListeners) { 729 for (MessagingListener l : mListeners) { 730 l.synchronizeMailboxNewMessage(account, folder, localMessage); 731 } 732 } 733 } 734 } 735 736 /* 737 * Get and store the unread message count. 738 */ 739 int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); 740 if (remoteUnreadMessageCount == -1) { 741 if (remoteSupportsSeenFlag) { 742 /* 743 * If remote folder doesn't supported unread message count but supports 744 * seen flag, use local folder's unread message count and the size of 745 * new messages. 746 * This mode is actually not used but for non-POP3, non-IMAP. 747 */ 748 localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() 749 + newMessages.size()); 750 } else { 751 /* 752 * If remote folder doesn't supported unread message count and doesn't 753 * support seen flag, use localUnreadCount and newMessageCount which 754 * don't rely on remote SEEN flag. 755 * This mode is used by POP3. 756 */ 757 localFolder.setUnreadMessageCount(localUnreadCount + newMessageCount); 758 } 759 } else { 760 /* 761 * If remote folder supports unread message count, 762 * use remoteUnreadMessageCount. 763 * This mode is used by IMAP. 764 */ 765 localFolder.setUnreadMessageCount(remoteUnreadMessageCount); 766 } 767 768 /* 769 * Remove any messages that are in the local store but no longer on the remote store. 770 */ 771 for (Message localMessage : localMessages) { 772 if (remoteUidMap.get(localMessage.getUid()) == null) { 773 localMessage.setFlag(Flag.X_DESTROYED, true); 774 synchronized (mListeners) { 775 for (MessagingListener l : mListeners) { 776 l.synchronizeMailboxRemovedMessage(account, folder, localMessage); 777 } 778 } 779 } 780 } 781 782 /* 783 * Now we download the actual content of messages. 784 */ 785 ArrayList<Message> largeMessages = new ArrayList<Message>(); 786 ArrayList<Message> smallMessages = new ArrayList<Message>(); 787 for (Message message : unsyncedMessages) { 788 /* 789 * Sort the messages into two buckets, small and large. Small messages will be 790 * downloaded fully and large messages will be downloaded in parts. By sorting 791 * into two buckets we can pipeline the commands for each set of messages 792 * into a single command to the server saving lots of round trips. 793 */ 794 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 795 largeMessages.add(message); 796 } else { 797 smallMessages.add(message); 798 } 799 } 800 /* 801 * Grab the content of the small messages first. This is going to 802 * be very fast and at very worst will be a single up of a few bytes and a single 803 * download of 625k. 804 */ 805 fp = new FetchProfile(); 806 fp.add(FetchProfile.Item.BODY); 807 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), 808 fp, new MessageRetrievalListener() { 809 public void messageFinished(Message message, int number, int ofTotal) { 810 try { 811 // Store the updated message locally 812 localFolder.appendMessages(new Message[] { 813 message 814 }); 815 816 Message localMessage = localFolder.getMessage(message.getUid()); 817 818 // Set a flag indicating this message has now be fully downloaded 819 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 820 821 // Update the listener with what we've found 822 synchronized (mListeners) { 823 for (MessagingListener l : mListeners) { 824 l.synchronizeMailboxNewMessage( 825 account, 826 folder, 827 localMessage); 828 } 829 } 830 } 831 catch (MessagingException me) { 832 833 } 834 } 835 836 public void messageStarted(String uid, int number, int ofTotal) { 837 } 838 }); 839 840 /* 841 * Now do the large messages that require more round trips. 842 */ 843 fp.clear(); 844 fp.add(FetchProfile.Item.STRUCTURE); 845 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), 846 fp, null); 847 for (Message message : largeMessages) { 848 if (message.getBody() == null) { 849 /* 850 * The provider was unable to get the structure of the message, so 851 * we'll download a reasonable portion of the messge and mark it as 852 * incomplete so the entire thing can be downloaded later if the user 853 * wishes to download it. 854 */ 855 fp.clear(); 856 fp.add(FetchProfile.Item.BODY_SANE); 857 /* 858 * TODO a good optimization here would be to make sure that all Stores set 859 * the proper size after this fetch and compare the before and after size. If 860 * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 861 */ 862 863 remoteFolder.fetch(new Message[] { message }, fp, null); 864 // Store the updated message locally 865 localFolder.appendMessages(new Message[] { 866 message 867 }); 868 869 Message localMessage = localFolder.getMessage(message.getUid()); 870 871 // Set a flag indicating that the message has been partially downloaded and 872 // is ready for view. 873 localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); 874 } else { 875 /* 876 * We have a structure to deal with, from which 877 * we can pull down the parts we want to actually store. 878 * Build a list of parts we are interested in. Text parts will be downloaded 879 * right now, attachments will be left for later. 880 */ 881 882 ArrayList<Part> viewables = new ArrayList<Part>(); 883 ArrayList<Part> attachments = new ArrayList<Part>(); 884 MimeUtility.collectParts(message, viewables, attachments); 885 886 /* 887 * Now download the parts we're interested in storing. 888 */ 889 for (Part part : viewables) { 890 fp.clear(); 891 fp.add(part); 892 // TODO what happens if the network connection dies? We've got partial 893 // messages with incorrect status stored. 894 remoteFolder.fetch(new Message[] { message }, fp, null); 895 } 896 // Store the updated message locally 897 localFolder.appendMessages(new Message[] { 898 message 899 }); 900 901 Message localMessage = localFolder.getMessage(message.getUid()); 902 903 // Set a flag indicating this message has been fully downloaded and can be 904 // viewed. 905 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 906 } 907 908 // Update the listener with what we've found 909 synchronized (mListeners) { 910 for (MessagingListener l : mListeners) { 911 l.synchronizeMailboxNewMessage( 912 account, 913 folder, 914 localFolder.getMessage(message.getUid())); 915 } 916 } 917 } 918 919 920 /* 921 * Report successful sync 922 */ 923 StoreSynchronizer.SyncResults results = new StoreSynchronizer.SyncResults( 924 remoteFolder.getMessageCount(), newMessages.size()); 925 926 remoteFolder.close(false); 927 localFolder.close(false); 928 929 return results; 930 } 931 932 private void queuePendingCommand(EmailStore.Account account, PendingCommand command) { 933 try { 934 LocalStore localStore = (LocalStore) Store.getInstance( 935 account.getLocalStoreUri(mContext), mContext, null); 936 localStore.addPendingCommand(command); 937 } 938 catch (Exception e) { 939 throw new RuntimeException("Unable to enqueue pending command", e); 940 } 941 } 942 943 private void processPendingCommands(final EmailStore.Account account) { 944 put("processPendingCommands", null, new Runnable() { 945 public void run() { 946 try { 947 processPendingCommandsSynchronous(account); 948 } 949 catch (MessagingException me) { 950 if (Config.LOGV) { 951 Log.v(Email.LOG_TAG, "processPendingCommands", me); 952 } 953 /* 954 * Ignore any exceptions from the commands. Commands will be processed 955 * on the next round. 956 */ 957 } 958 } 959 }); 960 } 961 962 private void processPendingCommandsSynchronous(EmailStore.Account account) 963 throws MessagingException { 964 LocalStore localStore = (LocalStore) Store.getInstance( 965 account.getLocalStoreUri(mContext), mContext, null); 966 ArrayList<PendingCommand> commands = localStore.getPendingCommands(); 967 for (PendingCommand command : commands) { 968 /* 969 * We specifically do not catch any exceptions here. If a command fails it is 970 * most likely due to a server or IO error and it must be retried before any 971 * other command processes. This maintains the order of the commands. 972 */ 973 if (PENDING_COMMAND_APPEND.equals(command.command)) { 974 processPendingAppend(command, account); 975 } 976 else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { 977 processPendingMarkRead(command, account); 978 } 979 else if (PENDING_COMMAND_TRASH.equals(command.command)) { 980 processPendingTrash(command, account); 981 } 982 localStore.removePendingCommand(command); 983 } 984 } 985 986 /** 987 * Process a pending append message command. This command uploads a local message to the 988 * server, first checking to be sure that the server message is not newer than 989 * the local message. Once the local message is successfully processed it is deleted so 990 * that the server message will be synchronized down without an additional copy being 991 * created. 992 * TODO update the local message UID instead of deleteing it 993 * 994 * @param command arguments = (String folder, String uid) 995 * @param account 996 * @throws MessagingException 997 */ 998 private void processPendingAppend(PendingCommand command, EmailStore.Account account) 999 throws MessagingException { 1000 String folder = command.arguments[0]; 1001 String uid = command.arguments[1]; 1002 1003 LocalStore localStore = (LocalStore) Store.getInstance( 1004 account.getLocalStoreUri(mContext), mContext, null); 1005 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1006 LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); 1007 1008 if (localMessage == null) { 1009 return; 1010 } 1011 1012 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 1013 localStore.getPersistentCallbacks()); 1014 Folder remoteFolder = remoteStore.getFolder(folder); 1015 if (!remoteFolder.exists()) { 1016 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 1017 return; 1018 } 1019 } 1020 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1021 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1022 return; 1023 } 1024 1025 Message remoteMessage = null; 1026 if (!localMessage.getUid().startsWith("Local") 1027 && !localMessage.getUid().contains("-")) { 1028 remoteMessage = remoteFolder.getMessage(localMessage.getUid()); 1029 } 1030 1031 if (remoteMessage == null) { 1032 /* 1033 * If the message does not exist remotely we just upload it and then 1034 * update our local copy with the new uid. 1035 */ 1036 FetchProfile fp = new FetchProfile(); 1037 fp.add(FetchProfile.Item.BODY); 1038 localFolder.fetch(new Message[] { localMessage }, fp, null); 1039 String oldUid = localMessage.getUid(); 1040 remoteFolder.appendMessages(new Message[] { localMessage }); 1041 localFolder.changeUid(localMessage); 1042 synchronized (mListeners) { 1043 for (MessagingListener l : mListeners) { 1044 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1045 } 1046 } 1047 } 1048 else { 1049 /* 1050 * If the remote message exists we need to determine which copy to keep. 1051 */ 1052 /* 1053 * See if the remote message is newer than ours. 1054 */ 1055 FetchProfile fp = new FetchProfile(); 1056 fp.add(FetchProfile.Item.ENVELOPE); 1057 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1058 Date localDate = localMessage.getInternalDate(); 1059 Date remoteDate = remoteMessage.getInternalDate(); 1060 if (remoteDate.compareTo(localDate) > 0) { 1061 /* 1062 * If the remote message is newer than ours we'll just 1063 * delete ours and move on. A sync will get the server message 1064 * if we need to be able to see it. 1065 */ 1066 localMessage.setFlag(Flag.DELETED, true); 1067 } 1068 else { 1069 /* 1070 * Otherwise we'll upload our message and then delete the remote message. 1071 */ 1072 fp.clear(); 1073 fp = new FetchProfile(); 1074 fp.add(FetchProfile.Item.BODY); 1075 localFolder.fetch(new Message[] { localMessage }, fp, null); 1076 String oldUid = localMessage.getUid(); 1077 remoteFolder.appendMessages(new Message[] { localMessage }); 1078 localFolder.changeUid(localMessage); 1079 synchronized (mListeners) { 1080 for (MessagingListener l : mListeners) { 1081 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1082 } 1083 } 1084 remoteMessage.setFlag(Flag.DELETED, true); 1085 } 1086 } 1087 } 1088 1089 /** 1090 * Process a pending trash message command. 1091 * 1092 * @param command arguments = (String folder, String uid) 1093 * @param account 1094 * @throws MessagingException 1095 */ 1096 private void processPendingTrash(PendingCommand command, final EmailStore.Account account) 1097 throws MessagingException { 1098 String folder = command.arguments[0]; 1099 String uid = command.arguments[1]; 1100 1101 final LocalStore localStore = (LocalStore) Store.getInstance( 1102 account.getLocalStoreUri(mContext), mContext, null); 1103 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1104 1105 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 1106 localStore.getPersistentCallbacks()); 1107 Folder remoteFolder = remoteStore.getFolder(folder); 1108 if (!remoteFolder.exists()) { 1109 return; 1110 } 1111 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1112 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1113 return; 1114 } 1115 1116 Message remoteMessage = null; 1117 if (!uid.startsWith("Local") 1118 && !uid.contains("-")) { 1119 remoteMessage = remoteFolder.getMessage(uid); 1120 } 1121 if (remoteMessage == null) { 1122 return; 1123 } 1124 1125 Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName(mContext)); 1126 /* 1127 * Attempt to copy the remote message to the remote trash folder. 1128 */ 1129 if (!remoteTrashFolder.exists()) { 1130 /* 1131 * If the remote trash folder doesn't exist we try to create it. 1132 */ 1133 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 1134 } 1135 1136 if (remoteTrashFolder.exists()) { 1137 /* 1138 * Because remoteTrashFolder may be new, we need to explicitly open it 1139 * and pass in the persistence callbacks. 1140 */ 1141 final LocalFolder localTrashFolder = 1142 (LocalFolder) localStore.getFolder(account.getTrashFolderName(mContext)); 1143 remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks()); 1144 if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { 1145 return; 1146 } 1147 1148 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, 1149 new Folder.MessageUpdateCallbacks() { 1150 public void onMessageUidChange(Message message, String newUid) 1151 throws MessagingException { 1152 // update the UID in the local trash folder, because some stores will 1153 // have to change it when copying to remoteTrashFolder 1154 LocalMessage localMessage = 1155 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1156 if(localMessage != null) { 1157 localMessage.setUid(newUid); 1158 localTrashFolder.updateMessage(localMessage); 1159 } 1160 } 1161 1162 /** 1163 * This will be called if the deleted message doesn't exist and can't be 1164 * deleted (e.g. it was already deleted from the server.) In this case, 1165 * attempt to delete the local copy as well. 1166 */ 1167 public void onMessageNotFound(Message message) throws MessagingException { 1168 LocalMessage localMessage = 1169 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1170 if (localMessage != null) { 1171 localMessage.setFlag(Flag.DELETED, true); 1172 } 1173 } 1174 1175 } 1176 ); 1177 } 1178 1179 remoteMessage.setFlag(Flag.DELETED, true); 1180 remoteFolder.expunge(); 1181 } 1182 1183 /** 1184 * Processes a pending mark read or unread command. 1185 * 1186 * @param command arguments = (String folder, String uid, boolean read) 1187 * @param account 1188 */ 1189 private void processPendingMarkRead(PendingCommand command, EmailStore.Account account) 1190 throws MessagingException { 1191 String folder = command.arguments[0]; 1192 String uid = command.arguments[1]; 1193 boolean read = Boolean.parseBoolean(command.arguments[2]); 1194 1195 LocalStore localStore = (LocalStore) Store.getInstance( 1196 account.getLocalStoreUri(mContext), mContext, null); 1197 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1198 1199 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 1200 localStore.getPersistentCallbacks()); 1201 Folder remoteFolder = remoteStore.getFolder(folder); 1202 if (!remoteFolder.exists()) { 1203 return; 1204 } 1205 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1206 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1207 return; 1208 } 1209 Message remoteMessage = null; 1210 if (!uid.startsWith("Local") 1211 && !uid.contains("-")) { 1212 remoteMessage = remoteFolder.getMessage(uid); 1213 } 1214 if (remoteMessage == null) { 1215 return; 1216 } 1217 remoteMessage.setFlag(Flag.SEEN, read); 1218 } 1219 1220 /** 1221 * Mark the message with the given account, folder and uid either Seen or not Seen. 1222 * @param account 1223 * @param folder 1224 * @param uid 1225 * @param seen 1226 */ 1227 public void markMessageRead( 1228 final EmailStore.Account account, 1229 final String folder, 1230 final String uid, 1231 final boolean seen) { 1232 try { 1233 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 1234 null); 1235 Folder localFolder = localStore.getFolder(folder); 1236 localFolder.open(OpenMode.READ_WRITE, null); 1237 1238 Message message = localFolder.getMessage(uid); 1239 message.setFlag(Flag.SEEN, seen); 1240 PendingCommand command = new PendingCommand(); 1241 command.command = PENDING_COMMAND_MARK_READ; 1242 command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; 1243 queuePendingCommand(account, command); 1244 processPendingCommands(account); 1245 } 1246 catch (MessagingException me) { 1247 throw new RuntimeException(me); 1248 } 1249 } 1250 1251 private void loadMessageForViewRemote(final EmailStore.Account account, final String folder, 1252 final String uid, MessagingListener listener) { 1253 put("loadMessageForViewRemote", listener, new Runnable() { 1254 public void run() { 1255 try { 1256 LocalStore localStore = (LocalStore) Store.getInstance( 1257 account.getLocalStoreUri(mContext), mContext, null); 1258 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1259 localFolder.open(OpenMode.READ_WRITE, null); 1260 1261 Message message = localFolder.getMessage(uid); 1262 1263 if (message.isSet(Flag.X_DOWNLOADED_FULL)) { 1264 /* 1265 * If the message has been synchronized since we were called we'll 1266 * just hand it back cause it's ready to go. 1267 */ 1268 FetchProfile fp = new FetchProfile(); 1269 fp.add(FetchProfile.Item.ENVELOPE); 1270 fp.add(FetchProfile.Item.BODY); 1271 localFolder.fetch(new Message[] { message }, fp, null); 1272 1273 synchronized (mListeners) { 1274 for (MessagingListener l : mListeners) { 1275 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1276 } 1277 for (MessagingListener l : mListeners) { 1278 l.loadMessageForViewFinished(account, folder, uid, message); 1279 } 1280 } 1281 localFolder.close(false); 1282 return; 1283 } 1284 1285 /* 1286 * At this point the message is not available, so we need to download it 1287 * fully if possible. 1288 */ 1289 1290 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 1291 localStore.getPersistentCallbacks()); 1292 Folder remoteFolder = remoteStore.getFolder(folder); 1293 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1294 1295 // Get the remote message and fully download it (and save into local store) 1296 1297 if (remoteStore.requireStructurePrefetch()) { 1298 // For remote stores that require it, prefetch the message structure. 1299 FetchProfile fp = new FetchProfile(); 1300 fp.add(FetchProfile.Item.STRUCTURE); 1301 localFolder.fetch(new Message[] { message }, fp, null); 1302 1303 ArrayList<Part> viewables = new ArrayList<Part>(); 1304 ArrayList<Part> attachments = new ArrayList<Part>(); 1305 MimeUtility.collectParts(message, viewables, attachments); 1306 fp.clear(); 1307 for (Part part : viewables) { 1308 fp.add(part); 1309 } 1310 1311 remoteFolder.fetch(new Message[] { message }, fp, null); 1312 1313 // Store the updated message locally 1314 localFolder.updateMessage((LocalMessage)message); 1315 1316 } else { 1317 // Most remote stores can directly obtain the message using only uid 1318 Message remoteMessage = remoteFolder.getMessage(uid); 1319 FetchProfile fp = new FetchProfile(); 1320 fp.add(FetchProfile.Item.BODY); 1321 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1322 1323 // Store the message locally 1324 localFolder.appendMessages(new Message[] { remoteMessage }); 1325 } 1326 1327 // Now obtain the local copy for further access & manipulation 1328 message = localFolder.getMessage(uid); 1329 FetchProfile fp = new FetchProfile(); 1330 fp.add(FetchProfile.Item.BODY); 1331 localFolder.fetch(new Message[] { message }, fp, null); 1332 1333 // This is a view message request, so mark it read 1334 if (!message.isSet(Flag.SEEN)) { 1335 markMessageRead(account, folder, uid, true); 1336 } 1337 1338 // Mark that this message is now fully synched 1339 message.setFlag(Flag.X_DOWNLOADED_FULL, true); 1340 1341 synchronized (mListeners) { 1342 for (MessagingListener l : mListeners) { 1343 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1344 } 1345 for (MessagingListener l : mListeners) { 1346 l.loadMessageForViewFinished(account, folder, uid, message); 1347 } 1348 } 1349 remoteFolder.close(false); 1350 localFolder.close(false); 1351 } 1352 catch (Exception e) { 1353 synchronized (mListeners) { 1354 for (MessagingListener l : mListeners) { 1355 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1356 } 1357 } 1358 } 1359 } 1360 }); 1361 } 1362 1363 public void loadMessageForView(final EmailStore.Account account, final String folder, 1364 final String uid, MessagingListener listener) { 1365 synchronized (mListeners) { 1366 for (MessagingListener l : mListeners) { 1367 l.loadMessageForViewStarted(account, folder, uid); 1368 } 1369 } 1370 try { 1371 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 1372 null); 1373 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1374 localFolder.open(OpenMode.READ_WRITE, null); 1375 1376 Message message = localFolder.getMessage(uid); 1377 1378 synchronized (mListeners) { 1379 for (MessagingListener l : mListeners) { 1380 l.loadMessageForViewHeadersAvailable(account, folder, uid, message); 1381 } 1382 } 1383 1384 if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { 1385 loadMessageForViewRemote(account, folder, uid, listener); 1386 localFolder.close(false); 1387 return; 1388 } 1389 1390 if (!message.isSet(Flag.SEEN)) { 1391 markMessageRead(account, folder, uid, true); 1392 } 1393 1394 FetchProfile fp = new FetchProfile(); 1395 fp.add(FetchProfile.Item.ENVELOPE); 1396 fp.add(FetchProfile.Item.BODY); 1397 localFolder.fetch(new Message[] { 1398 message 1399 }, fp, null); 1400 1401 synchronized (mListeners) { 1402 for (MessagingListener l : mListeners) { 1403 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1404 } 1405 for (MessagingListener l : mListeners) { 1406 l.loadMessageForViewFinished(account, folder, uid, message); 1407 } 1408 } 1409 localFolder.close(false); 1410 } 1411 catch (Exception e) { 1412 synchronized (mListeners) { 1413 for (MessagingListener l : mListeners) { 1414 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1415 } 1416 } 1417 } 1418 } 1419 1420 /** 1421 * Attempts to load the attachment specified by part from the given account and message. 1422 * @param account 1423 * @param message 1424 * @param part 1425 * @param listener 1426 */ 1427 public void loadAttachment( 1428 final EmailStore.Account account, 1429 final Message message, 1430 final Part part, 1431 final Object tag, 1432 MessagingListener listener) { 1433 /* 1434 * Check if the attachment has already been downloaded. If it has there's no reason to 1435 * download it, so we just tell the listener that it's ready to go. 1436 */ 1437 try { 1438 if (part.getBody() != null) { 1439 synchronized (mListeners) { 1440 for (MessagingListener l : mListeners) { 1441 l.loadAttachmentStarted(account, message, part, tag, false); 1442 } 1443 for (MessagingListener l : mListeners) { 1444 l.loadAttachmentFinished(account, message, part, tag); 1445 } 1446 } 1447 return; 1448 } 1449 } 1450 catch (MessagingException me) { 1451 /* 1452 * If the header isn't there the attachment isn't downloaded yet, so just continue 1453 * on. 1454 */ 1455 } 1456 1457 synchronized (mListeners) { 1458 for (MessagingListener l : mListeners) { 1459 l.loadAttachmentStarted(account, message, part, tag, true); 1460 } 1461 } 1462 1463 put("loadAttachment", listener, new Runnable() { 1464 public void run() { 1465 try { 1466 LocalStore localStore = (LocalStore) Store.getInstance( 1467 account.getLocalStoreUri(mContext), mContext, null); 1468 /* 1469 * We clear out any attachments already cached in the entire store and then 1470 * we update the passed in message to reflect that there are no cached 1471 * attachments. This is in support of limiting the account to having one 1472 * attachment downloaded at a time. 1473 */ 1474 localStore.pruneCachedAttachments(); 1475 ArrayList<Part> viewables = new ArrayList<Part>(); 1476 ArrayList<Part> attachments = new ArrayList<Part>(); 1477 MimeUtility.collectParts(message, viewables, attachments); 1478 for (Part attachment : attachments) { 1479 attachment.setBody(null); 1480 } 1481 Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, 1482 localStore.getPersistentCallbacks()); 1483 LocalFolder localFolder = 1484 (LocalFolder) localStore.getFolder(message.getFolder().getName()); 1485 Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1486 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1487 1488 FetchProfile fp = new FetchProfile(); 1489 fp.add(part); 1490 remoteFolder.fetch(new Message[] { message }, fp, null); 1491 localFolder.updateMessage((LocalMessage)message); 1492 localFolder.close(false); 1493 synchronized (mListeners) { 1494 for (MessagingListener l : mListeners) { 1495 l.loadAttachmentFinished(account, message, part, tag); 1496 } 1497 } 1498 } 1499 catch (MessagingException me) { 1500 if (Config.LOGV) { 1501 Log.v(Email.LOG_TAG, "", me); 1502 } 1503 synchronized (mListeners) { 1504 for (MessagingListener l : mListeners) { 1505 l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); 1506 } 1507 } 1508 } 1509 } 1510 }); 1511 } 1512 1513 /** 1514 * Stores the given message in the Outbox and starts a sendPendingMessages command to 1515 * attempt to send the message. 1516 * @param account 1517 * @param message 1518 * @param listener 1519 */ 1520 public void sendMessage(final EmailStore.Account account, final Message message, 1521 MessagingListener listener) { 1522 try { 1523 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 1524 null); 1525 LocalFolder localFolder = 1526 (LocalFolder) localStore.getFolder(account.getOutboxFolderName(mContext)); 1527 localFolder.open(OpenMode.READ_WRITE, null); 1528 localFolder.appendMessages(new Message[] { 1529 message 1530 }); 1531 Message localMessage = localFolder.getMessage(message.getUid()); 1532 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1533 localFolder.close(false); 1534 sendPendingMessages(account, null); 1535 } 1536 catch (Exception e) { 1537// synchronized (mListeners) { 1538// for (MessagingListener l : mListeners) { 1539// // TODO general failed 1540// } 1541// } 1542 } 1543 } 1544 1545 /** 1546 * Attempt to send any messages that are sitting in the Outbox. 1547 * @param account 1548 * @param listener 1549 */ 1550 public void sendPendingMessages(final EmailStore.Account account, MessagingListener listener) { 1551 put("sendPendingMessages", listener, new Runnable() { 1552 public void run() { 1553 sendPendingMessagesSynchronous(account); 1554 } 1555 }); 1556 } 1557 1558 /** 1559 * Attempt to send any messages that are sitting in the Outbox. 1560 * @param account 1561 * @param listener 1562 */ 1563 public void sendPendingMessagesSynchronous(final EmailStore.Account account) { 1564 try { 1565 LocalStore localStore = (LocalStore) Store.getInstance( 1566 account.getLocalStoreUri(mContext), mContext, null); 1567 Folder localFolder = localStore.getFolder(account.getOutboxFolderName(mContext)); 1568 if (!localFolder.exists()) { 1569 return; 1570 } 1571 localFolder.open(OpenMode.READ_WRITE, null); 1572 1573 Message[] localMessages = localFolder.getMessages(null); 1574 1575 /* 1576 * The profile we will use to pull all of the content 1577 * for a given local message into memory for sending. 1578 */ 1579 FetchProfile fp = new FetchProfile(); 1580 fp.add(FetchProfile.Item.ENVELOPE); 1581 fp.add(FetchProfile.Item.BODY); 1582 1583 LocalFolder localSentFolder = 1584 (LocalFolder) localStore.getFolder(account.getSentFolderName(mContext)); 1585 1586 // Determine if upload to "sent" folder is necessary 1587 Store remoteStore = Store.getInstance( 1588 account.getStoreUri(mContext), mContext, localStore.getPersistentCallbacks()); 1589 boolean requireCopyMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 1590 1591 Sender sender = Sender.getInstance(account.getSenderUri(mContext), mContext); 1592 for (Message message : localMessages) { 1593 try { 1594 localFolder.fetch(new Message[] { message }, fp, null); 1595 try { 1596 // Send message using Sender 1597 message.setFlag(Flag.X_SEND_IN_PROGRESS, true); 1598 sender.sendMessage(message); 1599 message.setFlag(Flag.X_SEND_IN_PROGRESS, false); 1600 1601 // Upload to "sent" folder if not supported server-side 1602 if (requireCopyMessageToSentFolder) { 1603 localFolder.copyMessages( 1604 new Message[] { message },localSentFolder, null); 1605 PendingCommand command = new PendingCommand(); 1606 command.command = PENDING_COMMAND_APPEND; 1607 command.arguments = 1608 new String[] { localSentFolder.getName(), message.getUid() }; 1609 queuePendingCommand(account, command); 1610 processPendingCommands(account); 1611 } 1612 1613 // And delete from outbox 1614 message.setFlag(Flag.X_DESTROYED, true); 1615 } 1616 catch (Exception e) { 1617 message.setFlag(Flag.X_SEND_FAILED, true); 1618 synchronized (mListeners) { 1619 for (MessagingListener l : mListeners) { 1620 l.sendPendingMessageFailed(account, message, e); 1621 } 1622 } 1623 } 1624 } 1625 catch (Exception e) { 1626 synchronized (mListeners) { 1627 for (MessagingListener l : mListeners) { 1628 l.sendPendingMessageFailed(account, message, e); 1629 } 1630 } 1631 } 1632 } 1633 localFolder.expunge(); 1634 synchronized (mListeners) { 1635 for (MessagingListener l : mListeners) { 1636 l.sendPendingMessagesCompleted(account); 1637 } 1638 } 1639 } 1640 catch (Exception e) { 1641 synchronized (mListeners) { 1642 for (MessagingListener l : mListeners) { 1643 l.sendPendingMessagesFailed(account, e); 1644 } 1645 } 1646 } 1647 } 1648 1649 /** 1650 * We do the local portion of this synchronously because other activities may have to make 1651 * updates based on what happens here 1652 * @param account 1653 * @param folder 1654 * @param message 1655 * @param listener 1656 */ 1657 public void deleteMessage(final EmailStore.Account account, final String folder, 1658 final Message message, MessagingListener listener) { 1659 if (folder.equals(account.getTrashFolderName(mContext))) { 1660 return; 1661 } 1662 try { 1663 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 1664 null); 1665 Folder localFolder = localStore.getFolder(folder); 1666 Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName(mContext)); 1667 1668 localFolder.copyMessages(new Message[] { message }, localTrashFolder, null); 1669 message.setFlag(Flag.DELETED, true); 1670 1671 if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { 1672 PendingCommand command = new PendingCommand(); 1673 command.command = PENDING_COMMAND_TRASH; 1674 command.arguments = new String[] { folder, message.getUid() }; 1675 queuePendingCommand(account, command); 1676 processPendingCommands(account); 1677 } 1678 } 1679 catch (MessagingException me) { 1680 throw new RuntimeException("Error deleting message from local store.", me); 1681 } 1682 } 1683 1684 public void emptyTrash(final EmailStore.Account account, MessagingListener listener) { 1685 put("emptyTrash", listener, new Runnable() { 1686 public void run() { 1687 // TODO IMAP 1688 try { 1689 Store localStore = Store.getInstance( 1690 account.getLocalStoreUri(mContext), mContext, null); 1691 Folder localFolder = localStore.getFolder(account.getTrashFolderName(mContext)); 1692 localFolder.open(OpenMode.READ_WRITE, null); 1693 Message[] messages = localFolder.getMessages(null); 1694 localFolder.setFlags(messages, new Flag[] { 1695 Flag.DELETED 1696 }, true); 1697 localFolder.close(true); 1698 synchronized (mListeners) { 1699 for (MessagingListener l : mListeners) { 1700 l.emptyTrashCompleted(account); 1701 } 1702 } 1703 } 1704 catch (Exception e) { 1705 // TODO 1706 if (Config.LOGV) { 1707 Log.v(Email.LOG_TAG, "emptyTrash"); 1708 } 1709 } 1710 } 1711 }); 1712 } 1713 1714 /** 1715 * Checks mail for one or multiple accounts. If account is null all accounts 1716 * are checked. 1717 * 1718 * TODO: There is no use case for "check all accounts". Clean up this API to remove 1719 * that case. Callers can supply the appropriate list. 1720 * 1721 * TODO: Better protection against a failure in account n, which should not prevent 1722 * syncing account in accounts n+1 and beyond. 1723 * 1724 * @param context 1725 * @param accounts List of accounts to check, or null to check all accounts 1726 * @param listener 1727 */ 1728 public void checkMail(final Context context, EmailStore.Account[] accounts, 1729 final MessagingListener listener) { 1730 /** 1731 * Note: The somewhat tortured logic here is to guarantee proper ordering of events: 1732 * listeners: checkMailStarted 1733 * account 1: list folders 1734 * account 1: sync messages 1735 * account 2: list folders 1736 * account 2: sync messages 1737 * ... 1738 * account n: list folders 1739 * account n: sync messages 1740 * listeners: checkMailFinished 1741 */ 1742 synchronized (mListeners) { 1743 for (MessagingListener l : mListeners) { 1744 l.checkMailStarted(context, null); // TODO this needs to pass the actual array 1745 } 1746 } 1747 if (accounts == null) { 1748 // TODO eliminate this use case, implement, or ...? 1749// accounts = Preferences.getPreferences(context).getAccounts(); 1750 } 1751 for (final EmailStore.Account account : accounts) { 1752 listFolders(account, true, null); 1753 1754 put("checkMail", listener, new Runnable() { 1755 public void run() { 1756 sendPendingMessagesSynchronous(account); 1757 synchronizeMailboxSynchronous(account, Email.INBOX); 1758 } 1759 }); 1760 } 1761 put("checkMailFinished", listener, new Runnable() { 1762 public void run() { 1763 synchronized (mListeners) { 1764 for (MessagingListener l : mListeners) { 1765 l.checkMailFinished(context, null); // TODO this needs to pass actual array 1766 } 1767 } 1768 } 1769 }); 1770 } 1771 1772 public void saveDraft(final EmailStore.Account account, final Message message) { 1773 try { 1774 Store localStore = Store.getInstance(account.getLocalStoreUri(mContext), mContext, 1775 null); 1776 LocalFolder localFolder = 1777 (LocalFolder) localStore.getFolder(account.getDraftsFolderName(mContext)); 1778 localFolder.open(OpenMode.READ_WRITE, null); 1779 localFolder.appendMessages(new Message[] { 1780 message 1781 }); 1782 Message localMessage = localFolder.getMessage(message.getUid()); 1783 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1784 1785 PendingCommand command = new PendingCommand(); 1786 command.command = PENDING_COMMAND_APPEND; 1787 command.arguments = new String[] { 1788 localFolder.getName(), 1789 localMessage.getUid() }; 1790 queuePendingCommand(account, command); 1791 processPendingCommands(account); 1792 } 1793 catch (MessagingException e) { 1794 Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); 1795 } 1796 } 1797 1798 private static class Command { 1799 public Runnable runnable; 1800 1801 public MessagingListener listener; 1802 1803 public String description; 1804 1805 @Override 1806 public String toString() { 1807 return description; 1808 } 1809 } 1810} 1811