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