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