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.
This commit is contained in:
parent
b3488f6cf9
commit
924104c282
13 changed files with 778 additions and 617 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
|
||||
Gee.Comparable<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);
|
||||
}
|
||||
|
||||
public static Gee.ArrayList<SearchEmailIdentifier> array_list_from_results(
|
||||
Gee.Collection<Geary.EmailIdentifier>? results) {
|
||||
Gee.ArrayList<SearchEmailIdentifier> r = new Gee.ArrayList<SearchEmailIdentifier>();
|
||||
|
||||
if (results != null) {
|
||||
foreach (Geary.EmailIdentifier id in results) {
|
||||
SearchEmailIdentifier? search_id = id as SearchEmailIdentifier;
|
||||
|
||||
assert(search_id != null);
|
||||
r.add(search_id);
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
// Searches for a generic EmailIdentifier in a collection of SearchEmailIdentifiers.
|
||||
public static SearchEmailIdentifier? collection_get_email_identifier(
|
||||
Gee.Collection<SearchEmailIdentifier> collection, Geary.EmailIdentifier id) {
|
||||
foreach (SearchEmailIdentifier search_id in collection) {
|
||||
if (id.equal_to(search_id))
|
||||
return search_id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override int natural_sort_comparator(Geary.EmailIdentifier o) {
|
||||
ImapDB.SearchEmailIdentifier? other = o as ImapDB.SearchEmailIdentifier;
|
||||
if (other == null)
|
||||
return 1;
|
||||
|
||||
return compare_to(other);
|
||||
}
|
||||
|
||||
public virtual int compare_to(SearchEmailIdentifier other) {
|
||||
// if both have date received, compare on that, using stable sort if the same
|
||||
if (date_received != null && other.date_received != null) {
|
||||
int compare = date_received.compare(other.date_received);
|
||||
|
||||
return (compare != 0) ? compare : stable_sort_comparator(other);
|
||||
}
|
||||
|
||||
// if neither have date received, fall back on stable sort
|
||||
if (date_received == null && other.date_received == null)
|
||||
return stable_sort_comparator(other);
|
||||
|
||||
// put identifiers with no date ahead of those with
|
||||
return (date_received == null ? -1 : 1);
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "[%s/null/%s]".printf(message_id.to_string(),
|
||||
(date_received == null ? "null" : date_received.to_string()));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
private class Geary.ImapDB.SearchFolderProperties : Geary.FolderProperties {
|
||||
public SearchFolderProperties(int total, int unread) {
|
||||
base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
|
||||
}
|
||||
|
||||
public void set_total(int total) {
|
||||
this.email_total = total;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
private class Geary.ImapDB.SearchFolder : Geary.SearchFolder, Geary.FolderSupport.Remove {
|
||||
|
||||
|
||||
/** Max number of emails that can ever be in the folder. */
|
||||
public const int MAX_RESULT_EMAILS = 1000;
|
||||
|
||||
/** The canonical name of the search folder. */
|
||||
public const string MAGIC_BASENAME = "$GearySearchFolder$";
|
||||
|
||||
private const Geary.SpecialFolderType[] EXCLUDE_TYPES = {
|
||||
Geary.SpecialFolderType.SPAM,
|
||||
Geary.SpecialFolderType.TRASH,
|
||||
Geary.SpecialFolderType.DRAFTS,
|
||||
// Orphan emails (without a folder) are also excluded; see ctor.
|
||||
};
|
||||
|
||||
|
||||
private Gee.HashSet<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, FolderRoot root) {
|
||||
base(
|
||||
account,
|
||||
new SearchFolderProperties(0, 0),
|
||||
root.get_child(MAGIC_BASENAME, Trillian.TRUE)
|
||||
);
|
||||
|
||||
account.folders_available_unavailable.connect(on_folders_available_unavailable);
|
||||
account.folders_special_type.connect(on_folders_special_type);
|
||||
account.email_locally_complete.connect(on_email_locally_complete);
|
||||
account.email_removed.connect(on_account_email_removed);
|
||||
|
||||
clear_search_results();
|
||||
|
||||
// We always want to exclude emails that don't live anywhere from
|
||||
// search results.
|
||||
exclude_orphan_emails();
|
||||
}
|
||||
|
||||
~SearchFolder() {
|
||||
account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
|
||||
account.folders_special_type.disconnect(on_folders_special_type);
|
||||
account.email_locally_complete.disconnect(on_email_locally_complete);
|
||||
account.email_removed.disconnect(on_account_email_removed);
|
||||
}
|
||||
|
||||
private async void append_new_email_async(Geary.Folder folder,
|
||||
Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
GLib.Cancellable? cancellable)
|
||||
throws GLib.Error {
|
||||
int result_mutex_token = yield result_mutex.claim_async();
|
||||
|
||||
Error? error = null;
|
||||
try {
|
||||
yield do_search_async(ids, null, cancellable);
|
||||
} catch(Error e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
result_mutex.release(ref result_mutex_token);
|
||||
|
||||
if (error != null)
|
||||
throw error;
|
||||
}
|
||||
|
||||
private async void handle_removed_email_async(Geary.Folder folder,
|
||||
Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
GLib.Cancellable? cancellable)
|
||||
throws GLib.Error {
|
||||
int result_mutex_token = yield result_mutex.claim_async();
|
||||
|
||||
GLib.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(null, relevant_ids, cancellable);
|
||||
}
|
||||
} catch (GLib.Error e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
result_mutex.release(ref result_mutex_token);
|
||||
|
||||
if (error != null)
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the search query and results.
|
||||
*/
|
||||
public override void clear() {
|
||||
var local_results = this.search_results;
|
||||
clear_search_results();
|
||||
notify_email_removed(local_results);
|
||||
notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
|
||||
|
||||
if (this.query != null) {
|
||||
this.query = null;
|
||||
this.query_evaluation_complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keyword string for this search.
|
||||
*/
|
||||
public override async void search(Geary.SearchQuery query,
|
||||
GLib.Cancellable? cancellable = null)
|
||||
throws GLib.Error {
|
||||
int result_mutex_token = yield result_mutex.claim_async();
|
||||
|
||||
this.query = query;
|
||||
GLib.Error? error = null;
|
||||
try {
|
||||
yield do_search_async(null, null, cancellable);
|
||||
} catch(Error e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
result_mutex.release(ref result_mutex_token);
|
||||
|
||||
if (error != null) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.query_evaluation_complete();
|
||||
}
|
||||
|
||||
// NOTE: you must call this ONLY after locking result_mutex_token.
|
||||
// If both *_ids parameters are null, the results of this search are
|
||||
// considered to be the full new set. If non-null, the results are
|
||||
// considered to be a delta and are added or subtracted from the full set.
|
||||
// add_ids are new ids to search for, remove_ids are ids in our result set
|
||||
// that will be removed if this search doesn't turn them up.
|
||||
private async void do_search_async(Gee.Collection<Geary.EmailIdentifier>? add_ids,
|
||||
Gee.Collection<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<SearchEmailIdentifier> results =
|
||||
SearchEmailIdentifier.array_list_from_results(
|
||||
yield account.local_search_async(
|
||||
this.query,
|
||||
MAX_RESULT_EMAILS,
|
||||
0,
|
||||
exclude_folders,
|
||||
add_ids ?? remove_ids,
|
||||
cancellable
|
||||
)
|
||||
);
|
||||
|
||||
Gee.List<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 semantics from fetch
|
||||
if (!(err is EngineError.NOT_FOUND) && !(err is EngineError.INCOMPLETE_MESSAGE)) {
|
||||
fetch_err = err;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result_mutex.release(ref result_mutex_token);
|
||||
|
||||
if (fetch_err != null)
|
||||
throw fetch_err;
|
||||
|
||||
return (results.size == 0 ? null : results);
|
||||
}
|
||||
|
||||
public override async Gee.List<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.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(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 = account.get_folder(path);
|
||||
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.NONE, cancellable);
|
||||
open = true;
|
||||
yield remove.remove_email_async(
|
||||
Collection.copy(ids), cancellable
|
||||
);
|
||||
} finally {
|
||||
if (open) {
|
||||
try {
|
||||
yield folder.close_async();
|
||||
} catch (Error e) {
|
||||
debug("Error closing folder %s: %s", folder.to_string(), e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of mail IDs, returns a set of casefolded words that match for the current
|
||||
* search query.
|
||||
*/
|
||||
public override async Gee.Set<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, ids, cancellable
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private void include_folder(Geary.Folder folder) {
|
||||
this.exclude_folders.remove(folder.path);
|
||||
}
|
||||
|
||||
private void exclude_folder(Geary.Folder folder) {
|
||||
this.exclude_folders.add(folder.path);
|
||||
}
|
||||
|
||||
private void exclude_orphan_emails() {
|
||||
this.exclude_folders.add(null);
|
||||
}
|
||||
|
||||
private void clear_search_results() {
|
||||
this.search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
|
||||
SearchEmailIdentifier.compare_descending
|
||||
);
|
||||
}
|
||||
|
||||
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.append_new_email_async.begin(
|
||||
folder, ids, null,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.append_new_email_async.end(res);
|
||||
} catch (GLib.Error error) {
|
||||
this.account.report_problem(
|
||||
new Geary.AccountProblemReport(
|
||||
this.account.information, error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_account_email_removed(Geary.Folder folder,
|
||||
Gee.Collection<Geary.EmailIdentifier> ids) {
|
||||
if (this.query != null) {
|
||||
this.handle_removed_email_async.begin(
|
||||
folder, ids, null,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.handle_removed_email_async.end(res);
|
||||
} catch (GLib.Error error) {
|
||||
this.account.report_problem(
|
||||
new Geary.AccountProblemReport(
|
||||
this.account.information, error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Various associated state with a single term in a {@link ImapDB.SearchQuery}.
|
||||
*/
|
||||
|
||||
private class Geary.ImapDB.SearchTerm : BaseObject {
|
||||
/**
|
||||
* The original tokenized search term with minimal other processing performed.
|
||||
*
|
||||
* For example, punctuation might be removed, but no casefolding has occurred.
|
||||
*/
|
||||
public string original { get; private set; }
|
||||
|
||||
/**
|
||||
* The parsed tokenized search term.
|
||||
*
|
||||
* Casefolding and other normalizing text operations have been performed.
|
||||
*/
|
||||
public string parsed { get; private set; }
|
||||
|
||||
/**
|
||||
* The stemmed search term.
|
||||
*
|
||||
* Only used if stemming is being done ''and'' the stem is different than the {@link parsed}
|
||||
* term.
|
||||
*/
|
||||
public string? stemmed { get; private set; }
|
||||
|
||||
/**
|
||||
* A list of terms ready for binding to an SQLite statement.
|
||||
*
|
||||
* This should include prefix operators and quotes (i.e. ["party"] or [party*]). These texts
|
||||
* are guaranteed not to be null or empty strings.
|
||||
*/
|
||||
public Gee.List<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 SearchTerm(string original, string parsed, string? stemmed, string? sql_parsed, string? sql_stemmed) {
|
||||
this.original = original;
|
||||
this.parsed = parsed;
|
||||
this.stemmed = stemmed;
|
||||
|
||||
// for now, only two variations: the parsed string and the stemmed; since stem is usually
|
||||
// shorter (and will be first in the OR statement), include it first
|
||||
if (!String.is_empty(sql_stemmed))
|
||||
sql.add(sql_stemmed);
|
||||
|
||||
if (!String.is_empty(sql_parsed))
|
||||
sql.add(sql_parsed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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} */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
129
src/engine/search/search-email-identifier.vala
Normal file
129
src/engine/search/search-email-identifier.vala
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
546
src/engine/search/search-folder-impl.vala
Normal file
546
src/engine/search/search-folder-impl.vala
Normal file
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue