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