Move ImapDB-specific code out of Geary.SearchFolder

SearchFolder is now an abstract base class and all the IMAP-specific
search folder code is moved to imap-db/search (much like the Outbox
is in imap-db/outbox).

Conflicts:
	src/engine/api/geary-search-folder.vala
This commit is contained in:
Jim Nelson 2015-03-02 17:34:17 -08:00
parent 70bc00c623
commit 3694140bb7
11 changed files with 489 additions and 445 deletions

View file

@ -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

View file

@ -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,408 +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;
if (ids.size == 0)
return;
// This shouldn't require a result_mutex lock since there's no yield.
// These must be ImapDB.EmailIdentifiers (which may also be SearchEmailIdentifiers) to
// xlat them into SearchEmailIdentifiers
Gee.HashSet<ImapDB.EmailIdentifier> db_ids = Geary.traverse<Geary.EmailIdentifier>(ids)
.cast_object<ImapDB.EmailIdentifier>()
.to_hash_set();
if (db_ids.size == 0)
return;
// Translate all ImapDB.EmailIdentifiers to the SearchEmailIdentifiers in the search results
// ... must do this in order to get the SearchEmailIdentifier's ordering, which is different
// than other ImapDB.EmailIdentifiers
//
// TODO: A dictionary of message_id => SearchEmailIdentifier would be useful here
Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>();
foreach (ImapDB.EmailIdentifier db_id in db_ids) {
foreach (ImapDB.SearchEmailIdentifier search_id in search_results) {
if (search_id.message_id == db_id.message_id)
in_folder.add(search_id);
}
}
if (in_folder.size == 0)
return;
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 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);
}
protected virtual void notify_search_query_changed(SearchQuery? query) {
search_query_changed(query);
}
/**
* 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 search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = null) {
set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
}
public abstract void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = null);
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);
}
}
}
}
}
/**
* Clears the search query and results.
*
* {@link search_query_changed} will be fired and {@link search_query} will be set to null.
*/
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;
}

View file

@ -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); }

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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) {