[geary/wip/api-search] Properly break-out SearchFolder ImapDB internals from API class
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/api-search] Properly break-out SearchFolder ImapDB internals from API class
- Date: Fri, 27 Feb 2015 22:49:41 +0000 (UTC)
commit 339e3a74d962cf94def6ea992d6ac25655b3db10
Author: Jim Nelson <jim yorba org>
Date: Fri Feb 27 14:49:01 2015 -0800
Properly break-out SearchFolder ImapDB internals from API class
src/CMakeLists.txt | 9 +-
src/engine/api/geary-search-folder.vala | 443 ++------------------
src/engine/imap-db/imap-db-account.vala | 2 +-
.../imap-db-search-email-identifier.vala | 16 +-
.../search/imap-db-search-folder-properties.vala | 16 +
.../imap-db/search/imap-db-search-folder-root.vala | 14 +
.../imap-db/search/imap-db-search-folder.vala | 404 ++++++++++++++++++
.../imap-db/{ => search}/imap-db-search-query.vala | 0
.../imap-db/{ => search}/imap-db-search-term.vala | 0
.../gmail/imap-engine-gmail-search-folder.vala | 4 +-
.../imap-engine/imap-engine-generic-account.vala | 4 +-
11 files changed, 488 insertions(+), 424 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 78151fc..102917e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -172,9 +172,12 @@ engine/imap-db/imap-db-folder.vala
engine/imap-db/imap-db-gc.vala
engine/imap-db/imap-db-message-addresses.vala
engine/imap-db/imap-db-message-row.vala
-engine/imap-db/imap-db-search-query.vala
-engine/imap-db/imap-db-search-term.vala
-engine/imap-db/imap-db-search-email-identifier.vala
+engine/imap-db/search/imap-db-search-email-identifier.vala
+engine/imap-db/search/imap-db-search-folder.vala
+engine/imap-db/search/imap-db-search-folder-properties.vala
+engine/imap-db/search/imap-db-search-folder-root.vala
+engine/imap-db/search/imap-db-search-query.vala
+engine/imap-db/search/imap-db-search-term.vala
engine/imap-db/outbox/smtp-outbox-email-identifier.vala
engine/imap-db/outbox/smtp-outbox-email-properties.vala
engine/imap-db/outbox/smtp-outbox-folder.vala
diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala
index 92363ae..af2d70d 100644
--- a/src/engine/api/geary-search-folder.vala
+++ b/src/engine/api/geary-search-folder.vala
@@ -4,44 +4,30 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.SearchFolderRoot : Geary.FolderRoot {
- public const string MAGIC_BASENAME = "$GearySearchFolder$";
-
- public SearchFolderRoot() {
- base(MAGIC_BASENAME, null, false, false);
- }
-}
-
-private class Geary.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;
- }
-}
-
/**
- * Special folder type used to query and display search results.
+ * Special local { link Folder} used to query and display search results of { link Email} from
+ * across the { link Account}'s local storage.
+ *
+ * SearchFolder is merely specified to be a Folder, but implementations may add various
+ * { link FolderSupport} interfaces. In particular { link FolderSupport.Remove} should be supported,
+ * but again, is not required.
+ *
+ * SearchFolder is expected to produce { link EmailIdentifier}s which can be accepted by other
+ * Folders within the Account (with the exception of the Outbox). Those Folders may need to
+ * translate those EmailIdentifiers to their own type for ordering reasons, but in general the
+ * expectation is that the results of SearchFolder can then be applied to operations on Email in
+ * other remote-backed folders.
*/
-public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport.Remove {
- // Max number of emails that can ever be in the folder.
- public static const int MAX_RESULT_EMAILS = 1000;
-
+public abstract class Geary.SearchFolder : Geary.AbstractLocalFolder {
private weak Account _account;
public override Account account { get { return _account; } }
- private SearchFolderProperties _properties = new SearchFolderProperties(0, 0);
+ private FolderProperties _properties;
public override FolderProperties properties { get { return _properties; } }
private FolderPath? _path = null;
- public override FolderPath path {
- get {
- return (_path != null) ? _path : _path = new SearchFolderRoot();
- }
- }
+ public override FolderPath path { get { return _path; } }
public override SpecialFolderType special_folder_type {
get {
@@ -49,17 +35,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
}
}
- public Geary.SearchQuery? search_query { get; private set; default = null; }
-
- private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
- private 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.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
- private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
+ public Geary.SearchQuery? search_query { get; protected set; default = null; }
/**
* Fired when the search query has changed. This signal is fired *after* the search
@@ -67,388 +43,39 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
*/
public signal void search_query_changed(Geary.SearchQuery? query);
- public SearchFolder(Account account) {
- base();
-
+ protected SearchFolder(Account account, FolderProperties properties, FolderPath path) {
_account = account;
-
- account.folders_available_unavailable.connect(on_folders_available_unavailable);
- 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();
+ _properties = properties;
+ _path = path;
}
- ~SearchFolder() {
- account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
- account.email_locally_complete.disconnect(on_email_locally_complete);
- account.email_removed.disconnect(on_account_email_removed);
- }
-
- 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);
- }
- }
-
- public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
- out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
- Cancellable? cancellable = null) throws Error {
- low = null;
- high = null;
-
- // This shouldn't require a result_mutex lock since there's no yield.
- Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = Geary.traverse<Geary.EmailIdentifier>(ids)
- .cast_object<ImapDB.SearchEmailIdentifier>()
- .filter(id => id in search_results)
- .to_tree_set();
-
- if (in_folder.size > 0) {
- low = in_folder.first();
- high = in_folder.last();
- }
- }
-
- private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
- int result_mutex_token = yield result_mutex.claim_async();
-
- Error? error = null;
- try {
- yield do_search_async(query, ids, null, cancellable);
- } catch(Error e) {
- error = e;
- }
-
- result_mutex.release(ref result_mutex_token);
-
- if (error != null)
- throw error;
- }
-
- private void on_append_new_email_complete(Object? source, AsyncResult result) {
- try {
- append_new_email_async.end(result);
- } catch(Error e) {
- debug("Error appending new email to search results: %s", e.message);
- }
- }
-
- private void on_email_locally_complete(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> ids) {
- if (search_query != null)
- append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete);
- }
-
- private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
- int result_mutex_token = yield result_mutex.claim_async();
-
- Error? error = null;
- try {
- Gee.ArrayList<ImapDB.SearchEmailIdentifier> relevant_ids
- = Geary.traverse<Geary.EmailIdentifier>(ids)
- .map_nonnull<ImapDB.SearchEmailIdentifier>(
- id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id))
- .to_array_list();
-
- if (relevant_ids.size > 0)
- yield do_search_async(query, null, relevant_ids, cancellable);
- } catch(Error e) {
- error = e;
- }
-
- result_mutex.release(ref result_mutex_token);
-
- if (error != null)
- throw error;
- }
-
- private void on_handle_removed_email_complete(Object? source, AsyncResult result) {
- try {
- handle_removed_email_async.end(result);
- } catch(Error e) {
- debug("Error removing removed email from search results: %s", e.message);
- }
- }
-
- private void on_account_email_removed(Geary.Folder folder,
- Gee.Collection<Geary.EmailIdentifier> ids) {
- if (search_query != null)
- handle_removed_email_async.begin(search_query, folder, ids, null,
on_handle_removed_email_complete);
+ protected virtual void notify_search_query_changed(SearchQuery? query) {
+ search_query_changed(query);
}
/**
- * Clears the search query and results.
+ * Sets the keyword string for this search.
+ *
+ * This is a nonblocking call that initiates a background search which can be stopped with the
+ * supplied Cancellable.
+ *
+ * When the search is completed, { link search_query_changed} will be fired. It's possible for
+ * the { link search_query} property to change before completion.
*/
- public void clear() {
- Gee.Collection<ImapDB.SearchEmailIdentifier> local_results = search_results;
- clear_search_results();
- notify_email_removed(local_results);
- notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
-
- if (search_query != null) {
- search_query = null;
- search_query_changed(null);
- }
- }
+ public abstract void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable =
null);
/**
- * Sets the keyword string for this search.
+ * Clears the search query and results.
+ *
+ * { link search_query_changed} will be fired and { link search_query} will be set to null.
*/
- public void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = null) {
- set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
- }
-
- private void on_set_search_query_complete(Object? source, AsyncResult result) {
- try {
- set_search_query_async.end(result);
- } catch(Error e) {
- debug("Search error: %s", e.message);
- }
- }
-
- private async void set_search_query_async(string query, SearchQuery.Strategy strategy,
- Cancellable? cancellable) throws Error {
- Geary.SearchQuery search_query = account.open_search(query, strategy);
-
- int result_mutex_token = yield result_mutex.claim_async();
-
- Error? error = null;
- try {
- yield do_search_async(search_query, null, null, cancellable);
- } catch(Error e) {
- error = e;
- }
-
- result_mutex.release(ref result_mutex_token);
-
- this.search_query = search_query;
- search_query_changed(search_query);
-
- if (error != null)
- throw error;
- }
-
- // 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(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>?
add_ids,
- Gee.Collection<ImapDB.SearchEmailIdentifier>? 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<ImapDB.SearchEmailIdentifier> results
- = ImapDB.SearchEmailIdentifier.array_list_from_results(yield account.local_search_async(
- query, MAX_RESULT_EMAILS, 0, exclude_folders, add_ids ?? remove_ids, cancellable));
-
- Gee.List<ImapDB.SearchEmailIdentifier> added
- = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
- Gee.List<ImapDB.SearchEmailIdentifier> removed
- = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
-
- if (remove_ids == null) {
- added = Geary.traverse<ImapDB.SearchEmailIdentifier>(results)
- .filter(id => !(id in search_results))
- .to_array_list();
- }
- if (add_ids == null) {
- removed = Geary.traverse<ImapDB.SearchEmailIdentifier>(remove_ids ?? search_results)
- .filter(id => !(id in results))
- .to_array_list();
- }
-
- search_results.remove_all(removed);
- search_results.add_all(added);
-
- _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<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
- int count, Geary.Email.Field required_fields, 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(Folder.ListFlags.OLDEST_TO_NEWEST))
- initial_index = ids.length - 1;
-
- Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
- 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 symantics 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<Geary.Email>? list_email_by_sparse_id_async(
- Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
- Cancellable? cancellable = null) throws Error {
- // TODO: Fetch emails in a batch.
- Gee.List<Geary.Email> result = new Gee.ArrayList<Geary.Email>();
- 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<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 Error {
- return yield account.local_fetch_email_async(id, required_fields, cancellable);
- }
-
- public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
- Cancellable? cancellable = null) throws Error {
- Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders
- = yield account.get_containing_folders_async(email_ids, cancellable);
- if (ids_to_folders == null)
- return;
-
- 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 = yield account.fetch_folder_async(path, cancellable);
- Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
- if (remove == null)
- continue;
-
- Gee.Collection<Geary.EmailIdentifier> 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.FAST_OPEN, cancellable);
- open = true;
-
- yield remove.remove_email_async(
- Geary.Collection.to_array_list<Geary.EmailIdentifier>(ids), cancellable);
-
- yield folder.close_async(cancellable);
- open = false;
- } catch (Error e) {
- debug("Error removing messages in %s: %s", folder.to_string(), e.message);
-
- if (open) {
- try {
- yield folder.close_async(cancellable);
- open = false;
- } catch (Error e) {
- debug("Error closing folder %s: %s", folder.to_string(), e.message);
- }
- }
- }
- }
- }
+ public abstract void clear();
/**
* Given a list of mail IDs, returns a set of casefolded words that match for the current
* search query.
*/
- public async Gee.Set<string>? get_search_matches_async(
- Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
- if (search_query == null)
- return null;
-
- return yield account.get_search_matches_async(search_query, ids, cancellable);
- }
-
- private void exclude_folder(Geary.Folder folder) {
- exclude_folders.add(folder.path);
- }
-
- private void exclude_orphan_emails() {
- exclude_folders.add(null);
- }
-
- private void clear_search_results() {
- search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
- ImapDB.SearchEmailIdentifier.compare_descending);
- }
+ public abstract async Gee.Set<string>? get_search_matches_async(
+ Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
}
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index 9366ea0..9e29cfa 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -27,7 +27,7 @@ private class Geary.ImapDB.Account : BaseObject {
// Only available when the Account is opened
public SmtpOutboxFolder? outbox { get; private set; default = null; }
- public SearchFolder? search_folder { get; private set; default = null; }
+ public Geary.SearchFolder? search_folder { get; private set; default = null; }
public ImapEngine.ContactStore contact_store { get; private set; }
public IntervalProgressMonitor search_index_monitor { get; private set;
default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
diff --git a/src/engine/imap-db/imap-db-search-email-identifier.vala
b/src/engine/imap-db/search/imap-db-search-email-identifier.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-email-identifier.vala
rename to src/engine/imap-db/search/imap-db-search-email-identifier.vala
index fceb1ab..6df1bef 100644
--- a/src/engine/imap-db/imap-db-search-email-identifier.vala
+++ b/src/engine/imap-db/search/imap-db-search-email-identifier.vala
@@ -6,6 +6,14 @@
private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
Gee.Comparable<SearchEmailIdentifier> {
+ 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);
}
@@ -36,14 +44,6 @@ private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
return null;
}
- 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 override int natural_sort_comparator(Geary.EmailIdentifier o) {
ImapDB.SearchEmailIdentifier? other = o as ImapDB.SearchEmailIdentifier;
if (other == null)
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
new file mode 100644
index 0000000..4b64823
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder-properties.vala
@@ -0,0 +1,16 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+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-root.vala
b/src/engine/imap-db/search/imap-db-search-folder-root.vala
new file mode 100644
index 0000000..8b8109f
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder-root.vala
@@ -0,0 +1,14 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+private class Geary.ImapDB.SearchFolderRoot : Geary.FolderRoot {
+ public const string MAGIC_BASENAME = "$GearySearchFolder$";
+
+ public SearchFolderRoot() {
+ base(MAGIC_BASENAME, null, false, false);
+ }
+}
+
diff --git a/src/engine/imap-db/search/imap-db-search-folder.vala
b/src/engine/imap-db/search/imap-db-search-folder.vala
new file mode 100644
index 0000000..a48631f
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder.vala
@@ -0,0 +1,404 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+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;
+
+ 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<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
+ private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
+ private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
+
+ public SearchFolder(Geary.Account account) {
+ base (account, new SearchFolderProperties(0, 0), new SearchFolderRoot());
+
+ account.folders_available_unavailable.connect(on_folders_available_unavailable);
+ 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.email_locally_complete.disconnect(on_email_locally_complete);
+ account.email_removed.disconnect(on_account_email_removed);
+ }
+
+ 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);
+ }
+ }
+
+ public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
+ out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+ Cancellable? cancellable = null) throws Error {
+ low = null;
+ high = null;
+
+ // This shouldn't require a result_mutex lock since there's no yield.
+ Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = Geary.traverse<Geary.EmailIdentifier>(ids)
+ .cast_object<ImapDB.SearchEmailIdentifier>()
+ .filter(id => id in search_results)
+ .to_tree_set();
+
+ if (in_folder.size > 0) {
+ low = in_folder.first();
+ high = in_folder.last();
+ }
+ }
+
+ private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
+ int result_mutex_token = yield result_mutex.claim_async();
+
+ Error? error = null;
+ try {
+ yield do_search_async(query, ids, null, cancellable);
+ } catch(Error e) {
+ error = e;
+ }
+
+ result_mutex.release(ref result_mutex_token);
+
+ if (error != null)
+ throw error;
+ }
+
+ private void on_append_new_email_complete(Object? source, AsyncResult result) {
+ try {
+ append_new_email_async.end(result);
+ } catch(Error e) {
+ debug("Error appending new email to search results: %s", e.message);
+ }
+ }
+
+ private void on_email_locally_complete(Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> ids) {
+ if (search_query != null)
+ append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete);
+ }
+
+ private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
+ int result_mutex_token = yield result_mutex.claim_async();
+
+ Error? error = null;
+ try {
+ Gee.ArrayList<ImapDB.SearchEmailIdentifier> relevant_ids
+ = Geary.traverse<Geary.EmailIdentifier>(ids)
+ .map_nonnull<ImapDB.SearchEmailIdentifier>(
+ id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id))
+ .to_array_list();
+
+ if (relevant_ids.size > 0)
+ yield do_search_async(query, null, relevant_ids, cancellable);
+ } catch(Error e) {
+ error = e;
+ }
+
+ result_mutex.release(ref result_mutex_token);
+
+ if (error != null)
+ throw error;
+ }
+
+ private void on_handle_removed_email_complete(Object? source, AsyncResult result) {
+ try {
+ handle_removed_email_async.end(result);
+ } catch(Error e) {
+ debug("Error removing removed email from search results: %s", e.message);
+ }
+ }
+
+ private void on_account_email_removed(Geary.Folder folder,
+ Gee.Collection<Geary.EmailIdentifier> ids) {
+ if (search_query != null)
+ handle_removed_email_async.begin(search_query, folder, ids, null,
on_handle_removed_email_complete);
+ }
+
+ /**
+ * Clears the search query and results.
+ */
+ public override void clear() {
+ Gee.Collection<ImapDB.SearchEmailIdentifier> local_results = search_results;
+ clear_search_results();
+ notify_email_removed(local_results);
+ notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
+
+ if (search_query != null) {
+ search_query = null;
+ notify_search_query_changed(null);
+ }
+ }
+
+ /**
+ * Sets the keyword string for this search.
+ */
+ public override void search(string query, Geary.SearchQuery.Strategy strategy, Cancellable? cancellable
= null) {
+ set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
+ }
+
+ private void on_set_search_query_complete(Object? source, AsyncResult result) {
+ try {
+ set_search_query_async.end(result);
+ } catch(Error e) {
+ debug("Search error: %s", e.message);
+ }
+ }
+
+ private async void set_search_query_async(string query, Geary.SearchQuery.Strategy strategy,
+ Cancellable? cancellable) throws Error {
+ Geary.SearchQuery search_query = account.open_search(query, strategy);
+
+ int result_mutex_token = yield result_mutex.claim_async();
+
+ Error? error = null;
+ try {
+ yield do_search_async(search_query, null, null, cancellable);
+ } catch(Error e) {
+ error = e;
+ }
+
+ result_mutex.release(ref result_mutex_token);
+
+ this.search_query = search_query;
+ notify_search_query_changed(search_query);
+
+ if (error != null)
+ throw error;
+ }
+
+ // 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(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>?
add_ids,
+ Gee.Collection<ImapDB.SearchEmailIdentifier>? 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<ImapDB.SearchEmailIdentifier> results
+ = ImapDB.SearchEmailIdentifier.array_list_from_results(yield account.local_search_async(
+ query, MAX_RESULT_EMAILS, 0, exclude_folders, add_ids ?? remove_ids, cancellable));
+
+ Gee.List<ImapDB.SearchEmailIdentifier> added
+ = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
+ Gee.List<ImapDB.SearchEmailIdentifier> removed
+ = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
+
+ if (remove_ids == null) {
+ added = Geary.traverse<ImapDB.SearchEmailIdentifier>(results)
+ .filter(id => !(id in search_results))
+ .to_array_list();
+ }
+ if (add_ids == null) {
+ removed = Geary.traverse<ImapDB.SearchEmailIdentifier>(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<Geary.Email>? 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<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
+ 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 symantics 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<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 Error {
+ // TODO: Fetch emails in a batch.
+ Gee.List<Geary.Email> result = new Gee.ArrayList<Geary.Email>();
+ 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<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 Error {
+ return yield account.local_fetch_email_async(id, required_fields, cancellable);
+ }
+
+ public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ Cancellable? cancellable = null) throws Error {
+ Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders
+ = yield account.get_containing_folders_async(email_ids, cancellable);
+ if (ids_to_folders == null)
+ return;
+
+ 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 = yield account.fetch_folder_async(path, cancellable);
+ Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
+ if (remove == null)
+ continue;
+
+ Gee.Collection<Geary.EmailIdentifier> 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.FAST_OPEN, cancellable);
+ open = true;
+
+ yield remove.remove_email_async(
+ Geary.Collection.to_array_list<Geary.EmailIdentifier>(ids), cancellable);
+
+ yield folder.close_async(cancellable);
+ open = false;
+ } catch (Error e) {
+ debug("Error removing messages in %s: %s", folder.to_string(), e.message);
+
+ if (open) {
+ try {
+ yield folder.close_async(cancellable);
+ open = false;
+ } 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<string>? get_search_matches_async(
+ Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
+ if (search_query == null)
+ return null;
+
+ return yield account.get_search_matches_async(search_query, ids, cancellable);
+ }
+
+ private void exclude_folder(Geary.Folder folder) {
+ exclude_folders.add(folder.path);
+ }
+
+ private void exclude_orphan_emails() {
+ exclude_folders.add(null);
+ }
+
+ private void clear_search_results() {
+ search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
+ ImapDB.SearchEmailIdentifier.compare_descending);
+ }
+}
+
diff --git a/src/engine/imap-db/imap-db-search-query.vala
b/src/engine/imap-db/search/imap-db-search-query.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-query.vala
rename to src/engine/imap-db/search/imap-db-search-query.vala
diff --git a/src/engine/imap-db/imap-db-search-term.vala b/src/engine/imap-db/search/imap-db-search-term.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-term.vala
rename to src/engine/imap-db/search/imap-db-search-term.vala
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 c11f0c9..d71d2ae 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
@@ -7,11 +7,11 @@
/**
* Gmail-specific SearchFolder implementation.
*/
-private class Geary.ImapEngine.GmailSearchFolder : Geary.SearchFolder {
+private class Geary.ImapEngine.GmailSearchFolder : ImapDB.SearchFolder {
private Geary.App.EmailStore email_store;
public GmailSearchFolder(Geary.Account account) {
- base(account);
+ base (account);
email_store = new Geary.App.EmailStore(account);
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index d11f3b4..a8aa022 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -46,7 +46,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
}
if (search_path == null) {
- search_path = new SearchFolderRoot();
+ search_path = new ImapDB.SearchFolderRoot();
}
}
@@ -247,7 +247,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
// Subclasses with specific SearchFolder implementations should override
// this to return the correct subclass.
internal virtual SearchFolder new_search_folder() {
- return new SearchFolder(this);
+ return new ImapDB.SearchFolder(this);
}
private MinimalFolder build_folder(ImapDB.Folder local_folder) {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]