[geary/mjog/search-refinement: 5/14] Clean up search folder implementation



commit 924104c282af80161ff2b9dd683481ed2a722eb2
Author: Michael Gratton <mike vee net>
Date:   Tue Dec 10 20:51:20 2019 +1100

    Clean up search folder implementation
    
    Move SearchFolder and search EmailIdentifier implementation out of
    ImapDb and into its own package. Decouple both from ImapDB, and improve
    the implementation, fixing a few inefficiencies. Merge search
    FolderProperties into the SearchFoldern implementation as an inner
    class.
    
    Merge SearchTerm into ImapDB.SearchQuery as an inner class and move the
    outer class's source down a level, since it was the only file left in
    the imap-db/search dir.

 po/POTFILES.in                                     |   8 +-
 src/engine/imap-db/imap-db-account.vala            |  12 +-
 .../imap-db/{search => }/imap-db-search-query.vala |  89 +++-
 .../search/imap-db-search-email-identifier.vala    |  75 ---
 .../search/imap-db-search-folder-properties.vala   |  16 -
 .../imap-db/search/imap-db-search-folder.vala      | 428 ----------------
 src/engine/imap-db/search/imap-db-search-term.vala |  62 ---
 .../gmail/imap-engine-gmail-search-folder.vala     |   2 +-
 .../imap-engine/imap-engine-generic-account.vala   |   4 +-
 src/engine/meson.build                             |   9 +-
 src/engine/search/search-email-identifier.vala     | 129 +++++
 src/engine/search/search-folder-impl.vala          | 546 +++++++++++++++++++++
 .../imap-engine-generic-account-test.vala          |  15 +
 13 files changed, 778 insertions(+), 617 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5ec9b69e..71229fb7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -229,11 +229,7 @@ src/engine/imap-db/imap-db-email-identifier.vala
 src/engine/imap-db/imap-db-folder.vala
 src/engine/imap-db/imap-db-gc.vala
 src/engine/imap-db/imap-db-message-row.vala
-src/engine/imap-db/search/imap-db-search-email-identifier.vala
-src/engine/imap-db/search/imap-db-search-folder-properties.vala
-src/engine/imap-db/search/imap-db-search-folder.vala
-src/engine/imap-db/search/imap-db-search-query.vala
-src/engine/imap-db/search/imap-db-search-term.vala
+src/engine/imap-db/imap-db-search-query.vala
 src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
 src/engine/imap-engine/gmail/imap-engine-gmail-all-mail-folder.vala
 src/engine/imap-engine/gmail/imap-engine-gmail-drafts-folder.vala
@@ -365,6 +361,8 @@ src/engine/rfc822/rfc822-message.vala
 src/engine/rfc822/rfc822-part.vala
 src/engine/rfc822/rfc822-utils.vala
 src/engine/rfc822/rfc822.vala
+src/engine/search/search-email-identifier.vala
+src/engine/search/search-folder-impl.vala
 src/engine/smtp/smtp-authenticator.vala
 src/engine/smtp/smtp-capabilities.vala
 src/engine/smtp/smtp-client-connection.vala
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index 59c60125..6540494a 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -575,7 +575,7 @@ private class Geary.ImapDB.Account : BaseObject {
 
         foreach (string? field in query.get_fields()) {
             debug(" - Field \"%s\" terms:", field);
-            foreach (SearchTerm? term in query.get_search_terms(field)) {
+            foreach (SearchQuery.Term? term in query.get_search_terms(field)) {
                 if (term != null) {
                     debug("    - \"%s\": %s, %s",
                           term.original,
@@ -613,7 +613,7 @@ private class Geary.ImapDB.Account : BaseObject {
             // <http://redmine.yorba.org/issues/7372>.
             StringBuilder sql = new StringBuilder();
             sql.append("""
-                SELECT id, internaldate_time_t
+                SELECT id
                 FROM MessageTable
                 INDEXED BY MessageTableInternalDateTimeTIndex
             """);
@@ -650,11 +650,7 @@ private class Geary.ImapDB.Account : BaseObject {
             Db.Result result = stmt.exec(cancellable);
             while (!result.finished) {
                 int64 message_id = result.int64_at(0);
-                int64 internaldate_time_t = result.int64_at(1);
-                DateTime? internaldate = (internaldate_time_t == -1
-                    ? null : new DateTime.from_unix_local(internaldate_time_t));
-
-                ImapDB.EmailIdentifier id = new ImapDB.SearchEmailIdentifier(message_id, internaldate);
+                var id = new ImapDB.EmailIdentifier(message_id, null);
                 matching_ids.add(id);
                 id_map.set(message_id, id);
 
@@ -739,7 +735,7 @@ private class Geary.ImapDB.Account : BaseObject {
             Gee.Set<string>? result = results.get(id);
             if (result != null) {
                 foreach (string match in result) {
-                    foreach (SearchTerm term in query.get_all_terms()) {
+                    foreach (SearchQuery.Term term in query.get_all_terms()) {
                         // if prefix-matches parsed term, then don't strip
                         if (match.has_prefix(term.parsed)) {
                             good_match_found = true;
diff --git a/src/engine/imap-db/search/imap-db-search-query.vala 
b/src/engine/imap-db/imap-db-search-query.vala
similarity index 89%
rename from src/engine/imap-db/search/imap-db-search-query.vala
rename to src/engine/imap-db/imap-db-search-query.vala
index ea00787a..85f556fa 100644
--- a/src/engine/imap-db/search/imap-db-search-query.vala
+++ b/src/engine/imap-db/imap-db-search-query.vala
@@ -43,6 +43,63 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
     private const string SEARCH_OP_VALUE_UNREAD = "unread";
 
 
+    /**
+     * Various associated state with a single term in a search query.
+     */
+    internal class Term : GLib.Object {
+
+        /**
+         * The original tokenized search term with minimal other processing performed.
+         *
+         * For example, punctuation might be removed, but no casefolding has occurred.
+         */
+        public string original { get; private set; }
+
+        /**
+         * The parsed tokenized search term.
+         *
+         * Casefolding and other normalizing text operations have been performed.
+         */
+        public string parsed { get; private set; }
+
+        /**
+         * The stemmed search term.
+         *
+         * Only used if stemming is being done ''and'' the stem is different than the {@link parsed}
+         * term.
+         */
+        public string? stemmed { get; private set; }
+
+        /**
+         * A list of terms ready for binding to an SQLite statement.
+         *
+         * This should include prefix operators and quotes (i.e. ["party"] or [party*]).  These texts
+         * are guaranteed not to be null or empty strings.
+         */
+        public Gee.List<string> sql { get; private set; default = new Gee.ArrayList<string>(); }
+
+        /**
+         * Returns true if the {@link parsed} term is exact-match only (i.e. starts with quotes) and
+         * there is no {@link stemmed} variant.
+         */
+        public bool is_exact { get { return parsed.has_prefix("\"") && stemmed == null; } }
+
+        public Term(string original, string parsed, string? stemmed, string? sql_parsed, string? 
sql_stemmed) {
+            this.original = original;
+            this.parsed = parsed;
+            this.stemmed = stemmed;
+
+            // for now, only two variations: the parsed string and the stemmed; since stem is usually
+            // shorter (and will be first in the OR statement), include it first
+            if (!String.is_empty(sql_stemmed))
+                sql.add(sql_stemmed);
+
+            if (!String.is_empty(sql_parsed))
+                sql.add(sql_parsed);
+        }
+    }
+
+
     // Maps of localised search operator names and values to their
     // internal forms
     private static Gee.HashMap<string, string> search_op_names =
@@ -255,11 +312,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
     // their search term values. Note that terms without an operator
     // are stored with null as the key. Not using a MultiMap because
     // we (might) need a guarantee of order.
-    private Gee.HashMap<string?, Gee.ArrayList<SearchTerm>> field_map
-        = new Gee.HashMap<string?, Gee.ArrayList<SearchTerm>>();
+    private Gee.HashMap<string?, Gee.ArrayList<Term>> field_map
+        = new Gee.HashMap<string?, Gee.ArrayList<Term>>();
 
     // A list of all search terms, regardless of search op field name
-    private Gee.ArrayList<SearchTerm> all = new Gee.ArrayList<SearchTerm>();
+    private Gee.ArrayList<Term> all = new Gee.ArrayList<Term>();
 
     public async SearchQuery(Geary.Account owner,
                              ImapDB.Account local,
@@ -306,11 +363,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
         return field_map.keys;
     }
 
-    public Gee.List<SearchTerm>? get_search_terms(string? field) {
+    public Gee.List<Term>? get_search_terms(string? field) {
         return field_map.has_key(field) ? field_map.get(field) : null;
     }
 
-    public Gee.List<SearchTerm>? get_all_terms() {
+    public Gee.List<Term>? get_all_terms() {
         return all;
     }
 
@@ -330,7 +387,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
         bool strip_results = true;
         if (this.strategy == Geary.SearchQuery.Strategy.HORIZON)
             strip_results = false;
-        else if (traverse<SearchTerm>(this.all).any(
+        else if (traverse<Term>(this.all).any(
                      term => term.stemmed == null || term.is_exact)) {
             strip_results = false;
         }
@@ -342,8 +399,8 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
             new Gee.HashMap<Geary.NamedFlag,bool>();
         foreach (string? field in this.field_map.keys) {
             if (field == SEARCH_OP_IS) {
-                Gee.List<SearchTerm>? terms = get_search_terms(field);
-                foreach (SearchTerm term in terms)
+                Gee.List<Term>? terms = get_search_terms(field);
+                foreach (Term term in terms)
                     if (term.parsed == SEARCH_OP_VALUE_READ)
                         conditions.set(new NamedFlag("UNREAD"), true);
                     else if (term.parsed == SEARCH_OP_VALUE_UNREAD)
@@ -359,11 +416,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
     internal Gee.HashMap<string, string> get_query_phrases() {
         Gee.HashMap<string, string> phrases = new Gee.HashMap<string, string>();
         foreach (string? field in field_map.keys) {
-            Gee.List<SearchTerm>? terms = get_search_terms(field);
+            Gee.List<Term>? terms = get_search_terms(field);
             if (terms == null || terms.size == 0 || field == "is")
                 continue;
 
-            // Each SearchTerm is an AND but the SQL text within in are OR ... this allows for
+            // Each Term is an AND but the SQL text within in are OR ... this allows for
             // each user term to be AND but the variants of each term are or.  So, if terms are
             // [party] and [eventful] and stems are [parti] and [event], the search would be:
             //
@@ -380,7 +437,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
             //
             // party* OR parti* eventful* OR event*
             StringBuilder builder = new StringBuilder();
-            foreach (SearchTerm term in terms) {
+            foreach (Term term in terms) {
                 if (term.sql.size == 0)
                     continue;
 
@@ -439,12 +496,12 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
                 --quotes;
             }
 
-            SearchTerm? term;
+            Term? term;
             if (in_quote) {
                 // HACK: this helps prevent a syntax error when the user types
                 // something like from:"somebody".  If we ever properly support
                 // quotes after : we can get rid of this.
-                term = new SearchTerm(s, s, null, s.replace(":", " "), null);
+                term = new Term(s, s, null, s.replace(":", " "), null);
             } else {
                 string original = s;
 
@@ -480,7 +537,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
 
                 if (field == SEARCH_OP_IS) {
                     // s will have been de-translated
-                    term = new SearchTerm(original, s, null, null, null);
+                    term = new Term(original, s, null, null, null);
                 } else {
                     // SQL MATCH syntax for parsed term
                     string? sql_s = "%s*".printf(s);
@@ -506,7 +563,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
                     if (String.contains_any_char(s, SEARCH_TERM_CONTINUATION_CHARS))
                         s = "\"%s\"".printf(s);
 
-                    term = new SearchTerm(original, s, stemmed, sql_s, sql_stemmed);
+                    term = new Term(original, s, stemmed, sql_s, sql_stemmed);
                 }
             }
 
@@ -515,7 +572,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
 
             // Finally, add the term
             if (!this.field_map.has_key(field)) {
-                this.field_map.set(field, new Gee.ArrayList<SearchTerm>());
+                this.field_map.set(field, new Gee.ArrayList<Term>());
             }
             this.field_map.get(field).add(term);
             this.all.add(term);
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala 
b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
index ef47256d..aacc0340 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
@@ -8,7 +8,7 @@
 /**
  * Gmail-specific SearchFolder implementation.
  */
-private class Geary.ImapEngine.GmailSearchFolder : ImapDB.SearchFolder {
+private class Geary.ImapEngine.GmailSearchFolder : Search.FolderImpl {
 
     private Geary.App.EmailStore email_store;
 
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 7599ee06..86f0c877 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -417,6 +417,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         char type = (char) serialised.get_child_value(0).get_byte();
         if (type == 'i')
             return new ImapDB.EmailIdentifier.from_variant(serialised);
+        if (type == 's')
+            return new Search.EmailIdentifier.from_variant(serialised, this);
         if (type == 'o')
             return new Outbox.EmailIdentifier.from_variant(serialised);
 
@@ -808,7 +810,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
      * override this to return the correct subclass.
      */
     protected virtual SearchFolder new_search_folder() {
-        return new ImapDB.SearchFolder(this, this.local_folder_root);
+        return new Search.FolderImpl(this, this.local_folder_root);
     }
 
     /** {@inheritDoc} */
diff --git a/src/engine/meson.build b/src/engine/meson.build
index 3d83d81e..d98f9050 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -178,11 +178,7 @@ geary_engine_vala_sources = files(
   'imap-db/imap-db-folder.vala',
   'imap-db/imap-db-gc.vala',
   'imap-db/imap-db-message-row.vala',
-  'imap-db/search/imap-db-search-email-identifier.vala',
-  'imap-db/search/imap-db-search-folder.vala',
-  'imap-db/search/imap-db-search-folder-properties.vala',
-  'imap-db/search/imap-db-search-query.vala',
-  'imap-db/search/imap-db-search-term.vala',
+  'imap-db/imap-db-search-query.vala',
 
   'imap-engine/imap-engine.vala',
   'imap-engine/imap-engine-account-operation.vala',
@@ -274,6 +270,9 @@ geary_engine_vala_sources = files(
   'rfc822/rfc822-part.vala',
   'rfc822/rfc822-utils.vala',
 
+  'search/search-email-identifier.vala',
+  'search/search-folder-impl.vala',
+
   'smtp/smtp-authenticator.vala',
   'smtp/smtp-capabilities.vala',
   'smtp/smtp-client-connection.vala',
diff --git a/src/engine/search/search-email-identifier.vala b/src/engine/search/search-email-identifier.vala
new file mode 100644
index 00000000..a6c9b4ea
--- /dev/null
+++ b/src/engine/search/search-email-identifier.vala
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 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.
+ */
+
+private class Geary.Search.EmailIdentifier :
+    Geary.EmailIdentifier, Gee.Comparable<EmailIdentifier> {
+
+
+    private const string VARIANT_TYPE = "(y(vx))";
+
+
+    public static int compare_descending(EmailIdentifier a, EmailIdentifier b) {
+        return b.compare_to(a);
+    }
+
+    public static Gee.Collection<Geary.EmailIdentifier> to_source_ids(
+        Gee.Collection<Geary.EmailIdentifier> ids
+    ) {
+        var engine_ids = new Gee.LinkedList<Geary.EmailIdentifier>();
+        foreach (var id in ids) {
+            var search_id = id as EmailIdentifier;
+            engine_ids.add(search_id.source_id ?? id);
+        }
+        return engine_ids;
+    }
+
+    public static Geary.EmailIdentifier to_source_id(
+        Geary.EmailIdentifier id
+    ) {
+        var search_id = id as EmailIdentifier;
+        return search_id.source_id ?? id;
+    }
+
+
+    public Geary.EmailIdentifier source_id { get; private set; }
+
+    public GLib.DateTime? date_received { get; private set; }
+
+
+    public EmailIdentifier(Geary.EmailIdentifier source_id,
+                           GLib.DateTime? date_received) {
+        this.source_id = source_id;
+        this.date_received = date_received;
+    }
+
+    /** Reconstructs an identifier from its variant representation. */
+    public EmailIdentifier.from_variant(GLib.Variant serialised,
+                                        Account account)
+        throws EngineError.BAD_PARAMETERS {
+        if (serialised.get_type_string() != VARIANT_TYPE) {
+            throw new EngineError.BAD_PARAMETERS(
+                "Invalid serialised id type: %s", serialised.get_type_string()
+            );
+        }
+        GLib.Variant inner = serialised.get_child_value(1);
+        this(
+            account.to_email_identifier(
+                inner.get_child_value(0).get_variant()
+            ),
+            new GLib.DateTime.from_unix_utc(
+                inner.get_child_value(1).get_int64()
+            )
+        );
+    }
+
+    /** {@inheritDoc} */
+    public override uint hash() {
+        return this.source_id.hash();
+    }
+
+    /** {@inheritDoc} */
+    public override bool equal_to(Geary.EmailIdentifier other) {
+        return (
+            this.get_type() == other.get_type() &&
+            this.source_id.equal_to(((EmailIdentifier) other).source_id)
+        );
+    }
+
+    /** {@inheritDoc} */
+    public override GLib.Variant to_variant() {
+        // Return a tuple to satisfy the API contract, add an 's' to
+        // inform GenericAccount that it's an IMAP id.
+        return new GLib.Variant.tuple(new Variant[] {
+                new GLib.Variant.byte('s'),
+                new GLib.Variant.tuple(new Variant[] {
+                        new GLib.Variant.variant(this.source_id.to_variant()),
+                        new GLib.Variant.int64(this.date_received.to_unix())
+                    })
+            });
+    }
+
+    /** {@inheritDoc} */
+    public override string to_string() {
+        return "%s(%s,%lld)".printf(
+            this.get_type().name(),
+            this.source_id.to_string(),
+            this.date_received.to_unix()
+        );
+    }
+
+    public override int natural_sort_comparator(Geary.EmailIdentifier o) {
+        EmailIdentifier? other = o as EmailIdentifier;
+        if (other == null)
+            return 1;
+
+        return compare_to(other);
+    }
+
+    public virtual int compare_to(EmailIdentifier other) {
+        // if both have date received, compare on that, using stable sort if the same
+        if (date_received != null && other.date_received != null) {
+            int compare = date_received.compare(other.date_received);
+
+            return (compare != 0) ? compare : stable_sort_comparator(other);
+        }
+
+        // if neither have date received, fall back on stable sort
+        if (date_received == null && other.date_received == null)
+            return stable_sort_comparator(other);
+
+        // put identifiers with no date ahead of those with
+        return (date_received == null ? -1 : 1);
+    }
+
+}
diff --git a/src/engine/search/search-folder-impl.vala b/src/engine/search/search-folder-impl.vala
new file mode 100644
index 00000000..25009437
--- /dev/null
+++ b/src/engine/search/search-folder-impl.vala
@@ -0,0 +1,546 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 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.
+ */
+
+/**
+ * A default implementation of a search folder.
+ *
+ * This implementation of {@link Geary.SearchFolder} uses the search
+ * methods on {@link Account} to implement account-wide email search.
+ */
+internal class Geary.Search.FolderImpl :
+    Geary.SearchFolder, Geary.FolderSupport.Remove {
+
+
+    /** Max number of emails that can ever be in the folder. */
+    public const int MAX_RESULT_EMAILS = 1000;
+
+    /** The canonical name of the search folder. */
+    public const string MAGIC_BASENAME = "$GearySearchFolder$";
+
+    private const Geary.SpecialFolderType[] EXCLUDE_TYPES = {
+        Geary.SpecialFolderType.SPAM,
+        Geary.SpecialFolderType.TRASH,
+        Geary.SpecialFolderType.DRAFTS,
+        // Orphan emails (without a folder) are also excluded; see ct or.
+    };
+
+
+    private class FolderProperties : Geary.FolderProperties {
+
+
+        public FolderProperties(int total, int unread) {
+            base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
+        }
+
+        public void set_total(int total) {
+            this.email_total = total;
+        }
+
+    }
+
+
+    // Folders that should be excluded from search
+    private Gee.HashSet<Geary.FolderPath?> exclude_folders =
+        new Gee.HashSet<Geary.FolderPath?>();
+
+    // The email present in the folder, sorted
+    private Gee.TreeSet<EmailIdentifier> contents;
+
+    // Map of engine ids to search ids
+    private Gee.Map<Geary.EmailIdentifier,EmailIdentifier> id_map;
+
+    private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
+
+
+    public FolderImpl(Geary.Account account, FolderRoot root) {
+        base(
+            account,
+            new FolderProperties(0, 0),
+            root.get_child(MAGIC_BASENAME, Trillian.TRUE)
+        );
+
+        account.folders_available_unavailable.connect(on_folders_available_unavailable);
+        account.folders_special_type.connect(on_folders_special_type);
+        account.email_locally_complete.connect(on_email_locally_complete);
+        account.email_removed.connect(on_account_email_removed);
+
+        clear_contents();
+
+        // We always want to exclude emails that don't live anywhere
+        // from search results.
+        exclude_orphan_emails();
+    }
+
+    ~FolderImpl() {
+        account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
+        account.folders_special_type.disconnect(on_folders_special_type);
+        account.email_locally_complete.disconnect(on_email_locally_complete);
+        account.email_removed.disconnect(on_account_email_removed);
+    }
+
+    /**
+     * Sets the keyword string for this search.
+     */
+    public override async void search(Geary.SearchQuery query,
+                                      GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+
+        this.query = query;
+        GLib.Error? error = null;
+        try {
+            yield do_search_async(null, null, cancellable);
+        } catch(Error e) {
+            error = e;
+        }
+
+        result_mutex.release(ref result_mutex_token);
+
+        if (error != null) {
+            throw error;
+        }
+
+        this.query_evaluation_complete();
+    }
+
+    /**
+     * Clears the search query and results.
+     */
+    public override void clear() {
+        var old_contents = this.contents;
+        clear_contents();
+        notify_email_removed(old_contents);
+        notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
+
+        if (this.query != null) {
+            this.query = null;
+            this.query_evaluation_complete();
+        }
+    }
+
+    /**
+     * Given a list of mail IDs, returns a set of casefolded words that match for the current
+     * search query.
+     */
+    public override async Gee.Set<string>? get_search_matches_async(
+        Gee.Collection<Geary.EmailIdentifier> ids,
+        GLib.Cancellable? cancellable = null
+    ) throws GLib.Error {
+        Gee.Set<string>? results = null;
+        if (this.query != null) {
+            results = yield account.get_search_matches_async(
+                this.query,
+                EmailIdentifier.to_source_ids(ids),
+                cancellable
+            );
+        }
+        return results;
+    }
+
+    public override async Gee.List<Email>? list_email_by_id_async(
+        Geary.EmailIdentifier? initial_id,
+        int count,
+        Email.Field required_fields,
+        Geary.Folder.ListFlags flags,
+        Cancellable? cancellable = null
+    ) throws GLib.Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+
+        var engine_ids = new Gee.LinkedList<Geary.EmailIdentifier>();
+
+        if (Geary.Folder.ListFlags.OLDEST_TO_NEWEST in flags) {
+            EmailIdentifier? oldest = null;
+            if (!this.contents.is_empty) {
+                if (initial_id == null) {
+                    oldest = this.contents.last();
+                } else {
+                    oldest = this.id_map.get(initial_id);
+
+                    if (oldest == null) {
+                        throw new EngineError.NOT_FOUND(
+                            "Initial id not found %s", initial_id.to_string()
+                        );
+                    }
+
+                    if (!(Geary.Folder.ListFlags.INCLUDING_ID in flags)) {
+                        oldest = contents.higher(oldest);
+                    }
+                }
+            }
+            if (oldest != null) {
+                var iter = (
+                    this.contents.iterator_at(oldest) as
+                    Gee.BidirIterator<EmailIdentifier>
+                );
+                engine_ids.add(oldest.source_id);
+                while (engine_ids.size < count && iter.previous()) {
+                    engine_ids.add(iter.get().source_id);
+                }
+            }
+        } else {
+            // Newest to oldest
+            EmailIdentifier? newest = null;
+            if (!this.contents.is_empty) {
+                if (initial_id == null) {
+                    newest = this.contents.first();
+                } else {
+                    newest = this.id_map.get(initial_id);
+
+                    if (newest == null) {
+                        throw new EngineError.NOT_FOUND(
+                            "Initial id not found %s", initial_id.to_string()
+                        );
+                    }
+
+                    if (!(Geary.Folder.ListFlags.INCLUDING_ID in flags)) {
+                        newest = contents.lower(newest);
+                    }
+                }
+            }
+            if (newest != null) {
+                var iter = (
+                    this.contents.iterator_at(newest) as
+                    Gee.BidirIterator<EmailIdentifier>
+                );
+                engine_ids.add(newest.source_id);
+                while (engine_ids.size < count && iter.next()) {
+                    engine_ids.add(iter.get().source_id);
+                }
+            }
+        }
+
+        Gee.List<Email>? results = null;
+        GLib.Error? list_error = null;
+        if (!engine_ids.is_empty) {
+            try {
+                results = yield this.account.list_local_email_async(
+                    engine_ids,
+                    required_fields,
+                    cancellable
+                );
+            } catch (GLib.Error error) {
+                list_error = error;
+            }
+        }
+
+        result_mutex.release(ref result_mutex_token);
+
+        if (list_error != null) {
+            throw list_error;
+        }
+
+        return results;
+    }
+
+    public override async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
+        Gee.Collection<Geary.EmailIdentifier> ids,
+        Geary.Email.Field required_fields,
+        Geary.Folder.ListFlags flags,
+        Cancellable? cancellable = null
+    ) throws GLib.Error {
+        return yield this.account.list_local_email_async(
+            EmailIdentifier.to_source_ids(ids),
+            required_fields,
+            cancellable
+        );
+    }
+
+    public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
+        // TODO: This method is not currently called, but is required by the interface.  Before completing
+        // this feature, it should either be implemented either here or in AbstractLocalFolder.
+        error("Search folder does not implement list_local_email_fields_async");
+    }
+
+    public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
+                                                        Geary.Email.Field required_fields,
+                                                        Geary.Folder.ListFlags flags,
+                                                        Cancellable? cancellable = null)
+        throws GLib.Error {
+        return yield this.account.local_fetch_email_async(
+            EmailIdentifier.to_source_id(id), required_fields, cancellable
+        );
+    }
+
+    public virtual async void remove_email_async(
+        Gee.Collection<Geary.EmailIdentifier> email_ids,
+        GLib.Cancellable? cancellable = null
+    ) throws GLib.Error {
+        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders =
+            yield account.get_containing_folders_async(
+                EmailIdentifier.to_source_ids(email_ids),
+                cancellable
+            );
+        if (ids_to_folders != null) {
+            Gee.MultiMap<Geary.FolderPath, Geary.EmailIdentifier> folders_to_ids =
+                Geary.Collection.reverse_multi_map<Geary.EmailIdentifier, Geary.FolderPath>(ids_to_folders);
+
+            foreach (Geary.FolderPath path in folders_to_ids.get_keys()) {
+                Geary.Folder folder = account.get_folder(path);
+                Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
+                if (remove != null) {
+                    Gee.Collection<Geary.EmailIdentifier> ids = folders_to_ids.get(path);
+
+                    debug("Search folder removing %d emails from %s", ids.size, folder.to_string());
+
+                    bool open = false;
+                    try {
+                        yield folder.open_async(Geary.Folder.OpenFlags.NONE, cancellable);
+                        open = true;
+                        yield remove.remove_email_async(ids, cancellable);
+                    } finally {
+                        if (open) {
+                            try {
+                                yield folder.close_async();
+                            } catch (Error e) {
+                                debug("Error closing folder %s: %s", folder.to_string(), e.message);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // NOTE: you must call this ONLY after locking result_mutex_token.
+    // If both *_ids parameters are null, the results of this search are
+    // considered to be the full new set.  If non-null, the results are
+    // considered to be a delta and are added or subtracted from the full set.
+    // add_ids are new ids to search for, remove_ids are ids in our result set
+    // and will be removed.
+    private async void do_search_async(Gee.Collection<Geary.EmailIdentifier>? add_ids,
+                                       Gee.Collection<Geary.EmailIdentifier>? remove_ids,
+                                       GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        var id_map = this.id_map;
+        var contents = this.contents;
+        var added = new Gee.LinkedList<EmailIdentifier>();
+        var removed = new Gee.LinkedList<EmailIdentifier>();
+
+        if (remove_ids == null) {
+            // Adding email to the search, either searching all local
+            // email if to_add is null, or adding only a matching
+            // subset of the given in to_add
+            //
+            // TODO: don't limit this to MAX_RESULT_EMAILS.  Instead,
+            // we could be smarter about only fetching the search
+            // results in list_email_async() etc., but this leads to
+            // some more complications when redoing the search.
+            Gee.Collection<Geary.EmailIdentifier>? id_results =
+                yield this.account.local_search_async(
+                    this.query,
+                    MAX_RESULT_EMAILS,
+                    0,
+                    this.exclude_folders,
+                    add_ids, // If null, will search all local email
+                    cancellable
+                );
+
+            if (id_results != null) {
+                // Fetch email to get the received date for
+                // correct ordering in the search folder
+                Gee.Collection<Email> email_results =
+                    yield this.account.list_local_email_async(
+                        id_results,
+                        PROPERTIES,
+                        cancellable
+                    );
+
+                if (add_ids == null) {
+                    // Not appending new email, so remove any not
+                    // found in the results. Add to a set first to
+                    // avoid O(N^2) lookup complexity.
+                    var hashed_results = new Gee.HashSet<Geary.EmailIdentifier>();
+                    hashed_results.add_all(id_results);
+
+                    var existing = id_map.map_iterator();
+                    while (existing.next()) {
+                        if (!hashed_results.contains(existing.get_key())) {
+                            var search_id = existing.get_value();
+                            existing.unset();
+                            contents.remove(search_id);
+                            removed.add(search_id);
+                        }
+                    }
+                }
+
+                foreach (var email in email_results) {
+                    if (!id_map.has_key(email.id)) {
+                        var search_id = new EmailIdentifier(
+                            email.id, email.properties.date_received
+                        );
+                        id_map.set(email.id, search_id);
+                        contents.add(search_id);
+                        added.add(search_id);
+                    }
+                }
+            }
+        } else {
+            // Removing email, can just remove them directly
+            foreach (var id in remove_ids) {
+                EmailIdentifier search_id;
+                if (id_map.unset(id, out search_id)) {
+                    contents.remove(search_id);
+                    removed.add(search_id);
+                }
+            }
+        }
+
+        ((FolderProperties) this.properties).set_total(this.contents.size);
+
+        // Note that we probably shouldn't be firing these signals from inside
+        // our mutex lock.  Keep an eye on it, and if there's ever a case where
+        // it might cause problems, it shouldn't be too hard to move the
+        // firings outside.
+
+        Geary.Folder.CountChangeReason reason = CountChangeReason.NONE;
+        if (added.size > 0) {
+            // TODO: we'd like to be able to use APPENDED here when applicable,
+            // but because of the potential to append a thousand results at
+            // once and the ConversationMonitor's inability to handle that
+            // gracefully (#7464), we always use INSERTED for now.
+            notify_email_inserted(added);
+            reason |= Geary.Folder.CountChangeReason.INSERTED;
+        }
+        if (removed.size > 0) {
+            notify_email_removed(removed);
+            reason |= Geary.Folder.CountChangeReason.REMOVED;
+        }
+        if (reason != CountChangeReason.NONE)
+            notify_email_count_changed(this.contents.size, reason);
+    }
+
+    private async void do_append(Geary.Folder folder,
+                                 Gee.Collection<Geary.EmailIdentifier> ids,
+                                 GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+
+        GLib.Error? error = null;
+        try {
+            if (!this.exclude_folders.contains(folder.path)) {
+                yield do_search_async(ids, null, cancellable);
+            }
+        } catch (GLib.Error e) {
+            error = e;
+        }
+
+        result_mutex.release(ref result_mutex_token);
+
+        if (error != null)
+            throw error;
+    }
+
+    private async void do_remove(Geary.Folder folder,
+                                 Gee.Collection<Geary.EmailIdentifier> ids,
+                                 GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+
+        GLib.Error? error = null;
+        try {
+            var id_map = this.id_map;
+            var relevant_ids = (
+                traverse(ids)
+                .filter(id => id_map.has_key(id))
+                .to_linked_list()
+            );
+
+            if (relevant_ids.size > 0) {
+                yield do_search_async(null, relevant_ids, cancellable);
+            }
+        } catch (GLib.Error e) {
+            error = e;
+        }
+
+        result_mutex.release(ref result_mutex_token);
+
+        if (error != null)
+            throw error;
+    }
+
+    private void clear_contents() {
+        this.contents = new Gee.TreeSet<EmailIdentifier>(
+            EmailIdentifier.compare_descending
+        );
+        this.id_map = new Gee.HashMap<Geary.EmailIdentifier,EmailIdentifier>();
+    }
+
+    private void include_folder(Geary.Folder folder) {
+        this.exclude_folders.remove(folder.path);
+    }
+
+    private void exclude_folder(Geary.Folder folder) {
+        this.exclude_folders.add(folder.path);
+    }
+
+    private void exclude_orphan_emails() {
+        this.exclude_folders.add(null);
+    }
+
+    private void on_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
+        Gee.Collection<Geary.Folder>? unavailable) {
+        if (available != null) {
+            // Exclude it from searching if it's got the right special type.
+            foreach(Geary.Folder folder in Geary.traverse<Geary.Folder>(available)
+                .filter(f => f.special_folder_type in EXCLUDE_TYPES))
+                exclude_folder(folder);
+        }
+    }
+
+    private void on_folders_special_type(Gee.Collection<Geary.Folder> folders) {
+        foreach (Geary.Folder folder in folders) {
+            if (folder.special_folder_type in EXCLUDE_TYPES) {
+                exclude_folder(folder);
+            } else {
+                include_folder(folder);
+            }
+        }
+    }
+
+    private void on_email_locally_complete(Geary.Folder folder,
+                                           Gee.Collection<Geary.EmailIdentifier> ids) {
+        if (this.query != null) {
+            this.do_append.begin(
+                folder, ids, null,
+                (obj, res) => {
+                    try {
+                        this.do_append.end(res);
+                    } catch (GLib.Error error) {
+                        this.account.report_problem(
+                            new Geary.AccountProblemReport(
+                                this.account.information, error
+                            )
+                        );
+                    }
+                }
+            );
+        }
+    }
+
+    private void on_account_email_removed(Geary.Folder folder,
+                                          Gee.Collection<Geary.EmailIdentifier> ids) {
+        if (this.query != null) {
+            this.do_remove.begin(
+                folder, ids, null,
+                (obj, res) => {
+                    try {
+                        this.do_remove.end(res);
+                    } catch (GLib.Error error) {
+                        this.account.report_problem(
+                            new Geary.AccountProblemReport(
+                                this.account.information, error
+                            )
+                        );
+                    }
+                }
+            );
+        }
+    }
+
+}
diff --git a/test/engine/imap-engine/imap-engine-generic-account-test.vala 
b/test/engine/imap-engine/imap-engine-generic-account-test.vala
index b22af954..8698f12f 100644
--- a/test/engine/imap-engine/imap-engine-generic-account-test.vala
+++ b/test/engine/imap-engine/imap-engine-generic-account-test.vala
@@ -99,6 +99,21 @@ public class Geary.ImapEngine.GenericAccountTest : TestCase {
                 )
             )
         );
+        assert_non_null(
+            test_article.to_email_identifier(
+                new GLib.Variant(
+                    "(yr)",
+                    's',
+                    new GLib.Variant(
+                        "(vx)",
+                        new GLib.Variant(
+                            "(yr)", 'o', new GLib.Variant("(xx)", 1, 2)
+                        ),
+                        3
+                    )
+                )
+            )
+        );
     }
 
 }



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