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