From 924104c282af80161ff2b9dd683481ed2a722eb2 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Tue, 10 Dec 2019 20:51:20 +1100 Subject: [PATCH] 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 +- .../{search => }/imap-db-search-query.vala | 89 ++- .../imap-db-search-email-identifier.vala | 75 --- .../imap-db-search-folder-properties.vala | 16 - .../imap-db/search/imap-db-search-folder.vala | 428 -------------- .../imap-db/search/imap-db-search-term.vala | 62 -- .../imap-engine-gmail-search-folder.vala | 2 +- .../imap-engine-generic-account.vala | 4 +- src/engine/meson.build | 9 +- .../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(-) rename src/engine/imap-db/{search => }/imap-db-search-query.vala (89%) delete mode 100644 src/engine/imap-db/search/imap-db-search-email-identifier.vala delete mode 100644 src/engine/imap-db/search/imap-db-search-folder-properties.vala delete mode 100644 src/engine/imap-db/search/imap-db-search-folder.vala delete mode 100644 src/engine/imap-db/search/imap-db-search-term.vala create mode 100644 src/engine/search/search-email-identifier.vala create mode 100644 src/engine/search/search-folder-impl.vala 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 { // . 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? 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 sql { get; private set; default = new Gee.ArrayList(); } + + /** + * 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 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> field_map - = new Gee.HashMap>(); + private Gee.HashMap> field_map + = new Gee.HashMap>(); // A list of all search terms, regardless of search op field name - private Gee.ArrayList all = new Gee.ArrayList(); + private Gee.ArrayList all = new Gee.ArrayList(); 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? get_search_terms(string? field) { + public Gee.List? get_search_terms(string? field) { return field_map.has_key(field) ? field_map.get(field) : null; } - public Gee.List? get_all_terms() { + public Gee.List? 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(this.all).any( + else if (traverse(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(); foreach (string? field in this.field_map.keys) { if (field == SEARCH_OP_IS) { - Gee.List? terms = get_search_terms(field); - foreach (SearchTerm term in terms) + Gee.List? 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 get_query_phrases() { Gee.HashMap phrases = new Gee.HashMap(); foreach (string? field in field_map.keys) { - Gee.List? terms = get_search_terms(field); + Gee.List? 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()); + this.field_map.set(field, new Gee.ArrayList()); } this.field_map.get(field).add(term); this.all.add(term); diff --git a/src/engine/imap-db/search/imap-db-search-email-identifier.vala b/src/engine/imap-db/search/imap-db-search-email-identifier.vala deleted file mode 100644 index 0a014851..00000000 --- a/src/engine/imap-db/search/imap-db-search-email-identifier.vala +++ /dev/null @@ -1,75 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier, - Gee.Comparable { - public DateTime? date_received { get; private set; } - - public SearchEmailIdentifier(int64 message_id, DateTime? date_received) { - base(message_id, null); - - this.date_received = date_received; - } - - public static int compare_descending(SearchEmailIdentifier a, SearchEmailIdentifier b) { - return b.compare_to(a); - } - - public static Gee.ArrayList array_list_from_results( - Gee.Collection? results) { - Gee.ArrayList r = new Gee.ArrayList(); - - if (results != null) { - foreach (Geary.EmailIdentifier id in results) { - SearchEmailIdentifier? search_id = id as SearchEmailIdentifier; - - assert(search_id != null); - r.add(search_id); - } - } - - return r; - } - - // Searches for a generic EmailIdentifier in a collection of SearchEmailIdentifiers. - public static SearchEmailIdentifier? collection_get_email_identifier( - Gee.Collection collection, Geary.EmailIdentifier id) { - foreach (SearchEmailIdentifier search_id in collection) { - if (id.equal_to(search_id)) - return search_id; - } - return null; - } - - public override int natural_sort_comparator(Geary.EmailIdentifier o) { - ImapDB.SearchEmailIdentifier? other = o as ImapDB.SearchEmailIdentifier; - if (other == null) - return 1; - - return compare_to(other); - } - - public virtual int compare_to(SearchEmailIdentifier 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); - } - - public override string to_string() { - return "[%s/null/%s]".printf(message_id.to_string(), - (date_received == null ? "null" : date_received.to_string())); - } -} diff --git a/src/engine/imap-db/search/imap-db-search-folder-properties.vala b/src/engine/imap-db/search/imap-db-search-folder-properties.vala deleted file mode 100644 index 870a55ef..00000000 --- a/src/engine/imap-db/search/imap-db-search-folder-properties.vala +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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.ImapDB.SearchFolderProperties : Geary.FolderProperties { - public SearchFolderProperties(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; - } -} - diff --git a/src/engine/imap-db/search/imap-db-search-folder.vala b/src/engine/imap-db/search/imap-db-search-folder.vala deleted file mode 100644 index a3eb8ac6..00000000 --- a/src/engine/imap-db/search/imap-db-search-folder.vala +++ /dev/null @@ -1,428 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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.ImapDB.SearchFolder : 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 ctor. - }; - - - private Gee.HashSet exclude_folders = new Gee.HashSet(); - private Gee.TreeSet search_results; - private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex(); - - - public SearchFolder(Geary.Account account, FolderRoot root) { - base( - account, - new SearchFolderProperties(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_search_results(); - - // We always want to exclude emails that don't live anywhere from - // search results. - exclude_orphan_emails(); - } - - ~SearchFolder() { - 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); - } - - private async void append_new_email_async(Geary.Folder folder, - Gee.Collection ids, - GLib.Cancellable? cancellable) - throws GLib.Error { - int result_mutex_token = yield result_mutex.claim_async(); - - Error? error = null; - try { - yield do_search_async(ids, null, cancellable); - } catch(Error e) { - error = e; - } - - result_mutex.release(ref result_mutex_token); - - if (error != null) - throw error; - } - - private async void handle_removed_email_async(Geary.Folder folder, - Gee.Collection ids, - GLib.Cancellable? cancellable) - throws GLib.Error { - int result_mutex_token = yield result_mutex.claim_async(); - - GLib.Error? error = null; - try { - Gee.ArrayList relevant_ids - = Geary.traverse(ids) - .map_nonnull( - id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id)) - .to_array_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; - } - - /** - * Clears the search query and results. - */ - public override void clear() { - var local_results = this.search_results; - clear_search_results(); - notify_email_removed(local_results); - notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED); - - if (this.query != null) { - this.query = null; - this.query_evaluation_complete(); - } - } - - /** - * 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(); - } - - // 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 - // that will be removed if this search doesn't turn them up. - private async void do_search_async(Gee.Collection? add_ids, - Gee.Collection? remove_ids, Cancellable? cancellable) throws Error { - // There are three cases here: 1) replace full result set, where the - // *_ids parameters are both null, 2) add to result set, where just - // remove_ids is null, and 3) remove from result set, where just - // add_ids is null. We can't add and remove at the same time. - assert(add_ids == null || remove_ids == null); - - // 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.ArrayList results = - SearchEmailIdentifier.array_list_from_results( - yield account.local_search_async( - this.query, - MAX_RESULT_EMAILS, - 0, - exclude_folders, - add_ids ?? remove_ids, - cancellable - ) - ); - - Gee.List added - = Gee.List.empty(); - Gee.List removed - = Gee.List.empty(); - - if (remove_ids == null) { - added = Geary.traverse(results) - .filter(id => !(id in search_results)) - .to_array_list(); - } - if (add_ids == null) { - removed = Geary.traverse(remove_ids ?? search_results) - .filter(id => !(id in results)) - .to_array_list(); - } - - search_results.remove_all(removed); - search_results.add_all(added); - - ((ImapDB.SearchFolderProperties) properties).set_total(search_results.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(search_results.size, reason); - } - - public override async Gee.List? list_email_by_id_async(Geary.EmailIdentifier? initial_id, - int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null) - throws Error { - if (count <= 0) - return null; - - // TODO: as above, this is incomplete and inefficient. - int result_mutex_token = yield result_mutex.claim_async(); - - Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size]; - int initial_index = 0; - int i = 0; - foreach (ImapDB.SearchEmailIdentifier id in search_results) { - if (initial_id != null && id.equal_to(initial_id)) - initial_index = i; - ids[i++] = id; - } - - if (initial_id == null && flags.is_all_set(Geary.Folder.ListFlags.OLDEST_TO_NEWEST)) - initial_index = ids.length - 1; - - Gee.List results = new Gee.ArrayList(); - Error? fetch_err = null; - if (initial_index >= 0) { - int increment = flags.is_oldest_to_newest() ? -1 : 1; - i = initial_index; - if (!flags.is_including_id() && initial_id != null) - i += increment; - int end = i + (count * increment); - - for (; i >= 0 && i < search_results.size && i != end; i += increment) { - try { - results.add(yield fetch_email_async(ids[i], required_fields, flags, cancellable)); - } catch (Error err) { - // Don't let missing or incomplete messages stop the list operation, which has - // different semantics from fetch - if (!(err is EngineError.NOT_FOUND) && !(err is EngineError.INCOMPLETE_MESSAGE)) { - fetch_err = err; - - break; - } - } - } - } - - result_mutex.release(ref result_mutex_token); - - if (fetch_err != null) - throw fetch_err; - - return (results.size == 0 ? null : results); - } - - public override async Gee.List? list_email_by_sparse_id_async( - Gee.Collection ids, Geary.Email.Field required_fields, - Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { - // TODO: Fetch emails in a batch. - Gee.List result = new Gee.ArrayList(); - foreach(Geary.EmailIdentifier id in ids) - result.add(yield fetch_email_async(id, required_fields, flags, cancellable)); - - return (result.size == 0 ? null : result); - } - - public override async Gee.Map? list_local_email_fields_async( - Gee.Collection 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 Error { - return yield account.local_fetch_email_async(id, required_fields, cancellable); - } - - public virtual async void - remove_email_async(Gee.Collection email_ids, - GLib.Cancellable? cancellable = null) - throws GLib.Error { - Gee.MultiMap? ids_to_folders - = yield account.get_containing_folders_async(email_ids, cancellable); - if (ids_to_folders == null) - return; - - Gee.MultiMap folders_to_ids - = Geary.Collection.reverse_multi_map(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) - continue; - - Gee.Collection ids = folders_to_ids.get(path); - assert(ids.size > 0); - - 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( - Collection.copy(ids), cancellable - ); - } finally { - if (open) { - try { - yield folder.close_async(); - } catch (Error e) { - debug("Error closing folder %s: %s", folder.to_string(), e.message); - } - } - } - } - } - - /** - * Given a list of mail IDs, returns a set of casefolded words that match for the current - * search query. - */ - public override async Gee.Set? get_search_matches_async( - Gee.Collection ids, - GLib.Cancellable? cancellable = null - ) throws GLib.Error { - Gee.Set? results = null; - if (this.query != null) { - results = yield account.get_search_matches_async( - this.query, ids, cancellable - ); - } - return results; - } - - 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 clear_search_results() { - this.search_results = new Gee.TreeSet( - SearchEmailIdentifier.compare_descending - ); - } - - private void on_folders_available_unavailable(Gee.Collection? available, - Gee.Collection? unavailable) { - if (available != null) { - // Exclude it from searching if it's got the right special type. - foreach(Geary.Folder folder in Geary.traverse(available) - .filter(f => f.special_folder_type in EXCLUDE_TYPES)) - exclude_folder(folder); - } - } - - private void on_folders_special_type(Gee.Collection 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 ids) { - if (this.query != null) { - this.append_new_email_async.begin( - folder, ids, null, - (obj, res) => { - try { - this.append_new_email_async.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 ids) { - if (this.query != null) { - this.handle_removed_email_async.begin( - folder, ids, null, - (obj, res) => { - try { - this.handle_removed_email_async.end(res); - } catch (GLib.Error error) { - this.account.report_problem( - new Geary.AccountProblemReport( - this.account.information, error - ) - ); - } - } - ); - } - } - -} diff --git a/src/engine/imap-db/search/imap-db-search-term.vala b/src/engine/imap-db/search/imap-db-search-term.vala deleted file mode 100644 index 8427b25f..00000000 --- a/src/engine/imap-db/search/imap-db-search-term.vala +++ /dev/null @@ -1,62 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/** - * Various associated state with a single term in a {@link ImapDB.SearchQuery}. - */ - -private class Geary.ImapDB.SearchTerm : BaseObject { - /** - * 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 sql { get; private set; default = new Gee.ArrayList(); } - - /** - * 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 SearchTerm(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); - } -} - 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 + * + * 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 { + + + 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 to_source_ids( + Gee.Collection ids + ) { + var engine_ids = new Gee.LinkedList(); + 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 + * + * 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 exclude_folders = + new Gee.HashSet(); + + // The email present in the folder, sorted + private Gee.TreeSet contents; + + // Map of engine ids to search ids + private Gee.Map 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? get_search_matches_async( + Gee.Collection ids, + GLib.Cancellable? cancellable = null + ) throws GLib.Error { + Gee.Set? 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? 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(); + + 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 + ); + 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 + ); + engine_ids.add(newest.source_id); + while (engine_ids.size < count && iter.next()) { + engine_ids.add(iter.get().source_id); + } + } + } + + Gee.List? 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? list_email_by_sparse_id_async( + Gee.Collection 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? list_local_email_fields_async( + Gee.Collection 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 email_ids, + GLib.Cancellable? cancellable = null + ) throws GLib.Error { + Gee.MultiMap? ids_to_folders = + yield account.get_containing_folders_async( + EmailIdentifier.to_source_ids(email_ids), + cancellable + ); + if (ids_to_folders != null) { + Gee.MultiMap folders_to_ids = + Geary.Collection.reverse_multi_map(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 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? add_ids, + Gee.Collection? remove_ids, + GLib.Cancellable? cancellable) + throws GLib.Error { + var id_map = this.id_map; + var contents = this.contents; + var added = new Gee.LinkedList(); + var removed = new Gee.LinkedList(); + + 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? 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_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(); + 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 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 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.compare_descending + ); + this.id_map = new Gee.HashMap(); + } + + 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? available, + Gee.Collection? unavailable) { + if (available != null) { + // Exclude it from searching if it's got the right special type. + foreach(Geary.Folder folder in Geary.traverse(available) + .filter(f => f.special_folder_type in EXCLUDE_TYPES)) + exclude_folder(folder); + } + } + + private void on_folders_special_type(Gee.Collection 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 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 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 + ) + ) + ) + ); } }