[geary/mjog/search-refinement: 5/14] Clean up search folder implementation
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/search-refinement: 5/14] Clean up search folder implementation
- Date: Mon, 16 Dec 2019 23:11:19 +0000 (UTC)
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]