[geary/wip/713611-conversation-message-delete: 8/10] Substantially clean up source and API for main Conversation-related classes.



commit 07c1cb6da8e98dfa1c67ed1ccfd81f2ec5d8ddf3
Author: Michael James Gratton <mike vee net>
Date:   Thu Dec 7 15:49:35 2017 +1100

    Substantially clean up source and API for main Conversation-related classes.

 src/client/application/geary-controller.vala       |    4 +-
 .../conversation-list/conversation-list-store.vala |   10 +-
 .../conversation-viewer/conversation-list-box.vala |    2 +-
 src/engine/app/app-conversation-monitor.vala       |  829 ++++++++++---------
 src/engine/app/app-conversation.vala               |  176 +++--
 .../conversation-monitor/app-conversation-set.vala |  327 ++++----
 test/engine/app/app-conversation-set-test.vala     |  178 +----
 test/engine/app/app-conversation-test.vala         |   10 +-
 8 files changed, 743 insertions(+), 793 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 8f2fa50..65cd6b4 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -329,13 +329,13 @@ public class GearyController : Geary.BaseObject {
         // Close the ConversationMonitor
         if (current_conversations != null) {
             debug("Stopping conversation monitor for %s...",
-                  this.current_conversations.folder.to_string());
+                  this.current_conversations.base_folder.to_string());
             try {
                 yield this.current_conversations.stop_monitoring_async(null);
             } catch (Error err) {
                 debug(
                     "Error closing conversation monitor %s at shutdown: %s",
-                    this.current_conversations.folder.to_string(),
+                    this.current_conversations.base_folder.to_string(),
                     err.message
                 );
             } finally {
diff --git a/src/client/conversation-list/conversation-list-store.vala 
b/src/client/conversation-list/conversation-list-store.vala
index c3fc1dd..695e1ed 100644
--- a/src/client/conversation-list/conversation-list-store.vala
+++ b/src/client/conversation-list/conversation-list-store.vala
@@ -106,7 +106,7 @@ public class ConversationListStore : Gtk.ListStore {
             Priority.LOW, 60, update_date_strings
         );
         this.email_store = new Geary.App.EmailStore(
-            conversations.folder.account
+            conversations.base_folder.account
         );
         GearyApplication.instance.config.settings.changed[Configuration.DISPLAY_PREVIEW_KEY].connect(
             on_display_preview_changed);
@@ -182,7 +182,7 @@ public class ConversationListStore : Gtk.ListStore {
         if (emails.size < 1)
             return;
         
-        debug("Displaying %d previews for %s...", emails.size, conversation_monitor.folder.to_string());
+        debug("Displaying %d previews for %s...", emails.size, conversation_monitor.base_folder.to_string());
         foreach (Geary.Email email in emails) {
             Geary.App.Conversation? conversation = conversation_monitor.get_conversation_for_email(email.id);
             if (conversation != null)
@@ -190,7 +190,7 @@ public class ConversationListStore : Gtk.ListStore {
             else
                 debug("Couldn't find conversation for %s", email.id.to_string());
         }
-        debug("Displayed %d previews for %s", emails.size, conversation_monitor.folder.to_string());
+        debug("Displayed %d previews for %s", emails.size, conversation_monitor.base_folder.to_string());
     }
     
     private async Gee.Collection<Geary.Email> do_get_previews_async(
@@ -278,8 +278,8 @@ public class ConversationListStore : Gtk.ListStore {
         FormattedConversationData conversation_data = new FormattedConversationData(
             conversation,
             preview,
-            this.conversations.folder,
-            this.conversations.folder.account.information.get_all_mailboxes()
+            this.conversations.base_folder,
+            this.conversations.base_folder.account.information.get_all_mailboxes()
         );
 
         Gtk.TreePath? path = get_path(iter);
diff --git a/src/client/conversation-viewer/conversation-list-box.vala 
b/src/client/conversation-viewer/conversation-list-box.vala
index 48f1d74..241d24a 100644
--- a/src/client/conversation-viewer/conversation-list-box.vala
+++ b/src/client/conversation-viewer/conversation-list-box.vala
@@ -860,7 +860,7 @@ public class ConversationListBox : Gtk.ListBox {
         // Should be able to edit draft emails from any
         // conversation. This test should be more like "is in drafts
         // folder"
-        bool is_in_folder = this.conversation.is_in_current_folder(email.id);
+        bool is_in_folder = this.conversation.is_in_base_folder(email.id);
         bool is_draft = (this.is_draft_folder && is_in_folder);
 
         bool is_sent = false;
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index 47f525d..f607317 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -11,32 +11,41 @@ public class Geary.App.ConversationMonitor : BaseObject {
      */
     public const Geary.Email.Field REQUIRED_FIELDS = Geary.Email.Field.REFERENCES |
         Geary.Email.Field.FLAGS | Geary.Email.Field.DATE;
-    
+
     // # of messages to load at a time as we attempt to fill the min window.
     private const int WINDOW_FILL_MESSAGE_COUNT = 5;
-    
+
     private class ProcessJobContext : BaseObject {
         public Gee.HashMap<Geary.EmailIdentifier, Geary.Email> emails
             = new Gee.HashMap<Geary.EmailIdentifier, Geary.Email>();
-        
+
         public bool inside_scan;
-        
+
         public ProcessJobContext(bool inside_scan) {
             this.inside_scan = inside_scan;
         }
     }
-    
-    public Geary.Folder folder { get; private set; }
+
+
+    /** Folder from which the conversation is originating. */
+    public Folder base_folder { get; private set; }
+
+    /** Determines if this monitor is monitoring the base folder. */
     public bool is_monitoring { get; private set; default = false; }
+
+    /** Minimum number of emails large conversations should contain. */
     public int min_window_count { get { return _min_window_count; }
         set {
             _min_window_count = value;
             operation_queue.add(new FillWindowOperation(this, false));
         }
     }
-    
-    public Geary.ProgressMonitor progress_monitor { get { return operation_queue.progress_monitor; } }
-    
+
+    /** Indicates process loading conversations. */
+    public Geary.ProgressMonitor progress_monitor {
+        get { return operation_queue.progress_monitor; }
+    }
+
     private ConversationSet conversations = new ConversationSet();
     private Geary.Email.Field required_fields;
     private Geary.Folder.OpenFlags open_flags;
@@ -44,24 +53,27 @@ public class Geary.App.ConversationMonitor : BaseObject {
     private bool reseed_notified = false;
     private int _min_window_count = 0;
     private ConversationOperationQueue operation_queue = new ConversationOperationQueue();
-    
+
+
     /**
      * "monitoring-started" is fired when the Conversations folder has been opened for monitoring.
      */
     public virtual signal void monitoring_started() {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::monitoring_started",
-            folder.to_string());
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::monitoring_started",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "monitoring-stopped" is fired when the Geary.Folder object has closed (either due to error
      * or user) and the Conversations object is therefore unable to continue monitoring.
      */
     public virtual signal void monitoring_stopped() {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::monitoring_stopped",
-            folder.to_string());
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::monitoring_stopped",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "scan-started" is fired whenever beginning to load messages into the Conversations object.
      *
@@ -71,44 +83,49 @@ public class Geary.App.ConversationMonitor : BaseObject {
      * only a single "scan-completed" is fired to indicate multiple loads have finished.
      */
     public virtual signal void scan_started() {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::scan_started",
-            folder.to_string());
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::scan_started",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "scan-error" is fired when an Error is encounted while loading messages.  It will be followed
      * by a "scan-completed" signal.
      */
     public virtual signal void scan_error(Error err) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::scan_error %s",
-            folder.to_string(), err.message);
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::scan_error %s",
+                      this.base_folder.to_string(), err.message);
     }
-    
+
     /**
      * "scan-completed" is fired when the scan of the email has finished.
      */
     public virtual signal void scan_completed() {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::scan_completed",
-            folder.to_string());
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::scan_completed",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "seed-completed" is fired when the folder has opened and email has been populated.
      */
     public virtual signal void seed_completed() {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::seed_completed",
-            folder.to_string());
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::seed_completed",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "conversations-added" indicates that one or more new Conversations have been detected while
      * processing email, either due to a user-initiated load request or due to monitoring.
      */
     public virtual signal void conversations_added(Gee.Collection<Conversation> conversations) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::conversations_added %d",
-            folder.to_string(), conversations.size);
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::conversations_added %d",
+                      this.base_folder.to_string(), conversations.size);
     }
-    
+
     /**
      * "conversations-removed" is fired when all the email in a Conversation has been removed.
      * It's possible this will be called without a signal alerting that it's emails have been
@@ -119,21 +136,23 @@ public class Geary.App.ConversationMonitor : BaseObject {
      * user call to manually remove email from Conversations.
      */
     public virtual signal void conversations_removed(Gee.Collection<Conversation> conversations) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::conversations_removed %d",
-            folder.to_string(), conversations.size);
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::conversations_removed %d",
+                      this.base_folder.to_string(), conversations.size);
     }
-    
+
     /**
      * "conversation-appended" is fired when one or more Email objects have been added to the
      * specified Conversation.  This can happen due to a user-initiated load or while monitoring
      * the Folder.
      */
     public virtual signal void conversation_appended(Conversation conversation,
-        Gee.Collection<Geary.Email> email) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::conversation_appended",
-            folder.to_string());
+                                                     Gee.Collection<Email> email) {
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::conversation_appended",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "conversation-trimmed" is fired when one or more Emails have been removed from the Folder,
      * and therefore from the specified Conversation.  If the trimmed Email is the last usable
@@ -145,11 +164,12 @@ public class Geary.App.ConversationMonitor : BaseObject {
      * This is only called when monitoring is enabled.
      */
     public virtual signal void conversation_trimmed(Conversation conversation,
-        Gee.Collection<Geary.Email> email) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::conversation_trimmed",
-            folder.to_string());
+                                                    Gee.Collection<Email> email) {
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::conversation_trimmed",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * "email-flags-changed" is fired when the flags of an email in a conversation have changed,
      * as reported by the monitored folder.  The local copy of the Email is updated and this
@@ -159,104 +179,63 @@ public class Geary.App.ConversationMonitor : BaseObject {
      * is fired.  To know of all changes to all flags, subscribe to the Geary.Folder's
      * "email-flags-changed" signal.
      */
-    public virtual signal void email_flags_changed(Conversation conversation, Geary.Email email) {
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::email_flag_changed",
-            folder.to_string());
+    public virtual signal void email_flags_changed(Conversation conversation,
+                                                   Email email) {
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::email_flag_changed",
+                      this.base_folder.to_string());
     }
-    
+
     /**
      * Creates a conversation monitor for the given folder.
      *
-     * @param folder Folder to monitor
+     * @param base_folder the Folder to monitor
      * @param open_flags See {@link Geary.Folder}
      * @param required_fields See {@link Geary.Folder}
      * @param min_window_count Minimum number of conversations that will be loaded
      */
-    public ConversationMonitor(Geary.Folder folder, Geary.Folder.OpenFlags open_flags,
-        Geary.Email.Field required_fields, int min_window_count) {
-        this.folder = folder;
+    public ConversationMonitor(Folder base_folder,
+                               Folder.OpenFlags open_flags,
+                               Email.Field required_fields,
+                               int min_window_count) {
+        this.base_folder = base_folder;
         this.open_flags = open_flags;
         this.required_fields = required_fields | REQUIRED_FIELDS;
-        _min_window_count = min_window_count;
-    }
-
-    protected virtual void notify_monitoring_started() {
-        monitoring_started();
-    }
-    
-    protected virtual void notify_monitoring_stopped() {
-        monitoring_stopped();
-    }
-    
-    protected virtual void notify_scan_started() {
-        scan_started();
-    }
-    
-    protected virtual void notify_scan_error(Error err) {
-        scan_error(err);
-    }
-    
-    protected virtual void notify_scan_completed() {
-        scan_completed();
-    }
-    
-    protected virtual void notify_seed_completed() {
-        seed_completed();
-    }
-    
-    protected virtual void notify_conversations_added(Gee.Collection<Conversation> conversations) {
-        conversations_added(conversations);
-    }
-    
-    protected virtual void notify_conversations_removed(Gee.Collection<Conversation> conversations) {
-        conversations_removed(conversations);
-    }
-    
-    protected virtual void notify_conversation_appended(Conversation conversation,
-        Gee.Collection<Geary.Email> emails) {
-        conversation_appended(conversation, emails);
-    }
-    
-    protected virtual void notify_conversation_trimmed(Conversation conversation,
-        Gee.Collection<Geary.Email> emails) {
-        conversation_trimmed(conversation, emails);
-    }
-
-    protected virtual void notify_email_flags_changed(Conversation conversation, Geary.Email email) {
-        conversation.email_flags_changed(email);
-        email_flags_changed(conversation, email);
+        this._min_window_count = min_window_count;
     }
 
     public int get_conversation_count() {
         return conversations.size;
     }
-    
+
     public Gee.Collection<Conversation> get_conversations() {
         return conversations.conversations;
     }
-    
+
     public Geary.App.Conversation? get_conversation_for_email(Geary.EmailIdentifier email_id) {
         return conversations.get_by_email_identifier(email_id);
     }
-    
+
     public async bool start_monitoring_async(Cancellable? cancellable = null)
         throws Error {
         if (is_monitoring)
             return false;
-        
+
         // set before yield to guard against reentrancy
         is_monitoring = true;
-        
+
         cancellable_monitor = cancellable;
-        
+
         // Double check that the last run of the queue got stopped and that
         // it's empty.
         if (operation_queue.is_processing)
             yield operation_queue.stop_processing_async(cancellable_monitor);
         operation_queue.clear();
-        
-        bool reseed_now = (folder.get_open_state() != Geary.Folder.OpenState.CLOSED);
-        
+
+        bool reseed_now = (
+            this.base_folder.get_open_state() != Geary.Folder.OpenState.CLOSED
+        );
+
         // Add the necessary initial operations ahead of anything the folder
         // might add as it opens.
         operation_queue.add(new LocalLoadOperation(this));
@@ -266,51 +245,41 @@ public class Geary.App.ConversationMonitor : BaseObject {
             operation_queue.add(new ReseedOperation(this, "already opened"));
         operation_queue.add(new FillWindowOperation(this, false));
 
-        this.folder.email_appended.connect(on_folder_email_appended);
-        this.folder.email_inserted.connect(on_folder_email_inserted);
-        this.folder.email_removed.connect(on_folder_email_removed);
-        this.folder.opened.connect(on_folder_opened);
-        this.folder.account.email_appended.connect(on_account_email_appended);
-        this.folder.account.email_inserted.connect(on_account_email_inserted);
-        this.folder.account.email_removed.connect(on_account_email_removed);
-        this.folder.account.email_flags_changed.connect(on_account_email_flags_changed);
+        this.base_folder.email_appended.connect(on_folder_email_appended);
+        this.base_folder.email_inserted.connect(on_folder_email_inserted);
+        this.base_folder.email_removed.connect(on_folder_email_removed);
+        this.base_folder.opened.connect(on_folder_opened);
+        this.base_folder.account.email_appended.connect(on_account_email_appended);
+        this.base_folder.account.email_inserted.connect(on_account_email_inserted);
+        this.base_folder.account.email_removed.connect(on_account_email_removed);
+        this.base_folder.account.email_flags_changed.connect(on_account_email_flags_changed);
 
         try {
-            yield folder.open_async(open_flags, cancellable);
+            yield this.base_folder.open_async(open_flags, cancellable);
         } catch (Error err) {
             is_monitoring = false;
 
-            this.folder.email_appended.disconnect(on_folder_email_appended);
-            this.folder.email_inserted.disconnect(on_folder_email_inserted);
-            this.folder.email_removed.disconnect(on_folder_email_removed);
-            this.folder.opened.disconnect(on_folder_opened);
-            this.folder.account.email_appended.disconnect(on_account_email_appended);
-            this.folder.account.email_inserted.disconnect(on_account_email_inserted);
-            this.folder.account.email_removed.disconnect(on_account_email_removed);
-            this.folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
+            this.base_folder.email_appended.disconnect(on_folder_email_appended);
+            this.base_folder.email_inserted.disconnect(on_folder_email_inserted);
+            this.base_folder.email_removed.disconnect(on_folder_email_removed);
+            this.base_folder.opened.disconnect(on_folder_opened);
+            this.base_folder.account.email_appended.disconnect(on_account_email_appended);
+            this.base_folder.account.email_inserted.disconnect(on_account_email_inserted);
+            this.base_folder.account.email_removed.disconnect(on_account_email_removed);
+            this.base_folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
 
             throw err;
         }
-        
+
         notify_monitoring_started();
         reseed_notified = false;
-        
+
         // Process operations in the background.
         operation_queue.run_process_async.begin();
-        
+
         return true;
     }
-    
-    internal async void local_load_async() {
-        debug("ConversationMonitor seeding with local email for %s", folder.to_string());
-        try {
-            yield load_by_id_async(null, min_window_count, Folder.ListFlags.LOCAL_ONLY, cancellable_monitor);
-        } catch (Error e) {
-            debug("Error loading local messages: %s", e.message);
-        }
-        debug("ConversationMonitor seeded for %s", folder.to_string());
-    }
-    
+
     /**
      * Halt monitoring of the Folder and, if specified, close it.  Note that the Cancellable
      * supplied to start_monitoring_async() is used during monitoring but *not* for this method.
@@ -323,42 +292,258 @@ public class Geary.App.ConversationMonitor : BaseObject {
     public async bool stop_monitoring_async(Cancellable? cancellable) throws Error {
         if (!is_monitoring)
             return false;
-        
+
         yield operation_queue.stop_processing_async(cancellable);
-        
+
         // set now to prevent reentrancy during yield or signal
         is_monitoring = false;
 
-        this.folder.email_appended.disconnect(on_folder_email_appended);
-        this.folder.email_inserted.disconnect(on_folder_email_inserted);
-        this.folder.email_removed.disconnect(on_folder_email_removed);
-        this.folder.opened.disconnect(on_folder_opened);
-        this.folder.account.email_appended.disconnect(on_account_email_appended);
-        this.folder.account.email_inserted.disconnect(on_account_email_inserted);
-        this.folder.account.email_removed.disconnect(on_account_email_removed);
-        this.folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
+        this.base_folder.email_appended.disconnect(on_folder_email_appended);
+        this.base_folder.email_inserted.disconnect(on_folder_email_inserted);
+        this.base_folder.email_removed.disconnect(on_folder_email_removed);
+        this.base_folder.opened.disconnect(on_folder_opened);
+        this.base_folder.account.email_appended.disconnect(on_account_email_appended);
+        this.base_folder.account.email_inserted.disconnect(on_account_email_inserted);
+        this.base_folder.account.email_removed.disconnect(on_account_email_removed);
+        this.base_folder.account.email_flags_changed.disconnect(on_account_email_flags_changed);
 
         bool closing = false;
         Error? close_err = null;
         try {
-            closing = yield folder.close_async(cancellable);
+            closing = yield this.base_folder.close_async(cancellable);
         } catch (Error err) {
             // throw, but only after cleaning up (which is to say, if close_async() fails,
             // then the Folder is still treated as closed, which is the best that can be
             // expected; it definitely shouldn't still be considered open).
-            debug("Unable to close monitored folder %s: %s", folder.to_string(), err.message);
-            
+            debug("Unable to close monitored folder %s: %s",
+                  this.base_folder.to_string(), err.message);
             close_err = err;
         }
-        
+
         notify_monitoring_stopped();
-        
+
         if (close_err != null)
             throw close_err;
-        
+
         return closing;
     }
-    
+
+    internal async void local_load_async() {
+        debug("ConversationMonitor seeding with local email for %s",
+              this.base_folder.to_string());
+        try {
+            yield load_by_id_async(null, min_window_count, Folder.ListFlags.LOCAL_ONLY, cancellable_monitor);
+        } catch (Error e) {
+            debug("Error loading local messages: %s", e.message);
+        }
+        debug("ConversationMonitor seeded for %s",
+              this.base_folder.to_string());
+    }
+
+    internal async void append_emails_async(Gee.Collection<Geary.EmailIdentifier> appended_ids) {
+        debug("%d message(s) appended to %s, fetching to add to conversations...",
+              appended_ids.size, this.base_folder.to_string());
+
+        yield load_by_sparse_id(appended_ids, Geary.Folder.ListFlags.NONE, null);
+    }
+
+    internal async void remove_emails_async(Folder source_folder,
+                                            Gee.Collection<EmailIdentifier> removed_ids) {
+        debug("%d messages(s) removed from %s, trimming/removing conversations...",
+            removed_ids.size, source_folder.to_string()
+        );
+
+        Gee.Collection<Conversation> removed;
+        Gee.MultiMap<Conversation, Email> trimmed;
+        conversations.remove_all_emails_by_identifier(
+            source_folder.path,
+            removed_ids,
+            out removed,
+            out trimmed
+        );
+
+        // Check for conversations that have been evaporated as a
+        // result, update removed and trimmed collections to reflect
+        // any that evaporated
+        try {
+            Gee.Collection<Conversation> evaporated = yield check_conversations_in_base_folder(
+                trimmed.get_keys(), null
+            );
+            removed.add_all(evaporated);
+            foreach (Conversation target in evaporated) {
+                trimmed.remove_all(target);
+            }
+        } catch (Error err) {
+            debug("Error checking conversation for messages in %s: %s",
+                  this.base_folder.path.to_string(), err.message);
+        }
+
+        // Fire signals, clean up
+
+        foreach (Conversation conversation in trimmed.get_keys())
+            notify_conversation_trimmed(conversation, trimmed.get(conversation));
+
+        if (removed.size > 0)
+            notify_conversations_removed(removed);
+
+        if (source_folder == this.base_folder) {
+            // For any still-existing conversations that we've trimmed messages
+            // from, do a search for any messages that should still be there due to
+            // full conversations.  This way, some removed messages are instead
+            // "demoted" to out-of-folder emails.  This is kind of inefficient, but
+            // it doesn't seem like there's a way around it.
+            Gee.HashSet<RFC822.MessageID> search_message_ids = new Gee.HashSet<RFC822.MessageID>();
+            foreach (Conversation conversation in trimmed.get_keys()) {
+                search_message_ids.add_all(conversation.get_message_ids());
+            }
+            yield expand_conversations_async(search_message_ids, new ProcessJobContext(false));
+        }
+    }
+
+    internal async void external_append_emails_async(Folder folder,
+                                                     Gee.Collection<EmailIdentifier> appended_ids) {
+        if (get_search_blacklist().contains(folder.path))
+            return;
+
+        if (conversations.is_empty)
+            return;
+
+        debug("%d out of folder message(s) appended to %s, fetching to add to conversations...", 
appended_ids.size,
+            folder.to_string());
+
+        yield external_load_by_sparse_id(folder, appended_ids, Geary.Folder.ListFlags.NONE, null);
+    }
+
+    internal async void reseed_async(string why) {
+        Geary.EmailIdentifier? earliest_id = yield get_lowest_email_id_async(null);
+        try {
+            if (earliest_id != null) {
+                debug("ConversationMonitor (%s) reseeding starting from Email ID %s on opened %s", why,
+                    earliest_id.to_string(), this.base_folder.to_string());
+                yield load_by_id_async(earliest_id, int.MAX,
+                    Geary.Folder.ListFlags.OLDEST_TO_NEWEST | Geary.Folder.ListFlags.INCLUDING_ID,
+                    cancellable_monitor);
+            } else {
+                debug("ConversationMonitor (%s) reseeding latest %d emails on opened %s", why,
+                    min_window_count, this.base_folder.to_string());
+                yield load_by_id_async(null, min_window_count, Geary.Folder.ListFlags.NONE, 
cancellable_monitor);
+            }
+        } catch (Error e) {
+            debug("Reseed error: %s", e.message);
+        }
+
+        if (!reseed_notified) {
+            reseed_notified = true;
+            notify_seed_completed();
+        }
+    }
+
+    /**
+     * Attempts to load enough conversations to fill min_window_count.
+     */
+    internal async void fill_window_async(bool is_insert) {
+        if (!is_monitoring)
+            return;
+
+        if (!is_insert && min_window_count <= conversations.size)
+            return;
+
+        int initial_message_count = conversations.get_email_count();
+
+        // only do local-load if the Folder isn't completely opened, otherwise this operation
+        // will block other (more important) operations while it waits for the folder to
+        // remote-open
+        Folder.ListFlags flags;
+        switch (this.base_folder.get_open_state()) {
+            case Folder.OpenState.CLOSED:
+            case Folder.OpenState.LOCAL:
+            case Folder.OpenState.OPENING:
+                flags = Folder.ListFlags.LOCAL_ONLY;
+            break;
+
+            case Folder.OpenState.BOTH:
+            case Folder.OpenState.REMOTE:
+                flags = Folder.ListFlags.NONE;
+            break;
+
+            default:
+                assert_not_reached();
+        }
+
+        Geary.EmailIdentifier? low_id = yield get_lowest_email_id_async(null);
+        if (low_id != null && !is_insert) {
+            // Load at least as many messages as remianing conversations.
+            int num_to_load = min_window_count - conversations.size;
+            if (num_to_load < WINDOW_FILL_MESSAGE_COUNT)
+                num_to_load = WINDOW_FILL_MESSAGE_COUNT;
+
+            try {
+                yield load_by_id_async(low_id, num_to_load, flags, cancellable_monitor);
+            } catch(Error e) {
+                debug("Error filling conversation window: %s", e.message);
+            }
+        } else {
+            // No existing messages or an insert invalidated our existing list,
+            // need to start from scratch.
+            try {
+                yield load_by_id_async(null, min_window_count, flags, cancellable_monitor);
+            } catch(Error e) {
+                debug("Error filling conversation window: %s", e.message);
+            }
+        }
+
+        // Run again to make sure we're full unless we ran out of messages.
+        if (conversations.get_email_count() != initial_message_count)
+            operation_queue.add(new FillWindowOperation(this, is_insert));
+    }
+
+    protected virtual void notify_monitoring_started() {
+        monitoring_started();
+    }
+
+    protected virtual void notify_monitoring_stopped() {
+        monitoring_stopped();
+    }
+
+    protected virtual void notify_scan_started() {
+        scan_started();
+    }
+
+    protected virtual void notify_scan_error(Error err) {
+        scan_error(err);
+    }
+
+    protected virtual void notify_scan_completed() {
+        scan_completed();
+    }
+
+    protected virtual void notify_seed_completed() {
+        seed_completed();
+    }
+
+    protected virtual void notify_conversations_added(Gee.Collection<Conversation> conversations) {
+        conversations_added(conversations);
+    }
+
+    protected virtual void notify_conversations_removed(Gee.Collection<Conversation> conversations) {
+        conversations_removed(conversations);
+    }
+
+    protected virtual void notify_conversation_appended(Conversation conversation,
+        Gee.Collection<Geary.Email> emails) {
+        conversation_appended(conversation, emails);
+    }
+
+    protected virtual void notify_conversation_trimmed(Conversation conversation,
+        Gee.Collection<Geary.Email> emails) {
+        conversation_trimmed(conversation, emails);
+    }
+
+    protected virtual void notify_email_flags_changed(Conversation conversation, Geary.Email email) {
+        conversation.email_flags_changed(email);
+        email_flags_changed(conversation, email);
+    }
+
     /**
      * See Geary.Folder.list_email_by_id_async() for details of how these parameters operate.  Instead
      * of returning emails, this method will load the Conversations object with them sorted into
@@ -368,42 +553,58 @@ public class Geary.App.ConversationMonitor : BaseObject {
         Geary.Folder.ListFlags flags, Cancellable? cancellable) throws Error {
         notify_scan_started();
         try {
-            yield process_email_async(yield folder.list_email_by_id_async(initial_id,
-                count, required_fields, flags, cancellable), new ProcessJobContext(true));
+            yield process_email_async(
+                yield this.base_folder.list_email_by_id_async(
+                    initial_id, count, required_fields, flags, cancellable
+                ),
+                new ProcessJobContext(true)
+            );
         } catch (Error err) {
             list_error(err);
             throw err;
         }
     }
-    
+
     private async void load_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
         Geary.Folder.ListFlags flags, Cancellable? cancellable) {
         notify_scan_started();
-        
+
         try {
-            yield process_email_async(yield folder.list_email_by_sparse_id_async(ids,
-                required_fields, flags, cancellable), new ProcessJobContext(true));
+            yield process_email_async(
+                yield this.base_folder.list_email_by_sparse_id_async(
+                    ids, required_fields, flags, cancellable
+                ),
+                new ProcessJobContext(true)
+            );
         } catch (Error err) {
             list_error(err);
         }
     }
-    
-    private async void external_load_by_sparse_id(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Folder.ListFlags flags, Cancellable? cancellable) {
+
+    /**
+     * Loads messages from outside the monitor's base folder.
+     *
+     * Since this requires opening and closing the other folder, it is
+     * handled separately.
+     */
+    private async void external_load_by_sparse_id(Folder folder,
+                                                  Gee.Collection<EmailIdentifier> ids,
+                                                  Folder.ListFlags flags,
+                                                  Cancellable? cancellable) {
         bool opened = false;
         try {
             yield folder.open_async(Geary.Folder.OpenFlags.NONE, cancellable);
             opened = true;
-            
+
             debug("Listing %d external emails", ids.size);
-            
+
             // First just get the bare minimum we need to determine if we even
             // care about the messages.
             Gee.List<Geary.Email>? emails = yield folder.list_email_by_sparse_id_async(ids,
                 Geary.Email.Field.REFERENCES, flags, cancellable);
-            
+
             debug("List found %d emails", (emails == null ? 0 : emails.size));
-            
+
             Gee.HashSet<Geary.EmailIdentifier> relevant_ids = new Gee.HashSet<Geary.EmailIdentifier>();
             foreach (Geary.Email email in emails) {
                 Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
@@ -411,16 +612,16 @@ public class Geary.App.ConversationMonitor : BaseObject {
                     Geary.traverse<RFC822.MessageID>(ancestors).any(id => conversations.has_message_id(id)))
                     relevant_ids.add(email.id);
             }
-            
+
             debug("%d external emails are relevant to current conversations", relevant_ids.size);
-            
+
             // List the relevant messages again with the full set of fields, to
             // make sure when we load them from the database we have all the
             // data we need.
             yield folder.list_email_by_sparse_id_async(relevant_ids, required_fields, flags, cancellable);
             yield folder.close_async(cancellable);
             opened = false;
-            
+
             Gee.ArrayList<Geary.Email> search_emails = new Gee.ArrayList<Geary.Email>();
             foreach (Geary.EmailIdentifier id in relevant_ids) {
                 // TODO: parallelize this.
@@ -432,9 +633,9 @@ public class Geary.App.ConversationMonitor : BaseObject {
                     debug("Error fetching out of folder message: %s", e.message);
                 }
             }
-            
+
             debug("Fetched %d relevant emails locally", search_emails.size);
-            
+
             yield process_email_async(search_emails, new ProcessJobContext(false));
         } catch (Error e) {
             debug("Error loading external emails: %s", e.message);
@@ -447,27 +648,29 @@ public class Geary.App.ConversationMonitor : BaseObject {
             }
         }
     }
-    
+
     private void list_error(Error err) {
-        debug("Error while assembling conversations in %s: %s", folder.to_string(), err.message);
+        debug("Error while assembling conversations in %s: %s",
+              this.base_folder.to_string(), err.message);
         notify_scan_error(err);
         notify_scan_completed();
     }
-    
+
     private async void process_email_async(Gee.Collection<Geary.Email>? emails, ProcessJobContext job) {
         if (emails == null || emails.size == 0) {
             yield process_email_complete_async(job);
             return;
         }
-        
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email: %d emails",
-            folder.to_string(), emails.size);
-        
+
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::process_email: %d emails",
+                      this.base_folder.to_string(), emails.size);
+
         Gee.HashSet<RFC822.MessageID> new_message_ids = new Gee.HashSet<RFC822.MessageID>();
         foreach (Geary.Email email in emails) {
             if (!job.emails.has_key(email.id)) {
                 job.emails.set(email.id, email);
-            
+
                 Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
                 if (ancestors != null) {
                     Geary.traverse<RFC822.MessageID>(ancestors)
@@ -476,77 +679,78 @@ public class Geary.App.ConversationMonitor : BaseObject {
                 }
             }
         }
-        
+
         // Expand the conversation to include any Message-IDs we know we need
         // and may have on disk, but aren't in the folder.
         yield expand_conversations_async(new_message_ids, job);
-        
-        Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email completed: %d 
emails",
-            folder.to_string(), emails.size);
+
+        Logging.debug(Logging.Flag.CONVERSATIONS,
+                      "[%s] ConversationMonitor::process_email completed: %d emails",
+                      this.base_folder.to_string(), emails.size);
     }
-    
+
     private Gee.Collection<Geary.FolderPath> get_search_blacklist() {
         Geary.SpecialFolderType[] blacklisted_folder_types = {
             Geary.SpecialFolderType.SPAM,
             Geary.SpecialFolderType.TRASH,
             Geary.SpecialFolderType.DRAFTS,
         };
-        
+
         Gee.ArrayList<Geary.FolderPath?> blacklist = new Gee.ArrayList<Geary.FolderPath?>();
         foreach (Geary.SpecialFolderType type in blacklisted_folder_types) {
             try {
-                Geary.Folder? blacklist_folder = folder.account.get_special_folder(type);
+                Geary.Folder? blacklist_folder = this.base_folder.account.get_special_folder(type);
                 if (blacklist_folder != null)
                     blacklist.add(blacklist_folder.path);
             } catch (Error e) {
                 debug("Error finding special folder %s on account %s: %s",
-                    type.to_string(), folder.account.to_string(), e.message);
+                    type.to_string(), this.base_folder.account.to_string(), e.message);
             }
         }
-        
+
         // Add "no folders" so we omit results that have been deleted permanently from the server.
         blacklist.add(null);
-        
+
         return blacklist;
     }
-    
+
     private Geary.EmailFlags get_search_flag_blacklist() {
         Geary.EmailFlags flags = new Geary.EmailFlags();
         flags.add(Geary.EmailFlags.DRAFT);
-        
+
         return flags;
     }
-    
+
     private async void expand_conversations_async(Gee.Set<RFC822.MessageID> needed_message_ids,
         ProcessJobContext job) {
         if (needed_message_ids.size == 0) {
             yield process_email_complete_async(job);
             return;
         }
-        
+
         Logging.debug(Logging.Flag.CONVERSATIONS,
-            "[%s] ConversationMonitor::expand_conversations: %d email ids",
-            folder.to_string(), needed_message_ids.size);
-        
+                      "[%s] ConversationMonitor::expand_conversations: %d email ids",
+                      this.base_folder.to_string(), needed_message_ids.size);
+
         Gee.Collection<Geary.FolderPath> folder_blacklist = get_search_blacklist();
         Geary.EmailFlags flag_blacklist = get_search_flag_blacklist();
-        
+
         // execute all the local search operations at once
         Nonblocking.Batch batch = new Nonblocking.Batch();
         foreach (RFC822.MessageID message_id in needed_message_ids) {
-            batch.add(new LocalSearchOperation(folder.account, message_id, required_fields,
+            batch.add(new LocalSearchOperation(this.base_folder.account, message_id, required_fields,
                 folder_blacklist, flag_blacklist));
         }
-        
+
         try {
             yield batch.execute_all_async();
         } catch (Error err) {
             debug("Unable to search local mail for conversations: %s", err.message);
-            
+
             yield process_email_complete_async(job);
             return;
         }
-        
+
         // collect their results into a single collection of addt'l emails
         Gee.HashMap<Geary.EmailIdentifier, Geary.Email> needed_messages = new Gee.HashMap<
             Geary.EmailIdentifier, Geary.Email>();
@@ -558,16 +762,16 @@ public class Geary.App.ConversationMonitor : BaseObject {
                     .add_all_to_map<Geary.EmailIdentifier>(needed_messages, e => e.id);
             }
         }
-        
+
         // process them as through they're been loaded from the folder; this, in turn, may
         // require more local searching of email
         yield process_email_async(needed_messages.values, job);
-        
+
         Logging.debug(Logging.Flag.CONVERSATIONS,
-            "[%s] ConversationMonitor::expand_conversations completed: %d email ids (%d found)",
-            folder.to_string(), needed_message_ids.size, needed_messages.size);
+                      "[%s] ConversationMonitor::expand_conversations completed: %d email ids (%d found)",
+                      this.base_folder.to_string(), needed_message_ids.size, needed_messages.size);
     }
-    
+
     private async void process_email_complete_async(ProcessJobContext job) {
         Gee.Collection<Geary.App.Conversation>? added = null;
         Gee.MultiMap<Geary.App.Conversation, Geary.Email>? appended = null;
@@ -575,215 +779,48 @@ public class Geary.App.ConversationMonitor : BaseObject {
         try {
             // Get known paths for all emails
             Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? email_paths =
-                yield this.folder.account.get_containing_folders_async(
+                yield this.base_folder.account.get_containing_folders_async(
                     job.emails.keys, null
                 );
 
             // Add them to the conversation set
-            yield this.conversations.add_all_emails_async(
-                job.emails.values,
-                email_paths,
-                this.folder,
-                out added,
-                out appended,
-                out removed_due_to_merge,
-                null);
+            this.conversations.add_all_emails(
+                job.emails.values, email_paths, this.base_folder,
+                out added, out appended, out removed_due_to_merge
+            );
         } catch (Error err) {
             debug("Unable to add emails to conversation: %s", err.message);
-            
+
             // fall-through
         }
-        
+
         if (removed_due_to_merge != null && removed_due_to_merge.size > 0) {
             notify_conversations_removed(removed_due_to_merge);
         }
-        
+
         if (added != null && added.size > 0)
             notify_conversations_added(added);
-        
+
         if (appended != null) {
             foreach (Geary.App.Conversation conversation in appended.get_keys())
                 notify_conversation_appended(conversation, appended.get(conversation));
         }
-        
+
         if (job.inside_scan)
             notify_scan_completed();
     }
 
-    internal async void append_emails_async(Gee.Collection<Geary.EmailIdentifier> appended_ids) {
-        debug("%d message(s) appended to %s, fetching to add to conversations...", appended_ids.size,
-            folder.to_string());
-        
-        yield load_by_sparse_id(appended_ids, Geary.Folder.ListFlags.NONE, null);
-    }
-
-    internal async void remove_emails_async(Folder source_folder,
-                                            Gee.Collection<EmailIdentifier> removed_ids) {
-        debug("%d messages(s) removed from %s, trimming/removing conversations...",
-            removed_ids.size, source_folder.to_string()
-        );
-
-        Gee.Collection<Conversation> removed;
-        Gee.MultiMap<Conversation, Email> trimmed;
-        conversations.remove_all_emails_by_identifier(
-            source_folder.path,
-            removed_ids,
-            out removed,
-            out trimmed
-        );
-
-        // Check for conversations that have been evaporated as a
-        // result, update removed and trimmed collections to reflect
-        // any that evaporated
-        try {
-            Gee.Collection<Conversation> evaporated = yield check_conversations_in_base_folder(
-                trimmed.get_keys(), null
-            );
-            removed.add_all(evaporated);
-            foreach (Conversation target in evaporated) {
-                trimmed.remove_all(target);
-            }
-        } catch (Error err) {
-            debug("Error checking conversation for messages in %s: %s",
-                  this.folder.path.to_string(), err.message);
-        }
-
-        // Fire signals, clean up
-
-        foreach (Conversation conversation in trimmed.get_keys())
-            notify_conversation_trimmed(conversation, trimmed.get(conversation));
-
-        if (removed.size > 0)
-            notify_conversations_removed(removed);
-
-        if (source_folder == this.folder) {
-            // For any still-existing conversations that we've trimmed messages
-            // from, do a search for any messages that should still be there due to
-            // full conversations.  This way, some removed messages are instead
-            // "demoted" to out-of-folder emails.  This is kind of inefficient, but
-            // it doesn't seem like there's a way around it.
-            Gee.HashSet<RFC822.MessageID> search_message_ids = new Gee.HashSet<RFC822.MessageID>();
-            foreach (Conversation conversation in trimmed.get_keys()) {
-                search_message_ids.add_all(conversation.get_message_ids());
-            }
-            yield expand_conversations_async(search_message_ids, new ProcessJobContext(false));
-        }
-    }
-
-    internal async void external_append_emails_async(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> appended_ids) {
-        if (get_search_blacklist().contains(folder.path))
-            return;
-        
-        if (conversations.is_empty)
-            return;
-        
-        debug("%d out of folder message(s) appended to %s, fetching to add to conversations...", 
appended_ids.size,
-            folder.to_string());
-        
-        yield external_load_by_sparse_id(folder, appended_ids, Geary.Folder.ListFlags.NONE, null);
-    }
-    
     private async Geary.EmailIdentifier? get_lowest_email_id_async(Cancellable? cancellable) {
         Geary.EmailIdentifier? earliest_id = null;
         try {
-            yield folder.find_boundaries_async(conversations.get_email_identifiers(),
+            yield this.base_folder.find_boundaries_async(conversations.get_email_identifiers(),
                 out earliest_id, null, cancellable);
         } catch (Error e) {
             debug("Error finding earliest email identifier: %s", e.message);
         }
-        
+
         return earliest_id;
     }
-    
-    internal async void reseed_async(string why) {
-        Geary.EmailIdentifier? earliest_id = yield get_lowest_email_id_async(null);
-        try {
-            if (earliest_id != null) {
-                debug("ConversationMonitor (%s) reseeding starting from Email ID %s on opened %s", why,
-                    earliest_id.to_string(), folder.to_string());
-                yield load_by_id_async(earliest_id, int.MAX,
-                    Geary.Folder.ListFlags.OLDEST_TO_NEWEST | Geary.Folder.ListFlags.INCLUDING_ID,
-                    cancellable_monitor);
-            } else {
-                debug("ConversationMonitor (%s) reseeding latest %d emails on opened %s", why,
-                    min_window_count, folder.to_string());
-                yield load_by_id_async(null, min_window_count, Geary.Folder.ListFlags.NONE, 
cancellable_monitor);
-            }
-        } catch (Error e) {
-            debug("Reseed error: %s", e.message);
-        }
-        
-        if (!reseed_notified) {
-            reseed_notified = true;
-            notify_seed_completed();
-        }
-    }
-    
-    private void on_folder_opened(Geary.Folder.OpenState state, int count) {
-        // once remote is open, reseed with messages from the earliest ID to the latest
-        if (state == Geary.Folder.OpenState.BOTH || state == Geary.Folder.OpenState.REMOTE)
-            operation_queue.add(new ReseedOperation(this, state.to_string()));
-    }
-    
-    /**
-     * Attempts to load enough conversations to fill min_window_count.
-     */
-    internal async void fill_window_async(bool is_insert) {
-        if (!is_monitoring)
-            return;
-        
-        if (!is_insert && min_window_count <= conversations.size)
-            return;
-        
-        int initial_message_count = conversations.get_email_count();
-        
-        // only do local-load if the Folder isn't completely opened, otherwise this operation
-        // will block other (more important) operations while it waits for the folder to
-        // remote-open
-        Folder.ListFlags flags;
-        switch (folder.get_open_state()) {
-            case Folder.OpenState.CLOSED:
-            case Folder.OpenState.LOCAL:
-            case Folder.OpenState.OPENING:
-                flags = Folder.ListFlags.LOCAL_ONLY;
-            break;
-            
-            case Folder.OpenState.BOTH:
-            case Folder.OpenState.REMOTE:
-                flags = Folder.ListFlags.NONE;
-            break;
-            
-            default:
-                assert_not_reached();
-        }
-        
-        Geary.EmailIdentifier? low_id = yield get_lowest_email_id_async(null);
-        if (low_id != null && !is_insert) {
-            // Load at least as many messages as remianing conversations.
-            int num_to_load = min_window_count - conversations.size;
-            if (num_to_load < WINDOW_FILL_MESSAGE_COUNT)
-                num_to_load = WINDOW_FILL_MESSAGE_COUNT;
-            
-            try {
-                yield load_by_id_async(low_id, num_to_load, flags, cancellable_monitor);
-            } catch(Error e) {
-                debug("Error filling conversation window: %s", e.message);
-            }
-        } else {
-            // No existing messages or an insert invalidated our existing list,
-            // need to start from scratch.
-            try {
-                yield load_by_id_async(null, min_window_count, flags, cancellable_monitor);
-            } catch(Error e) {
-                debug("Error filling conversation window: %s", e.message);
-            }
-        }
-        
-        // Run again to make sure we're full unless we ran out of messages.
-        if (conversations.get_email_count() != initial_message_count)
-            operation_queue.add(new FillWindowOperation(this, is_insert));
-    }
 
     /**
      * Check conversations to see if they still exist in the base folder.
@@ -798,11 +835,11 @@ public class Geary.App.ConversationMonitor : BaseObject {
         Gee.ArrayList<Conversation> evaporated = new Gee.ArrayList<Conversation>();
         foreach (Geary.App.Conversation conversation in conversations) {
             int count = yield conversation.get_count_in_folder_async(
-                this.folder.account, this.folder.path, cancellable
+                this.base_folder.account, this.base_folder.path, cancellable
             );
             if (count == 0) {
                 debug("Evaporating conversation %s because it has no emails in %s",
-                      conversation.to_string(), this.folder.to_string());
+                      conversation.to_string(), this.base_folder.to_string());
                 this.conversations.remove_conversation(conversation);
                 evaporated.add(conversation);
             }
@@ -811,6 +848,12 @@ public class Geary.App.ConversationMonitor : BaseObject {
         return evaporated;
     }
 
+    private void on_folder_opened(Geary.Folder.OpenState state, int count) {
+        // once remote is open, reseed with messages from the earliest ID to the latest
+        if (state == Geary.Folder.OpenState.BOTH || state == Geary.Folder.OpenState.REMOTE)
+            operation_queue.add(new ReseedOperation(this, state.to_string()));
+    }
+
     private void on_folder_email_appended(Gee.Collection<EmailIdentifier> appended_ids) {
         operation_queue.add(new AppendOperation(this, appended_ids));
     }
@@ -820,13 +863,13 @@ public class Geary.App.ConversationMonitor : BaseObject {
     }
 
     private void on_folder_email_removed(Gee.Collection<EmailIdentifier> removed_ids) {
-        operation_queue.add(new RemoveOperation(this, this.folder, removed_ids));
+        operation_queue.add(new RemoveOperation(this, this.base_folder, removed_ids));
         operation_queue.add(new FillWindowOperation(this, false));
     }
 
     private void on_account_email_appended(Folder folder,
                                            Gee.Collection<EmailIdentifier> added) {
-        if (folder != this.folder) {
+        if (folder != this.base_folder) {
             operation_queue.add(
                 new ExternalAppendOperation(this, folder, added)
             );
@@ -835,14 +878,14 @@ public class Geary.App.ConversationMonitor : BaseObject {
 
     private void on_account_email_inserted(Folder folder,
                                            Gee.Collection<EmailIdentifier> added) {
-        if (folder != this.folder) {
+        if (folder != this.base_folder) {
             operation_queue.add(new FillWindowOperation(this, false));
         }
     }
 
     private void on_account_email_removed(Folder folder,
                                           Gee.Collection<EmailIdentifier> removed) {
-        if (folder != this.folder) {
+        if (folder != this.base_folder) {
             operation_queue.add(new RemoveOperation(this, folder, removed));
         }
     }
diff --git a/src/engine/app/app-conversation.vala b/src/engine/app/app-conversation.vala
index 7c6e9a0..6442710 100644
--- a/src/engine/app/app-conversation.vala
+++ b/src/engine/app/app-conversation.vala
@@ -18,9 +18,9 @@ public class Geary.App.Conversation : BaseObject {
         RECV_DATE_ASCENDING,
         RECV_DATE_DESCENDING
     }
-    
+
     /**
-     * Specify the location of the {@link Email} in relation to the {@link Folder} being monitored
+     * Specify the location of the {@link Email} in relation to the base folder being monitored
      * by the {@link Conversation}'s {@link ConversationMonitor}.
      *
      * IN_FOLDER represents Email that is found in the Folder the ConversationMonitor is
@@ -37,9 +37,9 @@ public class Geary.App.Conversation : BaseObject {
         OUT_OF_FOLDER_IN_FOLDER,
         ANYWHERE
     }
-    
+
     private static int next_convnum = 0;
-    
+
     /** Folder from which the conversation originated. */
     public Folder base_folder { get; private set; }
 
@@ -71,12 +71,12 @@ public class Geary.App.Conversation : BaseObject {
      * Fired when email has been added to this conversation.
      */
     public signal void appended(Geary.Email email);
-    
+
     /**
      * Fired when email has been trimmed from this conversation.
      */
     public signal void trimmed(Geary.Email email);
-    
+
     /**
      * Fired when the flags of an email in this conversation have changed.
      */
@@ -97,7 +97,7 @@ public class Geary.App.Conversation : BaseObject {
     public int get_count() {
         return emails.size;
     }
-    
+
     /**
      * Returns the number of emails in the conversation in a particular folder.
      */
@@ -105,7 +105,7 @@ public class Geary.App.Conversation : BaseObject {
         Cancellable? cancellable) throws Error {
         Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? folder_map
             = yield account.get_containing_folders_async(emails.keys, cancellable);
-        
+
         int count = 0;
         if (folder_map != null) {
             foreach (Geary.EmailIdentifier id in folder_map.get_keys()) {
@@ -113,10 +113,59 @@ public class Geary.App.Conversation : BaseObject {
                     ++count;
             }
         }
-        
+
         return count;
     }
-    
+
+    /**
+     * Returns true if *any* message in the conversation is unread.
+     */
+    public bool is_unread() {
+        return has_flag(Geary.EmailFlags.UNREAD);
+    }
+
+    /**
+     * Returns true if any message in the conversation is not unread.
+     */
+    public bool has_any_read_message() {
+        return is_missing_flag(Geary.EmailFlags.UNREAD);
+    }
+
+    /**
+     * Returns true if *any* message in the conversation is flagged.
+     */
+    public bool is_flagged() {
+        return has_flag(Geary.EmailFlags.FLAGGED);
+    }
+
+    /**
+     * Returns the earliest (first sent) email in the Conversation.
+     */
+    public Geary.Email? get_earliest_sent_email(Location location) {
+        return get_single_email(Ordering.SENT_DATE_ASCENDING, location);
+    }
+
+    /**
+     * Returns the latest (most recently sent) email in the Conversation.
+     */
+    public Geary.Email? get_latest_sent_email(Location location) {
+        return get_single_email(Ordering.SENT_DATE_DESCENDING, location);
+    }
+
+    /**
+     * Returns the earliest (first received) email in the Conversation.
+     */
+    public Geary.Email? get_earliest_recv_email(Location location) {
+        return get_single_email(Ordering.RECV_DATE_ASCENDING, location);
+    }
+
+    /**
+     * Returns the latest (most recently received) email in the Conversation.
+     */
+    public Geary.Email? get_latest_recv_email(Location location) {
+        return get_single_email(Ordering.RECV_DATE_DESCENDING, location);
+    }
+
     /**
      * Returns all the email in the conversation sorted and filtered according to the specifiers.
      *
@@ -131,58 +180,58 @@ public class Geary.App.Conversation : BaseObject {
             case Ordering.SENT_DATE_ASCENDING:
                 email = sent_date_ascending;
             break;
-            
+
             case Ordering.SENT_DATE_DESCENDING:
                 email = sent_date_descending;
             break;
-            
+
             case Ordering.RECV_DATE_ASCENDING:
                 email = recv_date_ascending;
             break;
-            
+
             case Ordering.RECV_DATE_DESCENDING:
                 email = recv_date_descending;
             break;
-            
+
             case Ordering.NONE:
                 email = emails.values;
             break;
-            
+
             default:
                 assert_not_reached();
         }
-        
+
         switch (location) {
             case Location.IN_FOLDER:
                 email = traverse<Email>(email)
-                    .filter((e) => !is_in_current_folder(e.id))
+                    .filter((e) => !is_in_base_folder(e.id))
                     .to_array_list();
             break;
-            
+
             case Location.OUT_OF_FOLDER:
                 email = traverse<Email>(email)
-                    .filter((e) => is_in_current_folder(e.id))
+                    .filter((e) => is_in_base_folder(e.id))
                     .to_array_list();
             break;
-            
+
             case Location.IN_FOLDER_OUT_OF_FOLDER:
             case Location.OUT_OF_FOLDER_IN_FOLDER:
             case Location.ANYWHERE:
                 // make a modifiable copy
                 email = traverse<Email>(email).to_array_list();
             break;
-            
+
             default:
                 assert_not_reached();
         }
-        
+
         return email;
     }
 
     /**
      * Determines if the given id is in the conversation's base folder.
      */
-    public bool is_in_current_folder(Geary.EmailIdentifier id) {
+    public bool is_in_base_folder(Geary.EmailIdentifier id) {
         Gee.Collection<Geary.FolderPath>? paths = this.path_map.get(id);
         return (paths != null && paths.contains(this.base_folder.path));
     }
@@ -203,7 +252,7 @@ public class Geary.App.Conversation : BaseObject {
      * Determines if an email with the give id exists in the conversation.
      */
     public bool contains_email_by_id(EmailIdentifier id) {
-        return emails.contains(id);
+        return emails.has_key(id);
     }
 
     /**
@@ -231,6 +280,13 @@ public class Geary.App.Conversation : BaseObject {
     }
 
     /**
+     * Returns a string representation for debugging.
+     */
+    public string to_string() {
+        return "[#%d] (%d emails)".printf(convnum, emails.size);
+    }
+
+    /**
      * Add the email to the conversation if not already present.
      *
      * The value of `known_paths` should contain all the known {@link
@@ -311,61 +367,12 @@ public class Geary.App.Conversation : BaseObject {
         this.path_map.remove(id, path);
     }
 
-    /**
-     * Returns true if *any* message in the conversation is unread.
-     */
-    public bool is_unread() {
-        return has_flag(Geary.EmailFlags.UNREAD);
-    }
-
-    /**
-     * Returns true if any message in the conversation is not unread.
-     */
-    public bool has_any_read_message() {
-        return is_missing_flag(Geary.EmailFlags.UNREAD);
-    }
-
-    /**
-     * Returns true if *any* message in the conversation is flagged.
-     */
-    public bool is_flagged() {
-        return has_flag(Geary.EmailFlags.FLAGGED);
-    }
-    
-    /**
-     * Returns the earliest (first sent) email in the Conversation.
-     */
-    public Geary.Email? get_earliest_sent_email(Location location) {
-        return get_single_email(Ordering.SENT_DATE_ASCENDING, location);
-    }
-    
-    /**
-     * Returns the latest (most recently sent) email in the Conversation.
-     */
-    public Geary.Email? get_latest_sent_email(Location location) {
-        return get_single_email(Ordering.SENT_DATE_DESCENDING, location);
-    }
-    
-    /**
-     * Returns the earliest (first received) email in the Conversation.
-     */
-    public Geary.Email? get_earliest_recv_email(Location location) {
-        return get_single_email(Ordering.RECV_DATE_ASCENDING, location);
-    }
-    
-    /**
-     * Returns the latest (most recently received) email in the Conversation.
-     */
-    public Geary.Email? get_latest_recv_email(Location location) {
-        return get_single_email(Ordering.RECV_DATE_DESCENDING, location);
-    }
-
     private Geary.Email? get_single_email(Ordering ordering, Location location) {
         // note that the location-ordering preferences are treated as ANYWHERE by get_emails()
         Gee.Collection<Geary.Email> all = get_emails(ordering, location);
         if (all.size == 0)
             return null;
-        
+
         // Because IN_FOLDER_OUT_OF_FOLDER and OUT_OF_FOLDER_IN_FOLDER are treated as ANYWHERE,
         // have to do our own filtering
         switch (location) {
@@ -373,30 +380,30 @@ public class Geary.App.Conversation : BaseObject {
             case Location.OUT_OF_FOLDER:
             case Location.ANYWHERE:
                 return traverse<Email>(all).first();
-            
+
             case Location.IN_FOLDER_OUT_OF_FOLDER:
                 Geary.Email? found = traverse<Email>(all)
-                    .first_matching((email) => is_in_current_folder(email.id));
-                
+                    .first_matching((email) => is_in_base_folder(email.id));
+
                 return found ?? traverse<Email>(all).first();
-            
+
             case Location.OUT_OF_FOLDER_IN_FOLDER:
                 Geary.Email? found = traverse<Email>(all)
-                    .first_matching((email) => !is_in_current_folder(email.id));
-                
+                    .first_matching((email) => !is_in_base_folder(email.id));
+
                 return found ?? traverse<Email>(all).first();
-            
+
             default:
                 assert_not_reached();
         }
     }
-    
+
     private bool check_flag(Geary.NamedFlag flag, bool contains) {
         foreach (Geary.Email email in get_emails(Ordering.NONE)) {
             if (email.email_flags != null && email.email_flags.contains(flag) == contains)
                 return true;
         }
-        
+
         return false;
     }
 
@@ -408,7 +415,4 @@ public class Geary.App.Conversation : BaseObject {
         return check_flag(flag, false);
     }
 
-    public string to_string() {
-        return "[#%d] (%d emails)".printf(convnum, emails.size);
-    }
 }
diff --git a/src/engine/app/conversation-monitor/app-conversation-set.vala 
b/src/engine/app/conversation-monitor/app-conversation-set.vala
index 10a0269..624cd06 100644
--- a/src/engine/app/conversation-monitor/app-conversation-set.vala
+++ b/src/engine/app/conversation-monitor/app-conversation-set.vala
@@ -1,46 +1,55 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2017 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
+/**
+ * Creates and maintains set of conversations by adding and removing email.
+ */
 private class Geary.App.ConversationSet : BaseObject {
+
+    /** Determines the number of conversations in the set. */
+    public int size { get { return _conversations.size; } }
+
+    /** Determines the set contains no conversations.  */
+    public bool is_empty { get { return _conversations.is_empty; } }
+
+    /** Returns a read-only view of conversations in the set.  */
+    public Gee.Collection<Conversation> conversations {
+        owned get { return _conversations.read_only_view; }
+    }
+
     private Gee.Set<Conversation> _conversations = new Gee.HashSet<Conversation>();
-    
+
     // Maps email ids to conversations.
     private Gee.HashMap<Geary.EmailIdentifier, Conversation> email_id_map
         = new Gee.HashMap<Geary.EmailIdentifier, Conversation>();
-    
+
     // Contains the full set of Message IDs theoretically in each conversation,
     // as determined by the ancestors of all messages in the conversation.
     private Gee.HashMap<Geary.RFC822.MessageID, Conversation> logical_message_id_map
         = new Gee.HashMap<Geary.RFC822.MessageID, Conversation>();
-    
-    public int size { get { return _conversations.size; } }
-    public bool is_empty { get { return _conversations.is_empty; } }
-    public Gee.Collection<Conversation> conversations {
-        owned get { return _conversations.read_only_view; }
-    }
-    
-    public ConversationSet() {
-    }
-    
+
+
     public int get_email_count() {
         return email_id_map.size;
     }
-    
+
     public Gee.Collection<Geary.EmailIdentifier> get_email_identifiers() {
         return email_id_map.keys;
     }
-    
+
     public bool contains(Conversation conversation) {
         return _conversations.contains(conversation);
     }
-    
+
     public bool has_email_identifier(Geary.EmailIdentifier id) {
         return email_id_map.has_key(id);
     }
-    
+
     /**
      * Return whether the set has the given Message ID.  If logical_set,
      * there's no requirement that any conversation actually contain a message
@@ -50,12 +59,146 @@ private class Geary.App.ConversationSet : BaseObject {
     public bool has_message_id(Geary.RFC822.MessageID message_id) {
         return logical_message_id_map.has_key(message_id);
     }
-    
+
     public Conversation? get_by_email_identifier(Geary.EmailIdentifier id) {
         return email_id_map.get(id);
     }
 
     /**
+     * Adds a collection of emails to conversations in this set.
+     *
+     * This method will create and/or merge conversations as
+     * needed. The collection `emails` contains the messages to be
+     * added, and for each email in the collection, there should be an
+     * entry in `id_to_paths` that indicates the folders each message
+     * is known to belong to. The folder `base_folder` is the base
+     * folder for the conversation monitor that owns this set.
+     *
+     * The three collections returned include any conversation that
+     * were created, any that had email appended to them (and the
+     * messages that were appended), and any that were removed due to
+     * being merged into another.
+     */
+    public void add_all_emails(Gee.Collection<Email> emails,
+                               Gee.MultiMap<EmailIdentifier, FolderPath>? id_to_paths,
+                               Folder base_folder,
+                               out Gee.Collection<Conversation> added,
+                               out Gee.MultiMap<Conversation, Email> appended,
+                               out Gee.Collection<Conversation> removed_due_to_merge) {
+        Gee.HashSet<Conversation> _added =
+            new Gee.HashSet<Conversation>();
+        Gee.HashMultiMap<Conversation, Geary.Email> _appended =
+            new Gee.HashMultiMap<Conversation, Geary.Email>();
+        Gee.HashSet<Conversation> _removed_due_to_merge =
+            new Gee.HashSet<Conversation>();
+
+        foreach (Geary.Email email in emails) {
+            Gee.Set<Conversation> associated = get_associated_conversations(email);
+            if (associated.size > 1) {
+                // When multiple conversations hold one or more of the Message-IDs in the email's
+                // ancestry, it means a prior email processed here didn't properly list their entire
+                // In-Reply-To or References and a split in the conversation appeared ...
+                // ConversationSet *requires* each Message-ID is associated with one and only one
+                // Conversation
+                //
+                // By doing this first, it prevents ConversationSet getting itself into a bad state
+                // where more than one Conversation thinks it "owns" a Message-ID
+                debug("Merging %d conversations due new email associating with all...", associated.size);
+
+                // Note that this call will modify the List so it only holds the to-be-axed
+                // Conversations
+                Gee.Set<Geary.Email> moved_email = new Gee.HashSet<Geary.Email>();
+                Conversation dest = merge_conversations(
+                    associated, moved_email
+                );
+                assert(!associated.contains(dest));
+
+                // remove the remaining conversations from the added/appended Collections
+                _added.remove_all(associated);
+                foreach (Conversation removed_conversation in associated)
+                    _appended.remove_all(removed_conversation);
+
+                // but notify caller they were merged away
+                _removed_due_to_merge.add_all(associated);
+
+                // the dest was always appended to, never created
+                if (!_added.contains(dest)) {
+                    foreach (Geary.Email moved in moved_email)
+                        _appended.set(dest, moved);
+                }
+
+                // Nasty ol' Email won't cause problems now -- but let's check anyway!
+                assert(get_associated_conversations(email).size <= 1);
+            }
+
+            bool added_conversation;
+            Conversation? conversation = add_email(
+                email,
+                base_folder,
+                (id_to_paths != null) ? id_to_paths.get(email.id) : null,
+                out added_conversation
+            );
+
+            if (conversation == null)
+                continue;
+
+            if (added_conversation) {
+                _added.add(conversation);
+            } else {
+                if (!_added.contains(conversation))
+                    _appended.set(conversation, email);
+            }
+        }
+
+        added = _added;
+        appended = _appended;
+        removed_due_to_merge = _removed_due_to_merge;
+    }
+
+    /**
+     * Removes a number of emails from conversations in this set.
+     *
+     * This method will remove and/or trim conversations as
+     * needed. The collection `emails_ids` contains the identifiers
+     * of emails to be removed.
+     *
+     * The returned collections include any conversations that were
+     * removed (if all of their emails were removed), and any that
+     * were trimmed and the emails that were trimmed from it,
+     * respectively.
+     */
+    public void remove_all_emails_by_identifier(FolderPath source_path,
+                                                Gee.Collection<Geary.EmailIdentifier> ids,
+                                                out Gee.Collection<Conversation> removed,
+                                                out Gee.MultiMap<Conversation, Geary.Email> trimmed) {
+        Gee.HashSet<Conversation> _removed = new Gee.HashSet<Conversation>();
+        Gee.HashMultiMap<Conversation, Geary.Email> _trimmed
+            = new Gee.HashMultiMap<Conversation, Geary.Email>();
+
+        foreach (Geary.EmailIdentifier id in ids) {
+            Geary.Email email;
+            bool removed_conversation;
+            Conversation? conversation = remove_email_by_identifier(
+                source_path, id, out email, out removed_conversation
+            );
+
+            if (conversation == null)
+                continue;
+
+            if (removed_conversation) {
+                if (_trimmed.contains(conversation))
+                    _trimmed.remove_all(conversation);
+                _removed.add(conversation);
+            } else if (!conversation.contains_email_by_id(id)) {
+                _trimmed.set(conversation, email);
+            }
+        }
+
+        removed = _removed;
+        trimmed = _trimmed;
+    }
+
+    /**
      * Removes a conversation from the set.
      */
     public void remove_conversation(Conversation conversation) {
@@ -76,7 +219,7 @@ private class Geary.App.ConversationSet : BaseObject {
                 .map_nonnull<Conversation>(a => logical_message_id_map.get(a))
                 .to_hash_set();
         }
-        
+
         return Gee.Set.empty<Conversation>();
     }
 
@@ -147,107 +290,10 @@ private class Geary.App.ConversationSet : BaseObject {
         }
     }
 
-    /**
-     * Adds a collection of emails to conversations in this set.
-     *
-     * This method will create and/or merge conversations as
-     * needed. The collection `emails` contains the messages to be
-     * added, and for each email in the collection, there should be an
-     * entry in `id_to_paths` that indicates the folders each message
-     * is known to belong to. The folder `base_folder` is the base
-     * folder for the conversation monitor that owns this set.
-     *
-     * The three collections returned include any conversation that
-     * were created, any that had email appended to them (and the
-     * messages that were appended), and any that were removed due to
-     * being merged into another.
-     */
-    public async void add_all_emails_async(
-        Gee.Collection<Geary.Email> emails,
-        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? id_to_paths,
-        Folder base_folder,
-        out Gee.Collection<Conversation> added,
-        out Gee.MultiMap<Conversation, Geary.Email> appended,
-        out Gee.Collection<Conversation> removed_due_to_merge,
-        Cancellable? cancellable)
-    throws Error {
-        Gee.HashSet<Conversation> _added =
-            new Gee.HashSet<Conversation>();
-        Gee.HashMultiMap<Conversation, Geary.Email> _appended =
-            new Gee.HashMultiMap<Conversation, Geary.Email>();
-        Gee.HashSet<Conversation> _removed_due_to_merge =
-            new Gee.HashSet<Conversation>();
-
-        foreach (Geary.Email email in emails) {
-            Gee.Set<Conversation> associated = get_associated_conversations(email);
-            if (associated.size > 1) {
-                // When multiple conversations hold one or more of the Message-IDs in the email's
-                // ancestry, it means a prior email processed here didn't properly list their entire
-                // In-Reply-To or References and a split in the conversation appeared ...
-                // ConversationSet *requires* each Message-ID is associated with one and only one
-                // Conversation
-                //
-                // By doing this first, it prevents ConversationSet getting itself into a bad state
-                // where more than one Conversation thinks it "owns" a Message-ID
-                debug("Merging %d conversations due new email associating with all...", associated.size);
-
-                // Note that this call will modify the List so it only holds the to-be-axed
-                // Conversations
-                Gee.Set<Geary.Email> moved_email = new Gee.HashSet<Geary.Email>();
-                Conversation dest = yield merge_conversations_async(
-                    associated, moved_email, cancellable
-                );
-                assert(!associated.contains(dest));
-
-                // remove the remaining conversations from the added/appended Collections
-                _added.remove_all(associated);
-                foreach (Conversation removed_conversation in associated)
-                    _appended.remove_all(removed_conversation);
-                
-                // but notify caller they were merged away
-                _removed_due_to_merge.add_all(associated);
-                
-                // the dest was always appended to, never created
-                if (!_added.contains(dest)) {
-                    foreach (Geary.Email moved in moved_email)
-                        _appended.set(dest, moved);
-                }
-                
-                // Nasty ol' Email won't cause problems now -- but let's check anyway!
-                assert(get_associated_conversations(email).size <= 1);
-            }
-
-            bool added_conversation;
-            Conversation? conversation = add_email(
-                email,
-                base_folder,
-                (id_to_paths != null) ? id_to_paths.get(email.id) : null,
-                out added_conversation
-            );
-
-            if (conversation == null)
-                continue;
-            
-            if (added_conversation) {
-                _added.add(conversation);
-            } else {
-                if (!_added.contains(conversation))
-                    _appended.set(conversation, email);
-            }
-        }
-
-        added = _added;
-        appended = _appended;
-        removed_due_to_merge = _removed_due_to_merge;
-    }
-
     // This method will remove the destination (merged) Conversation from the List and return it
     // as the result, along with a Collection of email that must be merged into it
-    private async Conversation
-        merge_conversations_async(Gee.Set<Conversation> conversations,
-                                  Gee.Set<Geary.Email> moved_email,
-                                  Cancellable? cancellable)
-        throws Error {
+    private Conversation merge_conversations(Gee.Set<Conversation> conversations,
+                                             Gee.Set<Email> moved_email) {
         assert(conversations.size > 0);
 
         // find the largest conversation and merge the others into it
@@ -292,7 +338,7 @@ private class Geary.App.ConversationSet : BaseObject {
         // it would indicate a nasty error in our logic that we need to fix.
         if (!email_id_map.unset(email.id))
             error("Email %s already removed from conversation set", email.id.to_string());
-        
+
         Gee.Set<Geary.RFC822.MessageID>? removed_message_ids = conversation.remove(email);
         if (removed_message_ids != null) {
             foreach (Geary.RFC822.MessageID removed_message_id in removed_message_ids) {
@@ -353,47 +399,4 @@ private class Geary.App.ConversationSet : BaseObject {
         return conversation;
     }
 
-    /**
-     * Removes a number of emails from conversations in this set.
-     *
-     * This method will remove and/or trim conversations as
-     * needed. The collection `emails_ids` contains the identifiers
-     * of emails to be removed.
-     *
-     * The returned collections include any conversations that were
-     * removed (if all of their emails were removed), and any that
-     * were trimmed and the emails that were trimmed from it,
-     * respectively.
-     */
-    public void remove_all_emails_by_identifier(FolderPath source_path,
-                                                Gee.Collection<Geary.EmailIdentifier> ids,
-                                                out Gee.Collection<Conversation> removed,
-                                                out Gee.MultiMap<Conversation, Geary.Email> trimmed) {
-        Gee.HashSet<Conversation> _removed = new Gee.HashSet<Conversation>();
-        Gee.HashMultiMap<Conversation, Geary.Email> _trimmed
-            = new Gee.HashMultiMap<Conversation, Geary.Email>();
-
-        foreach (Geary.EmailIdentifier id in ids) {
-            Geary.Email email;
-            bool removed_conversation;
-            Conversation? conversation = remove_email_by_identifier(
-                source_path, id, out email, out removed_conversation
-            );
-
-            if (conversation == null)
-                continue;
-
-            if (removed_conversation) {
-                if (_trimmed.contains(conversation))
-                    _trimmed.remove_all(conversation);
-                _removed.add(conversation);
-            } else if (!conversation.contains_email_by_id(id)) {
-                _trimmed.set(conversation, email);
-            }
-        }
-
-        removed = _removed;
-        trimmed = _trimmed;
-    }
-
 }
diff --git a/test/engine/app/app-conversation-set-test.vala b/test/engine/app/app-conversation-set-test.vala
index 9a7324c..a2dc8c9 100644
--- a/test/engine/app/app-conversation-set-test.vala
+++ b/test/engine/app/app-conversation-set-test.vala
@@ -40,9 +40,9 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Email e1 = new Email(new MockEmailIdentifer(1));
         Email e2 = new Email(new MockEmailIdentifer(2));
 
-        Gee.LinkedList<Email> email = new Gee.LinkedList<Email>();
-        email.add(e1);
-        email.add(e2);
+        Gee.LinkedList<Email> emails = new Gee.LinkedList<Email>();
+        emails.add(e1);
+        emails.add(e2);
 
         Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath> email_paths =
             new Gee.HashMultiMap<Geary.EmailIdentifier, Geary.FolderPath>();
@@ -53,20 +53,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
 
-        this.test.add_all_emails_async.begin(
-            email,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 2);
         assert(this.test.get_email_count() == 2);
@@ -96,8 +86,9 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
     public void add_all_duplicate() {
         Email e1 = setup_email(1);
 
-        Gee.LinkedList<Email> email = new Gee.LinkedList<Email>();
-        email.add(e1);
+        Gee.LinkedList<Email> emails = new Gee.LinkedList<Email>();
+        emails.add(e1);
+        emails.add(e1);
 
         Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath> email_paths =
             new Gee.HashMultiMap<Geary.EmailIdentifier, Geary.FolderPath>();
@@ -108,20 +99,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            email,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 1);
@@ -137,20 +118,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         added = null;
         appended = null;
         removed = null;
-        this.test.add_all_emails_async.begin(
-            email,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 1);
@@ -178,21 +149,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 2);
@@ -219,20 +179,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         added = null;
         appended = null;
         removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 3);
@@ -269,20 +219,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 2);
@@ -322,20 +262,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 3);
@@ -369,26 +299,16 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 1);
 
         Conversation convo = this.test.get_by_email_identifier(e1.id);
-        assert(convo.is_in_current_folder(e1.id) == true);
+        assert(convo.is_in_base_folder(e1.id) == true);
         assert(convo.get_folder_count(e1.id) == 2);
     }
 
@@ -408,20 +328,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
 
         assert(this.test.size == 1);
         assert(this.test.get_email_count() == 1);
@@ -431,7 +341,7 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         assert(removed.is_empty);
 
         Conversation convo = this.test.get_by_email_identifier(e1.id);
-        assert(convo.is_in_current_folder(e1.id) == true);
+        assert(convo.is_in_base_folder(e1.id) == true);
         assert(convo.get_folder_count(e1.id) == 2);
     }
 
@@ -518,7 +428,7 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         assert(trimmed != null);
         assert(trimmed.size == 0);
 
-        assert(convo.is_in_current_folder(e1.id) == true);
+        assert(convo.is_in_base_folder(e1.id) == true);
         assert(convo.get_folder_count(e1.id) == 1);
     }
 
@@ -553,20 +463,10 @@ class Geary.App.ConversationSetTest : Gee.TestCase {
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
         Gee.Collection<Conversation>? removed = null;
-        this.test.add_all_emails_async.begin(
-            emails,
-            email_paths,
-            this.base_folder,
-            null,
-            (obj, ret) => { async_complete(ret); }
+        this.test.add_all_emails(
+            emails, email_paths, this.base_folder,
+            out added, out appended, out removed
         );
-        try {
-            this.test.add_all_emails_async.end(
-                async_result(), out added, out appended, out removed
-            );
-        } catch (Error error) {
-            assert_not_reached();
-        }
     }
 
 }
diff --git a/test/engine/app/app-conversation-test.vala b/test/engine/app/app-conversation-test.vala
index c189705..453ebeb 100644
--- a/test/engine/app/app-conversation-test.vala
+++ b/test/engine/app/app-conversation-test.vala
@@ -40,13 +40,13 @@ class Geary.App.ConversationTest : Gee.TestCase {
             });
 
         assert(this.test.add(e1, singleton(this.base_folder.path)) == true);
-        assert(this.test.is_in_current_folder(e1.id) == true);
+        assert(this.test.is_in_base_folder(e1.id) == true);
         assert(this.test.get_folder_count(e1.id) == 1);
         assert(appended == 1);
         assert(this.test.get_count() == 1);
 
         assert(this.test.add(e2, singleton(this.base_folder.path)) == true);
-        assert(this.test.is_in_current_folder(e2.id) == true);
+        assert(this.test.is_in_base_folder(e2.id) == true);
         assert(this.test.get_folder_count(e2.id) == 1);
         assert(appended == 2);
         assert(this.test.get_count() == 2);
@@ -80,14 +80,14 @@ class Geary.App.ConversationTest : Gee.TestCase {
         other_paths.add(other_path);
 
         assert(this.test.add(e1, other_paths) == false);
-        assert(this.test.is_in_current_folder(e1.id) == true);
+        assert(this.test.is_in_base_folder(e1.id) == true);
         assert(this.test.get_folder_count(e1.id) == 2);
 
-        assert(this.test.is_in_current_folder(e2.id) == true);
+        assert(this.test.is_in_base_folder(e2.id) == true);
         assert(this.test.get_folder_count(e2.id) == 1);
 
         this.test.remove_path(e1.id, other_path);
-        assert(this.test.is_in_current_folder(e1.id) == true);
+        assert(this.test.is_in_base_folder(e1.id) == true);
         assert(this.test.get_folder_count(e1.id) == 1);
     }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]