[geary/wip/713150-conversations: 3/3] Further along, seeing speed improvements already



commit 7b8bd13d46d3a42b57ef3386c3174eabe894024b
Author: Jim Nelson <jim yorba org>
Date:   Thu Feb 19 18:41:37 2015 -0800

    Further along, seeing speed improvements already
    
    Plenty of work to be done in ConversationMonitor

 src/CMakeLists.txt                                 |    1 +
 src/engine/api/geary-account.vala                  |    4 +-
 src/engine/api/geary-associated-emails.vala        |   41 +++++
 src/engine/app/app-conversation-monitor.vala       |  185 +++++++++++++------
 src/engine/imap-db/imap-db-account.vala            |  129 +++++++++-----
 .../imap-engine/imap-engine-generic-account.vala   |    6 +-
 6 files changed, 259 insertions(+), 107 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 35984d6..5ba1bc3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -7,6 +7,7 @@ engine/api/geary-abstract-local-folder.vala
 engine/api/geary-account.vala
 engine/api/geary-account-information.vala
 engine/api/geary-aggregated-folder-properties.vala
+engine/api/geary-associated-emails.vala
 engine/api/geary-attachment.vala
 engine/api/geary-base-object.vala
 engine/api/geary-composed-email.vala
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 14031d6..997b579 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -344,8 +344,8 @@ public abstract class Geary.Account : BaseObject {
      * The particulars of the folder_blacklist and flag_blacklist parameters are the same as in
      * local_search_message_id_async.
      */
-    public abstract async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_list_conversation_async(
-        Geary.EmailIdentifier email_id, Geary.Email.Field requested_fields, bool partial_ok,
+    public abstract async Gee.Collection<Geary.AssociatedEmails>? local_search_associated_emails_async(
+        Gee.Set<Geary.EmailIdentifier> email_ids, Geary.Email.Field requested_fields, bool partial_ok,
         Gee.Collection<Geary.FolderPath?> folder_blacklist, Geary.EmailFlags? flag_blacklist,
         Cancellable? cancellable = null) throws Error;
     
diff --git a/src/engine/api/geary-associated-emails.vala b/src/engine/api/geary-associated-emails.vala
new file mode 100644
index 0000000..c51f74a
--- /dev/null
+++ b/src/engine/api/geary-associated-emails.vala
@@ -0,0 +1,41 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * An immutable representation of all known local { link Email}s which are associated with one
+ * another due to their Message-ID, In-Reply-To, and References headers.
+ *
+ * This object is free-form and does not impose any ordering or threading on the emails.  It is
+ * also not updated as new email arrives and email is removed.
+ *
+ * @see Account.local_search_associated_emails_async
+ */
+
+public class Geary.AssociatedEmails : BaseObject {
+    /**
+     * All associated { link Email}s.
+     */
+    public Gee.Collection<Geary.Email> emails { get; private set; }
+    
+    /**
+     * All known { link FolderPath}s for each { link Email}.
+     *
+     * null if the Email is currently associated with no { link Folder}s.
+     */
+    public Gee.MultiMap<Geary.Email, Geary.FolderPath?> known_paths { get; private set; }
+    
+    public AssociatedEmails() {
+        emails = new Gee.ArrayList<Email>();
+        known_paths = new Gee.HashMultiMap<Email, FolderPath?>();
+    }
+    
+    public void add(Geary.Email email, Gee.Collection<Geary.FolderPath?> paths) {
+        emails.add(email);
+        foreach (FolderPath path in paths)
+            known_paths.set(email, path);
+    }
+}
+
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index 567ee26..efbbae6 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -12,21 +12,9 @@ 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; }
     public bool is_monitoring { get; private set; default = false; }
     public int min_window_count { get { return _min_window_count; }
@@ -38,7 +26,6 @@ public class Geary.App.ConversationMonitor : BaseObject {
     
     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;
     private Cancellable? cancellable_monitor = null;
@@ -46,6 +33,19 @@ public class Geary.App.ConversationMonitor : BaseObject {
     private int _min_window_count = 0;
     private ConversationOperationQueue operation_queue = new ConversationOperationQueue();
     
+    // All generated Conversations
+    private Gee.HashSet<Conversation> conversations = new Gee.HashSet<Conversation>();
+    
+    // A logical map of Message-ID to Conversation ... these Message-IDs are merely referenced by
+    // emails and the email itself may not be present in the conversation
+    private Gee.HashMap<RFC822.MessageID, Conversation> message_id_to_conversation =
+        new Gee.HashMap<RFC822.MessageID, Conversation>();
+    
+    // A map of EmailIdentifiers to Conversations ... unlike Message-IDs, these are known emails
+    // loaded into the conversations
+    private Gee.HashMap<EmailIdentifier, Conversation> email_id_to_conversation =
+        new Gee.HashMap<EmailIdentifier, Conversation>();
+    
     /**
      * "monitoring-started" is fired when the Conversations folder has been opened for monitoring.
      */
@@ -184,9 +184,6 @@ public class Geary.App.ConversationMonitor : BaseObject {
     ~ConversationMonitor() {
         if (is_monitoring)
             debug("Warning: Conversations object destroyed without stopping monitoring");
-        
-        // Manually detach all the weak refs in the Conversation objects
-        conversations.clear_owners();
     }
     
     protected virtual void notify_monitoring_started() {
@@ -239,12 +236,16 @@ public class Geary.App.ConversationMonitor : BaseObject {
         return conversations.size;
     }
     
+    public int get_email_count() {
+        return email_id_to_conversation.size;
+    }
+    
     public Gee.Collection<Conversation> get_conversations() {
-        return conversations.conversations;
+        return conversations.read_only_view;
     }
     
     public Geary.App.Conversation? get_conversation_for_email(Geary.EmailIdentifier email_id) {
-        return conversations.get_by_email_identifier(email_id);
+        return email_id_to_conversation[email_id];
     }
     
     public async bool start_monitoring_async(Cancellable? cancellable = null)
@@ -280,7 +281,6 @@ public class Geary.App.ConversationMonitor : BaseObject {
         folder.opened.connect(on_folder_opened);
         folder.account.email_flags_changed.connect(on_account_email_flags_changed);
         folder.account.email_locally_complete.connect(on_account_email_locally_complete);
-        // TODO: handle removed email
         
         try {
             yield folder.open_async(open_flags, cancellable);
@@ -356,32 +356,31 @@ public class Geary.App.ConversationMonitor : BaseObject {
             throw close_err;
     }
     
-    /**
-     * 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
-     * Conversation objects.
-     */
     private async void load_by_id_async(Geary.EmailIdentifier? initial_id, int count,
         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));
+                count, Email.Field.NONE, flags, cancellable));
         } catch (Error err) {
-            list_error(err);
+            notify_scan_error(err);
+            
             throw err;
+        } finally {
+            notify_scan_completed();
         }
     }
     
     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));
+                Email.Field.NONE, flags, cancellable));
         } catch (Error err) {
-            list_error(err);
+            notify_scan_error(err);
+        } finally {
+            notify_scan_completed();
         }
     }
     
@@ -405,7 +404,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
             foreach (Geary.Email email in emails) {
                 Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
                 if (ancestors != null &&
-                    Geary.traverse<RFC822.MessageID>(ancestors).any(id => conversations.has_message_id(id)))
+                    Geary.traverse<RFC822.MessageID>(ancestors).any(id => 
message_id_to_conversation.contains(id)))
                     relevant_ids.add(email.id);
             }
             
@@ -432,7 +431,8 @@ public class Geary.App.ConversationMonitor : BaseObject {
             
             debug("Fetched %d relevant emails locally", search_emails.size);
             
-            yield process_email_async(search_emails, new ProcessJobContext(false));
+            // TODO: Only need id's for this
+            yield process_email_async(search_emails);
         } catch (Error e) {
             debug("Error loading external emails: %s", e.message);
             if (opened) {
@@ -445,43 +445,102 @@ 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);
-        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);
+    // Emails for this call only require Email.Field.NONE, as the EmailIdentifier is then used to
+    // load the associations and the required_fields
+    private async void process_email_async(Gee.Collection<Geary.Email>? emails) {
+        if (emails == null || emails.size == 0)
             return;
-        }
         
         Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email: %d emails",
             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.HashSet<EmailIdentifier> email_ids = traverse<Email>(emails)
+            .map_nonnull<EmailIdentifier>(email => email.id)
+            .to_hash_set();
+        
+        Gee.Collection<AssociatedEmails> associated;
+        try {
+            associated = yield folder.account.local_search_associated_emails_async(
+                email_ids, required_fields, false, get_search_blacklist(), get_search_flag_blacklist(),
+                null);
+        } catch (Error err) {
+            debug("Unable to search for associated emails: %s", err.message);
             
-                Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
-                if (ancestors != null) {
-                    Geary.traverse<RFC822.MessageID>(ancestors)
-                        .filter(id => !new_message_ids.contains(id))
-                        .add_all_to(new_message_ids);
-                }
+            return;
+        }
+        
+        Gee.HashSet<Conversation> added = new Gee.HashSet<Conversation>();
+        Gee.HashMultiMap<Conversation, Email> appended = new Gee.HashMultiMap<Conversation, Email>();
+        
+        foreach (AssociatedEmails association in associated) {
+            // Get all ancestors for the associated emails
+            Gee.HashSet<RFC822.MessageID> ancestors = new Gee.HashSet<RFC822.MessageID>();
+            foreach (Email email in association.emails)
+                ancestors.add_all(email.get_ancestors());
+            
+            // get all conversations for these emails (possible for multiple conversations to be
+            // started and then coalesce as new emails come in)
+            Gee.HashSet<Conversation> existing = new Gee.HashSet<Conversation>();
+            foreach (RFC822.MessageID ancestor in ancestors) {
+                Conversation? conversation = message_id_to_conversation[ancestor];
+                if (conversation != null)
+                    existing.add(conversation);
+            }
+            
+            // Create or pick conversation and reporting collection for these emails
+            Conversation conversation;
+            switch (existing.size) {
+                case 0:
+                    conversation = new Conversation(this);
+                break;
+                
+                case 1:
+                    conversation = traverse<Conversation>(existing).first();
+                break;
+                
+                default:
+                    conversation = merge_conversations(existing);
+                break;
+            }
+            
+            // add all emails and each known path(s) to the Conversation and EmailIdentifier mapping
+            foreach (Email email in association.emails) {
+                conversation.add(email, association.known_paths[email]);
+                email_id_to_conversation[email.id] = conversation;
+            }
+            
+            // map all Message-IDs to this Conversation
+            foreach (RFC822.MessageID ancestor in ancestors)
+                message_id_to_conversation[ancestor] = conversation;
+            
+            // if new, added, otherwise appended
+            if (!conversations.contains(conversation)) {
+                added.add(conversation);
+                conversations.add(conversation);
+            } else {
+                foreach (Email email in association.emails)
+                    appended.set(conversation, email);
             }
         }
         
-        // 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);
+        if (added.size > 0)
+            notify_conversations_added(added);
+        
+        if (appended.size > 0) {
+            foreach (Conversation conversation in appended.get_keys())
+                notify_conversation_appended(conversation, appended.get(conversation));
+        }
         
         Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email completed: %d 
emails",
             folder.to_string(), emails.size);
     }
     
+    // TODO
+    private Conversation merge_conversations(Gee.Set<Conversation> conversations) {
+        breakpoint();
+        return new Conversation(this);
+    }
+    
     private Gee.Collection<Geary.FolderPath> get_search_blacklist() {
         Geary.SpecialFolderType[] blacklisted_folder_types = {
             Geary.SpecialFolderType.SPAM,
@@ -514,6 +573,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         return flags;
     }
     
+    /*
     private async void expand_conversations_async(Gee.Set<RFC822.MessageID> needed_message_ids,
         ProcessJobContext job) {
         if (needed_message_ids.size == 0) {
@@ -564,7 +624,9 @@ public class Geary.App.ConversationMonitor : BaseObject {
             "[%s] ConversationMonitor::expand_conversations completed: %d email ids (%d found)",
             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;
@@ -594,6 +656,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         if (job.inside_scan)
             notify_scan_completed();
     }
+    */
     
     private void on_folder_email_appended(Gee.Collection<Geary.EmailIdentifier> appended_ids) {
         operation_queue.add(new AppendOperation(this, appended_ids));
@@ -624,6 +687,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         debug("%d messages(s) removed from %s, trimming/removing conversations...", removed_ids.size,
             folder.to_string());
         
+        /*
         Gee.Collection<Geary.App.Conversation> removed;
         Gee.MultiMap<Geary.App.Conversation, Geary.Email> trimmed;
         yield conversations.remove_emails_and_check_in_folder_async(removed_ids, folder.account,
@@ -634,7 +698,9 @@ public class Geary.App.ConversationMonitor : BaseObject {
         
         foreach (Conversation conversation in removed)
             notify_conversation_removed(conversation);
+        */
         
+        /*
         // 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
@@ -644,6 +710,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         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,
@@ -663,7 +730,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
     private void on_account_email_flags_changed(Geary.Folder folder,
         Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map) {
         foreach (Geary.EmailIdentifier id in map.keys) {
-            Conversation? conversation = conversations.get_by_email_identifier(id);
+            Conversation? conversation = email_id_to_conversation[id];
             if (conversation == null)
                 continue;
             
@@ -679,8 +746,8 @@ public class Geary.App.ConversationMonitor : BaseObject {
     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(),
-                out earliest_id, null, cancellable);
+            yield folder.find_boundaries_async(email_id_to_conversation.keys, out earliest_id, null,
+                cancellable);
         } catch (Error e) {
             debug("Error finding earliest email identifier: %s", e.message);
         }
@@ -728,7 +795,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         if (!is_insert && min_window_count <= conversations.size)
             return;
         
-        int initial_message_count = conversations.get_email_count();
+        int initial_message_count = 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
@@ -773,7 +840,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         }
         
         // Run again to make sure we're full unless we ran out of messages.
-        if (conversations.get_email_count() != initial_message_count)
+        if (get_email_count() != initial_message_count)
             operation_queue.add(new FillWindowOperation(this, is_insert));
     }
 }
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index 3beee1f..0c55aba 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -659,8 +659,16 @@ private class Geary.ImapDB.Account : BaseObject {
             
             Db.Result result = stmt.exec(cancellable);
             while (!result.finished) {
+                Email? email;
+                Gee.Collection<FolderPath?>? known_paths;
                 do_fetch_message(cx, result.int64_at(0), requested_fields, partial_ok, folder_blacklist,
-                    flag_blacklist, messages, cancellable);
+                    flag_blacklist, out email, out known_paths, cancellable);
+                if (email != null) {
+                    assert(known_paths != null);
+                    
+                    foreach (FolderPath known_path in known_paths)
+                        messages.set(email, known_path);
+                }
                 
                 result.next(cancellable);
             }
@@ -671,91 +679,126 @@ private class Geary.ImapDB.Account : BaseObject {
         return (messages.size == 0 ? null : messages);
     }
     
-    public async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? list_conversation_async(
-        Geary.EmailIdentifier email_id, Email.Field requested_fields, bool partial_ok,
+    public async Gee.Collection<Geary.AssociatedEmails>? search_associated_emails_async(
+        Gee.Set<Geary.EmailIdentifier> email_ids, Email.Field requested_fields, bool partial_ok,
         Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? flag_blacklist,
         Cancellable? cancellable) throws Error {
         check_open();
         
-        ImapDB.EmailIdentifier? imapdb_id = email_id as ImapDB.EmailIdentifier;
-        if (imapdb_id == null)
-            throw new EngineError.BAD_PARAMETERS("Invalid identifier supplied to list conversation");
+        // Store in a casted HashSet that can be modified internally, as associated identifiers
+        // will be weeded out as loaded with other identifiers
+        Gee.HashSet<ImapDB.EmailIdentifier> db_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        foreach (Geary.EmailIdentifier email_id in email_ids) {
+            ImapDB.EmailIdentifier? db_id = email_id as ImapDB.EmailIdentifier;
+            if (db_id == null)
+                throw new EngineError.BAD_PARAMETERS("Invalid identifier supplied to list conversation");
+            
+            db_ids.add(db_id);
+        }
         
-        Gee.HashMultiMap<Geary.Email, Geary.FolderPath?> messages
-            = new Gee.HashMultiMap<Geary.Email, Geary.FolderPath?>();
+        Gee.Collection<AssociatedEmails> associations = new Gee.ArrayList<AssociatedEmails>();
+        Gee.HashSet<ImapDB.EmailIdentifier> found_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
         
         if (flag_blacklist != null)
             requested_fields = requested_fields | Geary.Email.Field.FLAGS;
         
+        debug("Searching %d ids for associations...", email_ids.size);
         yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
-            Db.Statement stmt = cx.prepare("""
-                SELECT conversation_id
-                FROM MessageTable
-                WHERE id = ?
-            """);
-            stmt.bind_rowid(0, imapdb_id.message_id);
-            
-            Db.Result result = stmt.exec(cancellable);
-            if (result.finished || result.is_null_at(0))
-                return Db.TransactionOutcome.DONE;
-            
-            int64 conversation_id = result.rowid_at(0);
-            
-            stmt = cx.prepare("""
-                SELECT id
-                FROM MessageTable
-                WHERE conversation_id = ?
-            """);
-            stmt.bind_rowid(0, conversation_id);
-            
-            result = stmt.exec(cancellable);
-            while (!result.finished) {
-                do_fetch_message(cx, result.int64_at(0), requested_fields, partial_ok, folder_blacklist,
-                    flag_blacklist, messages, cancellable);
+            foreach (ImapDB.EmailIdentifier db_id in db_ids) {
+                if (found_ids.contains(db_id))
+                    continue;
                 
-                result.next(cancellable);
+                Db.Statement stmt = cx.prepare("""
+                    SELECT conversation_id
+                    FROM MessageTable
+                    WHERE id = ?
+                """);
+                stmt.bind_rowid(0, db_id.message_id);
+                
+                Db.Result result = stmt.exec(cancellable);
+                if (result.finished || result.is_null_at(0))
+                    continue;
+                
+                int64 conversation_id = result.rowid_at(0);
+                
+                stmt = cx.prepare("""
+                    SELECT id
+                    FROM MessageTable
+                    WHERE conversation_id = ?
+                """);
+                stmt.bind_rowid(0, conversation_id);
+                
+                AssociatedEmails association = new AssociatedEmails();
+                
+                result = stmt.exec(cancellable);
+                while (!result.finished) {
+                    Email? email;
+                    Gee.Collection<FolderPath?>? known_paths;
+                    do_fetch_message(cx, result.int64_at(0), requested_fields, partial_ok, folder_blacklist,
+                        flag_blacklist, out email, out known_paths, cancellable);
+                    if (email != null) {
+                        association.add(email, known_paths);
+                        found_ids.add((ImapDB.EmailIdentifier) email.id);
+                    }
+                    
+                    result.next(cancellable);
+                }
+                
+                associations.add(association);
             }
             
             return Db.TransactionOutcome.DONE;
         }, cancellable);
+        debug("Found %d associations from %d ids", associations.size, email_ids.size);
         
-        return messages.size > 0 ? messages : null;
+        return associations.size > 0 ? associations : null;
     }
     
     private void do_fetch_message(Db.Connection cx, int64 message_id, Email.Field required_fields,
         bool partial_ok, Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? 
flag_blacklist,
-        Gee.HashMultiMap<Geary.Email, Geary.FolderPath?> messages, Cancellable? cancellable) throws Error {
+        out Email? email, out Gee.Collection<FolderPath?>? known_paths, Cancellable? cancellable) throws 
Error {
         Email.Field actual_fields;
         MessageRow row = ImapDB.Folder.do_fetch_message_row(cx, message_id, required_fields,
             out actual_fields, cancellable);
         
+        // prepare for the worst
+        email = null;
+        known_paths = null;
+        
         // Ignore any messages that don't have the required fields unless partial is ok
         if (!partial_ok && !row.fields.fulfills(required_fields))
             return;
         
-        Email email = row.to_email(new Geary.ImapDB.EmailIdentifier(message_id, null));
+        email = row.to_email(new Geary.ImapDB.EmailIdentifier(message_id, null));
         ImapDB.Folder.do_add_attachments(cx, email, message_id, cancellable);
         
+        // Check for blacklisted flags.
+        if (flag_blacklist != null && email.email_flags != null && 
email.email_flags.contains_any(flag_blacklist)) {
+            email = null;
+            
+            return;
+        }
+        
+        known_paths = new Gee.HashSet<FolderPath?>();
+        
         // Add folders email is found in, respecting blacklist
         Gee.Set<Geary.FolderPath>? folders = do_find_email_folders(cx, message_id, true, cancellable);
         if (folders == null) {
             if (folder_blacklist == null || !folder_blacklist.contains(null))
-                messages.set(email, null);
+                known_paths.add(null);
         } else {
             foreach (Geary.FolderPath path in folders) {
                 // If it's in a blacklisted folder, we don't report it at all.
                 if (folder_blacklist != null && folder_blacklist.contains(path)) {
-                    messages.remove_all(email);
+                    email = null;
+                    known_paths = null;
+                    
                     break;
                 } else {
-                    messages.set(email, path);
+                    known_paths.add(path);
                 }
             }
         }
-        
-        // Check for blacklisted flags.
-        if (flag_blacklist != null && email.email_flags != null && 
email.email_flags.contains_any(flag_blacklist))
-            messages.remove_all(email);
     }
     
     private string? extract_field_from_token(string[] parts, ref string token) {
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index fa029e0..8a3d3ea 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -859,11 +859,11 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
             message_id, requested_fields, partial_ok, folder_blacklist, flag_blacklist, cancellable);
     }
     
-    public override async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_list_conversation_async(
-        Geary.EmailIdentifier email_id, Geary.Email.Field requested_fields, bool partial_ok,
+    public override async Gee.Collection<Geary.AssociatedEmails>? local_search_associated_emails_async(
+        Gee.Set<Geary.EmailIdentifier> email_ids, Geary.Email.Field requested_fields, bool partial_ok,
         Gee.Collection<Geary.FolderPath?> folder_blacklist, Geary.EmailFlags? flag_blacklist,
         Cancellable? cancellable = null) throws Error {
-        return yield local.list_conversation_async(email_id, requested_fields, partial_ok,
+        return yield local.search_associated_emails_async(email_ids, requested_fields, partial_ok,
             folder_blacklist, flag_blacklist, cancellable);
     }
     


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