[geary/mjog/121-search-tokenising: 75/75] Geary.SearchQuery: Allow client apps to build search queries
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/mjog/121-search-tokenising: 75/75] Geary.SearchQuery: Allow client apps to build search queries
- Date: Tue, 13 Oct 2020 07:16:53 +0000 (UTC)
commit d6dd3bb86a14450e4df7117e893cb5c860906291
Author: Michael Gratton <mike vee net>
Date: Mon Dec 16 23:17:55 2019 +1100
Geary.SearchQuery: Allow client apps to build search queries
Adds classes that allow building arbitrary query expressions and require
an instance to be provided to Geary.SearchQuery, to be set as a
property.
This enables query expressions to be parsed by clients instead of the
engine, in which ever whay they choose.
.../application/application-main-window.vala | 1 +
.../conversation-viewer/conversation-viewer.vala | 1 +
src/client/util/util-email.vala | 625 ++++++++++++++++++++-
src/engine/api/geary-account.vala | 5 +-
src/engine/api/geary-search-query.vala | 208 ++++++-
src/engine/imap-db/imap-db-search-query.vala | 3 +-
.../imap-engine/imap-engine-generic-account.vala | 5 +-
test/client/util/util-email-test.vala | 328 ++++++++++-
test/mock/mock-account.vala | 13 +-
test/mock/mock-search-query.vala | 6 +-
10 files changed, 1177 insertions(+), 18 deletions(-)
---
diff --git a/src/client/application/application-main-window.vala
b/src/client/application/application-main-window.vala
index 0e6a89e19..6c1e59aa3 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -959,6 +959,7 @@ public class Application.MainWindow :
var strategy = this.application.config.get_search_strategy();
try {
var query = yield context.account.new_search_query(
+ new Geary.SearchQuery.TextOperator(ALL, strategy, query_text),
query_text,
strategy,
cancellable
diff --git a/src/client/conversation-viewer/conversation-viewer.vala
b/src/client/conversation-viewer/conversation-viewer.vala
index eec5f6a45..5a8fb16be 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -415,6 +415,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
if (text.length >= 2) {
var strategy = this.config.get_search_strategy();
query = yield account.new_search_query(
+ new Geary.SearchQuery.TextOperator(ALL, strategy, text),
text,
strategy,
cancellable
diff --git a/src/client/util/util-email.vala b/src/client/util/util-email.vala
index a18a45cfb..9166ae332 100644
--- a/src/client/util/util-email.vala
+++ b/src/client/util/util-email.vala
@@ -1,7 +1,9 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * 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.
+ * (version 2.1 or later). See the COPYING file in this distribution.
*/
namespace Util.Email {
@@ -296,3 +298,622 @@ namespace Util.Email {
}
}
+
+
+/**
+ * Parses a human-entered email query string as a query expression.
+ *
+ * @see Geary.SearchQuery.Operator
+ */
+public class Util.Email.SearchExpressionFactory : Geary.BaseObject {
+
+
+ private const unichar OPERATOR_SEPARATOR = ':';
+ private const string OPERATOR_TEMPLATE = "%s:%s";
+
+
+ private delegate Geary.SearchQuery.Operator? OperatorFactory(
+ string value,
+ bool is_quoted
+ );
+
+
+ private class FactoryContext {
+
+
+ public unowned OperatorFactory factory;
+
+
+ public FactoryContext(OperatorFactory factory) {
+ this.factory = factory;
+ }
+
+ }
+
+
+ private class Tokeniser {
+
+
+ // These characters are chosen for being commonly used to
+ // continue a single word (such as extended last names,
+ // i.e. "Lars-Eric") or in terms commonly searched for in an
+ // email client, i.e. unadorned mailbox addresses. Note that
+ // characters commonly used for wildcards or that would be
+ // interpreted as wildcards by SQLite are not included here.
+ private const unichar[] CONTINUATION_CHARS = {
+ '-', '_', '.', '@'
+ };
+
+ public bool has_next {
+ get { return (this.current_pos < this.query.length); }
+ }
+
+ public bool is_at_word {
+ get { return (this.attrs[this.current_c].is_word_start == 1); }
+ }
+
+ public bool is_at_quote {
+ get { return (this.c == '"'); }
+ }
+
+ public unichar current_character { get { return this.c; } }
+
+
+ private string query;
+ private int current_pos = -1;
+ private int next_pos = 0;
+
+ private unichar c = 0;
+ private int current_c = -1;
+ private Pango.LogAttr[] attrs;
+
+
+ public Tokeniser(string query, Pango.Language language) {
+ this.query = query;
+
+ // Break up search string into individual words and/or
+ // operators. Can't simply break on space or non-alphanumeric
+ // chars since some languages don't use spaces, so use Pango
+ // for its support for the Unicode UAX #29 word boundary spec.
+ this.attrs = new Pango.LogAttr[query.char_count() + 1];
+ Pango.get_log_attrs(
+ query, query.length, -1, language, this.attrs
+ );
+
+ consume_char();
+ }
+
+ public void consume_char() {
+ var current_pos = this.next_pos;
+ if (this.query.get_next_char(ref this.next_pos, out this.c)) {
+ this.current_c++;
+ }
+ this.current_pos = current_pos;
+ }
+
+ public void skip_to_next() {
+ while (this.has_next && !this.is_at_quote && !this.is_at_word) {
+ consume_char();
+ }
+ }
+
+ public string consume_word() {
+ var start = this.current_pos;
+ // the attr.is_word_end value applies to the first char
+ // after then end of a word, so need to move one past the
+ // end of the current word to determine where it ends
+ consume_char();
+ while (this.has_next &&
+ (this.c in CONTINUATION_CHARS ||
+ this.attrs[this.current_c].is_word_end != 1)) {
+ consume_char();
+ }
+ return this.query.slice(start, this.current_pos);
+ }
+
+ public string consume_quote() {
+ consume_char(); // skip the leading quote
+ var start = this.current_pos;
+ var last_c = this.c;
+ while (this.has_next && (this.c != '"' || last_c == '\\')) {
+ consume_char();
+ }
+ var quote = this.query.slice(start, this.current_pos);
+ consume_char(); // skip the trailing quote
+ return quote;
+ }
+
+ }
+
+
+ public Application.Configuration config { get; private set; }
+
+ public Geary.AccountInformation account { get; private set; }
+
+ public Pango.Language language {
+ get; set; default = Pango.Language.get_default();
+ }
+
+ // Maps of localised search operator names and values to their
+ // internal forms
+ private Gee.Map<string,FactoryContext> text_operators =
+ new Gee.HashMap<string,FactoryContext>();
+ private Gee.Map<string,FactoryContext> boolean_operators =
+ new Gee.HashMap<string,FactoryContext>();
+ private Gee.Set<string> search_op_to_me = new Gee.HashSet<string>();
+ private Gee.Set<string> search_op_from_me = new Gee.HashSet<string>();
+
+
+ public SearchExpressionFactory(Application.Configuration config,
+ Geary.AccountInformation account) {
+ this.config = config;
+ this.account = account;
+ construct_factories();
+ }
+
+ /** Constructs a search expression from the given query string. */
+ public Geary.SearchQuery.Operator parse_query(string query) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ var tokens = new Tokeniser(query, this.language);
+ while (tokens.has_next) {
+ if (tokens.is_at_word) {
+ Geary.SearchQuery.Operator? op = null;
+ var word = tokens.consume_word();
+ if (tokens.current_character == OPERATOR_SEPARATOR &&
+ tokens.has_next) {
+ op = new_extended_operator(word, tokens);
+ }
+ if (op == null) {
+ op = new_text_all_operator(word, false);
+ }
+ operands.add(op);
+ } else if (tokens.is_at_quote) {
+ operands.add(
+ new_text_all_operator(tokens.consume_quote(), true)
+ );
+ } else {
+ tokens.skip_to_next();
+ }
+ }
+
+ return (
+ operands.size == 1
+ ? operands[0]
+ : new Geary.SearchQuery.AndOperator(operands)
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_extended_operator(string name,
+ Tokeniser tokens) {
+ Geary.SearchQuery.Operator? op = null;
+
+ // consume the ':'
+ tokens.consume_char();
+
+ bool is_quoted = false;
+ string? value = null;
+ if (tokens.is_at_word) {
+ value = tokens.consume_word();
+ } else if (tokens.is_at_quote) {
+ value = tokens.consume_quote();
+ is_quoted = true;
+ }
+
+ FactoryContext? context = null;
+ if (value != null) {
+ context = this.text_operators[name];
+ if (context == null) {
+ context = this.boolean_operators[
+ OPERATOR_TEMPLATE.printf(name, value)
+ ];
+ }
+ }
+
+ if (context != null) {
+ op = context.factory(value, is_quoted);
+ }
+
+ if (op == null) {
+ // Still no operator, so the name or value must have been
+ // invalid. Repair by treating each as separate ops, if
+ // present.
+ op = new_text_all_operator(name, false);
+ if (value != null) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ operands.add(op);
+ operands.add(new_text_all_operator(value, is_quoted));
+ op = new Geary.SearchQuery.AndOperator(operands);
+ }
+ }
+
+ return op;
+ }
+
+ private inline Geary.SearchQuery.Strategy get_matching_strategy(bool is_quoted) {
+ return (
+ is_quoted
+ ? Geary.SearchQuery.Strategy.EXACT
+ : this.config.get_search_strategy()
+ );
+ }
+
+ private string[] get_account_addresses() {
+ Gee.List<Geary.RFC822.MailboxAddress>? mailboxes =
+ this.account.sender_mailboxes;
+ var addresses = new string[mailboxes != null ? mailboxes.size : 0];
+ if (mailboxes != null) {
+ for (int i = 0; i < mailboxes.size; i++) {
+ addresses[i] = mailboxes[i].address;
+ }
+ }
+ return addresses;
+ }
+
+ private void construct_factories() {
+ // Maps of possibly translated search operator names and values
+ // to English/internal names and values. We include the
+ // English version anyway so that when translations provide a
+ // localised version of the operator names but have not also
+ // translated the user manual, the English version in the
+ // manual still works.
+
+ // Text operators
+ ///////////////////////////////////////////////////////////
+
+ FactoryContext attachment_name = new FactoryContext(
+ this.new_text_attachment_name_operator
+ );
+ this.text_operators.set("attachment", attachment_name);
+ /// Translators: Can be typed in the search box like
+ /// "attachment:file.txt" to find messages with attachments
+ /// with a particular name.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "attachment"),
+ attachment_name);
+
+ FactoryContext bcc = new FactoryContext(this.new_text_bcc_operator);
+ this.text_operators.set("bcc", bcc);
+ /// Translators: Can be typed in the search box like
+ /// "bcc:johndoe example com" to find messages bcc'd to a
+ /// particular person.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "bcc"), bcc);
+
+ FactoryContext body = new FactoryContext(this.new_text_body_operator);
+ this.text_operators.set("body", body);
+ /// Translators: Can be typed in the search box like
+ /// "body:word" to find "word" only if it occurs in the body
+ /// of a message.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "body"), body);
+
+ FactoryContext cc = new FactoryContext(this.new_text_cc_operator);
+ this.text_operators.set("cc", cc);
+ /// Translators: Can be typed in the search box like
+ /// "cc:johndoe example com" to find messages cc'd to a
+ /// particular person.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "cc"), cc);
+
+ FactoryContext from = new FactoryContext(this.new_text_from_operator);
+ this.text_operators.set("from", from);
+ /// Translators: Can be typed in the search box like
+ /// "from:johndoe example com" to find messages from a
+ /// particular sender.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "from"), from);
+
+ FactoryContext subject = new FactoryContext(
+ this.new_text_subject_operator
+ );
+ this.text_operators.set("subject", subject);
+ /// Translators: Can be typed in the search box like
+ /// "subject:word" to find "word" only if it occurs in the
+ /// subject of a message.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "subject"), subject);
+
+ FactoryContext to = new FactoryContext(this.new_text_to_operator);
+ this.text_operators.set("to", to);
+ /// Translators: Can be typed in the search box like
+ /// "to:johndoe example com" to find messages received by a
+ /// particular person.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.text_operators.set(C_("Search operator", "to"), to);
+
+ /// Translators: Can be typed in the search box after "to:",
+ /// "cc:" and "bcc:" e.g.: "to:me". Matches conversations that
+ /// are addressed to the user.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.search_op_to_me.add(
+ C_("Search operator value - mail addressed to the user", "me")
+ );
+ this.search_op_to_me.add("me");
+
+ /// Translators: Can be typed in the search box after "from:"
+ /// i.e.: "from:me". Matches conversations were sent by the
+ /// user.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ this.search_op_from_me.add(
+ C_("Search operator value - mail sent by the user", "me")
+ );
+ this.search_op_from_me.add("me");
+
+ // Boolean operators
+ ///////////////////////////////////////////////////////////
+
+ /// Translators: Can be typed in the search box like
+ /// "is:unread" to find messages that are read, unread, or
+ /// starred.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ string bool_is_name = C_("Search operator", "is");
+
+ /// Translators: Can be typed in the search box after "is:"
+ /// i.e.: "is:unread". Matches conversations that are flagged
+ /// unread.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ string bool_is_unread_value = C_("'is:' search operator value", "unread");
+
+ /// Translators: Can be typed in the search box after "is:"
+ /// i.e.: "is:read". Matches conversations that are flagged as
+ /// read.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ string bool_is_read_value = C_("'is:' search operator value", "read");
+
+ /// Translators: Can be typed in the search box after "is:"
+ /// i.e.: "is:starred". Matches conversations that are flagged
+ /// as starred.
+ ///
+ /// The translated string must be a single word (use '-', '_'
+ /// or similar to combine words into one), should be short,
+ /// and also match the translation in "search.page" of the
+ /// Geary User Guide.
+ string bool_is_starred_value = C_("'is:' search operator value", "starred");
+
+ FactoryContext is_unread = new FactoryContext(
+ this.new_boolean_unread_operator
+ );
+ this.boolean_operators.set("is:unread", is_unread);
+ this.boolean_operators.set(
+ OPERATOR_TEMPLATE.printf(
+ bool_is_name, bool_is_unread_value
+ ), is_unread
+ );
+
+ FactoryContext is_read = new FactoryContext(
+ this.new_boolean_read_operator
+ );
+ this.boolean_operators.set("is:read", is_read);
+ this.boolean_operators.set(
+ OPERATOR_TEMPLATE.printf(
+ bool_is_name, bool_is_read_value
+ ), is_read
+ );
+
+ FactoryContext is_starred = new FactoryContext(
+ this.new_boolean_starred_operator
+ );
+ this.boolean_operators.set("is:starred", is_starred);
+ this.boolean_operators.set(
+ OPERATOR_TEMPLATE.printf(
+ bool_is_name, bool_is_starred_value
+ ), is_starred
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_text_all_operator(
+ string value, bool is_quoted
+ ) {
+ return new Geary.SearchQuery.TextOperator(
+ ALL, get_matching_strategy(is_quoted), value
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_text_attachment_name_operator(
+ string value, bool is_quoted
+ ) {
+ return new Geary.SearchQuery.TextOperator(
+ ATTACHMENT_NAME, get_matching_strategy(is_quoted), value
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_text_bcc_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted && value in this.search_op_to_me) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ foreach (var address in get_account_addresses()) {
+ operands.add(
+ new Geary.SearchQuery.TextOperator(BCC, EXACT, address)
+ );
+ }
+ op = (
+ operands.size == 1
+ ? operands[0]
+ : new Geary.SearchQuery.OrOperator(operands)
+ );
+ } else {
+ op = new Geary.SearchQuery.TextOperator(
+ BCC, get_matching_strategy(is_quoted), value
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_text_body_operator(
+ string value, bool is_quoted
+ ) {
+ return new Geary.SearchQuery.TextOperator(
+ BODY, get_matching_strategy(is_quoted), value
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_text_cc_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted && value in this.search_op_to_me) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ foreach (var address in get_account_addresses()) {
+ operands.add(
+ new Geary.SearchQuery.TextOperator(CC, EXACT, address)
+ );
+ }
+ op = (
+ operands.size == 1
+ ? operands[0]
+ : new Geary.SearchQuery.OrOperator(operands)
+ );
+ } else {
+ op = new Geary.SearchQuery.TextOperator(
+ CC, get_matching_strategy(is_quoted), value
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_text_from_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted && value in this.search_op_from_me) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ foreach (var address in get_account_addresses()) {
+ operands.add(
+ new Geary.SearchQuery.TextOperator(FROM, EXACT, address)
+ );
+ }
+ op = (
+ operands.size == 1
+ ? operands[0]
+ : new Geary.SearchQuery.OrOperator(operands)
+ );
+ } else {
+ op = new Geary.SearchQuery.TextOperator(
+ FROM, get_matching_strategy(is_quoted), value
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_text_subject_operator(
+ string value, bool is_quoted
+ ) {
+ return new Geary.SearchQuery.TextOperator(
+ SUBJECT, get_matching_strategy(is_quoted), value
+ );
+ }
+
+ private Geary.SearchQuery.Operator? new_text_to_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted && value in this.search_op_to_me) {
+ var operands = new Gee.LinkedList<Geary.SearchQuery.Operator>();
+ foreach (var address in get_account_addresses()) {
+ operands.add(
+ new Geary.SearchQuery.TextOperator(TO, EXACT, address)
+ );
+ }
+ op = (
+ operands.size == 1
+ ? operands[0]
+ : new Geary.SearchQuery.OrOperator(operands)
+ );
+ } else {
+ op = new Geary.SearchQuery.TextOperator(
+ TO, get_matching_strategy(is_quoted), value
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_boolean_unread_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted) {
+ op = new Geary.SearchQuery.BooleanOperator(
+ IS_UNREAD,
+ true
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_boolean_read_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted) {
+ op = new Geary.SearchQuery.BooleanOperator(
+ IS_UNREAD,
+ false
+ );
+ }
+ return op;
+ }
+
+ private Geary.SearchQuery.Operator? new_boolean_starred_operator(
+ string value, bool is_quoted
+ ) {
+ Geary.SearchQuery.Operator? op = null;
+ if (!is_quoted) {
+ op = new Geary.SearchQuery.BooleanOperator(
+ IS_FLAGGED,
+ true
+ );
+ }
+ return op;
+ }
+
+}
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index d81661cae..03bcc719d 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -512,7 +512,7 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
) throws GLib.Error;
/**
- * Create a new {@link SearchQuery} for this {@link Account}.
+ * Create a new search query for this account.
*
* See {@link Geary.SearchQuery.Strategy} for more information about how its interpreted by the
* Engine. In particular, note that it's an advisory parameter only and may have no effect,
@@ -525,7 +525,8 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
* The resulting object can only be used with calls into this
* account instance.
*/
- public abstract async SearchQuery new_search_query(string query,
+ public abstract async SearchQuery new_search_query(SearchQuery.Operator expression,
+ string text,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error;
diff --git a/src/engine/api/geary-search-query.vala b/src/engine/api/geary-search-query.vala
index 0a15ff2ea..46ab1f98a 100644
--- a/src/engine/api/geary-search-query.vala
+++ b/src/engine/api/geary-search-query.vala
@@ -58,9 +58,211 @@ public abstract class Geary.SearchQuery : BaseObject {
}
+ /**
+ * Base class for search term operators.
+ */
+ public abstract class Operator : BaseObject {
+
+ /** Returns a string representation, for debugging. */
+ public abstract string to_string();
+
+ }
+
+ /**
+ * Conjunction search operator, true if all operands are true.
+ */
+ public class AndOperator : Operator {
+
+
+ private Gee.Collection<Operator> operands;
+
+
+ public AndOperator(Gee.Collection<Operator> operands) {
+ this.operands = operands;
+ }
+
+ public Gee.Collection<Operator> get_operands() {
+ return this.operands.read_only_view;
+ }
+
+ public override string to_string() {
+ var builder = new GLib.StringBuilder("AND(");
+ var iter = this.operands.iterator();
+ if (iter.next()) {
+ builder.append(iter.get().to_string());
+ }
+ while (iter.next()) {
+ builder.append_c(',');
+ builder.append(iter.get().to_string());
+ }
+ builder.append_c(')');
+ return builder.str;
+ }
+
+ }
+
+ /**
+ * Disjunction search operator, true if any operands are true.
+ */
+ public class OrOperator : Operator {
+
+
+ private Gee.Collection<Operator> operands;
+
+
+ public OrOperator(Gee.Collection<Operator> operands) {
+ this.operands = operands;
+ }
+
+ public Gee.Collection<Operator> get_operands() {
+ return this.operands.read_only_view;
+ }
+
+ public override string to_string() {
+ var builder = new GLib.StringBuilder("OR(");
+ var iter = this.operands.iterator();
+ if (iter.next()) {
+ builder.append(iter.get().to_string());
+ }
+ while (iter.next()) {
+ builder.append_c(',');
+ builder.append(iter.get().to_string());
+ }
+ builder.append_c(')');
+ return builder.str;
+ }
+
+ }
+
+ /**
+ * Negation search operator, true if the operand is false.
+ */
+ public class NotOperator : Operator {
+
+
+ private Operator operand;
+
+
+ public NotOperator(Operator operand) {
+ this.operand = operand;
+ }
+
+ public override string to_string() {
+ return "NOT(%s)".printf(operand.to_string());
+ }
+
+ }
+
+ /**
+ * Text email property operator, true if it matches the given term.
+ */
+ public class TextOperator : Operator {
+
+
+ /**
+ * Supported text email properties that can be queried.
+ *
+ * @see TextOperator
+ */
+ public enum Property {
+ /** Search for a term in all supported properties. */
+ ALL,
+
+ /** Search for a term in the To field. */
+ TO,
+
+ /** Search for a term in the Bcc field. */
+ BCC,
+
+ /** Search for a term in the Cc field. */
+ CC,
+
+ /** Search for a term in the From field. */
+ FROM,
+
+ /** Search for a term in the email subject. */
+ SUBJECT,
+
+ /** Search for a term in the email body. */
+ BODY,
+
+ /** Search for a term in email attachment names. */
+ ATTACHMENT_NAME;
+ }
+
+
+ public Property target { get; private set; }
+ public Strategy matching_strategy { get; private set; }
+ public string term { get; private set; }
+
+
+ public TextOperator(Property target,
+ Strategy matching_strategy,
+ string term) {
+ this.target = target;
+ this.matching_strategy = matching_strategy;
+ this.term = term;
+ }
+
+ public override string to_string() {
+ return "%s:%s(%s)".printf(
+ ObjectUtils.to_enum_nick(typeof(Property), this.target).up(),
+ ObjectUtils.to_enum_nick(typeof(Strategy), this.target).up(),
+ this.term
+ );
+ }
+
+ }
+
+
+ /**
+ * Boolean email property operator, true if it matches the given value.
+ */
+ public class BooleanOperator : Operator {
+
+
+ /**
+ * Supported Boolean email properties that can be queried.
+ *
+ * @see BooleanOperator
+ */
+ public enum Property {
+ /** If the email is unread. */
+ IS_UNREAD,
+
+ /** If the email is flagged. */
+ IS_FLAGGED;
+
+ }
+
+
+ public Property target { get; private set; }
+ public bool value { get; private set; }
+
+
+ public BooleanOperator(Property target, bool value) {
+ this.target = target;
+ this.value = value;
+ }
+
+ public override string to_string() {
+ return "%s(%s)".printf(
+ ObjectUtils.to_enum_nick(typeof(Property), this.target).up(),
+ this.value.to_string()
+ );
+ }
+
+ }
+
+
/** The account that owns this query. */
public Account owner { get; private set; }
+ /**
+ * The search expression to be evaluated.
+ */
+ public Operator expression { get; private set; }
+
/**
* The original search text.
*
@@ -75,15 +277,17 @@ public abstract class Geary.SearchQuery : BaseObject {
protected SearchQuery(Account owner,
+ Operator expression,
string raw,
Strategy strategy) {
this.owner = owner;
+ this.expression = expression;
this.raw = raw;
this.strategy = strategy;
}
public string to_string() {
- return "\"%s\" (%s)".printf(this.raw, this.strategy.to_string());
+ return "\"%s\" (%s)".printf(this.raw, this.expression.to_string());
}
-}
+}
diff --git a/src/engine/imap-db/imap-db-search-query.vala b/src/engine/imap-db/imap-db-search-query.vala
index 85f556fa8..b3734ad1d 100644
--- a/src/engine/imap-db/imap-db-search-query.vala
+++ b/src/engine/imap-db/imap-db-search-query.vala
@@ -320,10 +320,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
public async SearchQuery(Geary.Account owner,
ImapDB.Account local,
+ Geary.SearchQuery.Operator expression,
string query,
Geary.SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable) {
- base(owner, query, strategy);
+ base(owner, expression, query, strategy);
this.account = local;
switch (strategy) {
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index ef1ba7b4d..80cecf76b 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -572,12 +572,13 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
}
/** {@inheritDoc} */
- public override async SearchQuery new_search_query(string query,
+ public override async SearchQuery new_search_query(SearchQuery.Operator expression,
+ string text,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error {
return yield new ImapDB.SearchQuery(
- this, local, query, strategy, cancellable
+ this, local, expression, text, strategy, cancellable
);
}
diff --git a/test/client/util/util-email-test.vala b/test/client/util/util-email-test.vala
index fb3c365f5..86b58c184 100644
--- a/test/client/util/util-email-test.vala
+++ b/test/client/util/util-email-test.vala
@@ -7,14 +7,45 @@
public class Util.Email.Test : TestCase {
+
+ private Application.Configuration? config = null;
+ private Geary.AccountInformation? account = null;
+
+
public Test() {
- base("UtilEmailTest");
+ base("Util.Email.Test");
add_test("null_originator", null_originator);
add_test("from_originator", from_originator);
add_test("sender_originator", sender_originator);
add_test("reply_to_originator", reply_to_originator);
add_test("reply_to_via_originator", reply_to_via_originator);
add_test("plain_via_originator", plain_via_originator);
+ add_test("empty_search_query", empty_search_query);
+ add_test("plain_search_terms", plain_search_terms);
+ add_test("continuation_search_terms", continuation_search_terms);
+ add_test("i18n_search_terms", i18n_search_terms);
+ add_test("multiple_search_terms", multiple_search_terms);
+ add_test("quoted_search_terms", quoted_search_terms);
+ add_test("text_op_terms", text_op_terms);
+ add_test("text_op_single_me_terms", text_op_single_me_terms);
+ add_test("text_op_multiple_me_terms", text_op_multiple_me_terms);
+ add_test("boolean_op_terms", boolean_op_terms);
+ add_test("invalid_op_terms", invalid_op_terms);
+ }
+
+ public override void set_up() {
+ this.config = new Application.Configuration(Application.Client.SCHEMA_ID);
+ this.account = new Geary.AccountInformation(
+ "test",
+ OTHER,
+ new Mock.CredentialsMediator(),
+ new Geary.RFC822.MailboxAddress("test", "test example com")
+ );
+ }
+
+ public override void tear_down() {
+ this.config = null;
+ this.account = null;
}
public void null_originator() throws GLib.Error {
@@ -95,6 +126,301 @@ public class Util.Email.Test : TestCase {
assert_equal(originator.address, "bot example com");
}
+ public void empty_search_query() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var empty = test_article.parse_query("");
+ assert_true(empty is Geary.SearchQuery.AndOperator);
+ var and = empty as Geary.SearchQuery.AndOperator;
+ assert_true(and.get_operands().is_empty);
+ }
+
+ public void plain_search_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple1 = test_article.parse_query("hello");
+ assert_true(simple1 is Geary.SearchQuery.TextOperator);
+ var text1 = simple1 as Geary.SearchQuery.TextOperator;
+ assert_true(text1.target == ALL);
+ assert_true(text1.matching_strategy == CONSERVATIVE);
+ assert_equal(text1.term, "hello");
+
+ var simple2 = test_article.parse_query("h");
+ assert_true(simple2 is Geary.SearchQuery.TextOperator);
+ var text2 = simple2 as Geary.SearchQuery.TextOperator;
+ assert_equal(text2.term, "h");
+
+ var simple3 = test_article.parse_query(" h");
+ assert_true(simple3 is Geary.SearchQuery.TextOperator);
+ var text3 = simple3 as Geary.SearchQuery.TextOperator;
+ assert_equal(text3.term, "h");
+
+ var simple4 = test_article.parse_query("h ");
+ assert_true(simple4 is Geary.SearchQuery.TextOperator);
+ var text4 = simple4 as Geary.SearchQuery.TextOperator;
+ assert_equal(text4.term, "h");
+ }
+
+ public void continuation_search_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple1 = test_article.parse_query("hello-there");
+ assert_true(simple1 is Geary.SearchQuery.TextOperator);
+ var text1 = simple1 as Geary.SearchQuery.TextOperator;
+ assert_equal("hello-there", text1.term);
+
+ var simple2 = test_article.parse_query("hello-");
+ assert_true(simple2 is Geary.SearchQuery.TextOperator);
+ var text2 = simple2 as Geary.SearchQuery.TextOperator;
+ assert_equal(text2.term, "hello-");
+
+ var simple3 = test_article.parse_query("test example com");
+ assert_true(simple3 is Geary.SearchQuery.TextOperator);
+ var text3 = simple3 as Geary.SearchQuery.TextOperator;
+ assert_equal(text3.term, "test example com");
+ }
+
+ public void i18n_search_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(
+ this.config, this.account
+ );
+ test_article.language = Pango.Language.from_string("th");
+
+ var multiple = test_article.parse_query("ภาษาไทย");
+ assert_true(multiple is Geary.SearchQuery.AndOperator);
+ var and = multiple as Geary.SearchQuery.AndOperator;
+
+ var operands = and.get_operands().to_array();
+ assert_equal<int?>(operands.length, 2);
+ assert_true(operands[0] is Geary.SearchQuery.TextOperator);
+ assert_true(operands[1] is Geary.SearchQuery.TextOperator);
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[0]).term,
+ "ภาษา"
+ );
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[1]).term,
+ "ไทย"
+ );
+ }
+
+ public void multiple_search_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var multiple = test_article.parse_query("hello there");
+ assert_true(multiple is Geary.SearchQuery.AndOperator);
+ var and = multiple as Geary.SearchQuery.AndOperator;
+
+ var operands = and.get_operands().to_array();
+ assert_equal<int?>(operands.length, 2);
+ assert_true(operands[0] is Geary.SearchQuery.TextOperator);
+ assert_true(operands[1] is Geary.SearchQuery.TextOperator);
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[0]).term,
+ "hello"
+ );
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[1]).term,
+ "there"
+ );
+ }
+
+ public void quoted_search_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple1 = test_article.parse_query("\"hello\"");
+ assert_true(simple1 is Geary.SearchQuery.TextOperator);
+ var text1 = simple1 as Geary.SearchQuery.TextOperator;
+ assert_true(text1.target == ALL);
+ assert_true(text1.matching_strategy == EXACT);
+ assert_equal(text1.term, "hello");
+
+ var simple2 = test_article.parse_query("\"h\"");
+ assert_true(simple2 is Geary.SearchQuery.TextOperator);
+ var text2 = simple2 as Geary.SearchQuery.TextOperator;
+ assert_equal(text2.term, "h");
+
+ var simple3 = test_article.parse_query(" \"h\"");
+ assert_true(simple3 is Geary.SearchQuery.TextOperator);
+ var text3 = simple3 as Geary.SearchQuery.TextOperator;
+ assert_equal(text3.term, "h");
+
+ var simple4 = test_article.parse_query("\"h");
+ assert_true(simple4 is Geary.SearchQuery.TextOperator);
+ var text4 = simple4 as Geary.SearchQuery.TextOperator;
+ assert_equal(text4.term, "h");
+
+ var simple5 = test_article.parse_query("\"h\" ");
+ assert_true(simple5 is Geary.SearchQuery.TextOperator);
+ var text5 = simple5 as Geary.SearchQuery.TextOperator;
+ assert_equal(text5.term, "h");
+
+ var simple6 = test_article.parse_query("\"hello there\"");
+ assert_true(simple6 is Geary.SearchQuery.TextOperator);
+ var text6 = simple6 as Geary.SearchQuery.TextOperator;
+ assert_equal(text6.term, "hello there");
+ }
+
+ public void text_op_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple_body = test_article.parse_query("body:hello");
+ assert_true(simple_body is Geary.SearchQuery.TextOperator);
+ var text_body = simple_body as Geary.SearchQuery.TextOperator;
+ assert_true(text_body.target == BODY);
+ assert_true(text_body.matching_strategy == CONSERVATIVE);
+ assert_equal(text_body.term, "hello");
+
+ var simple_body_quoted = test_article.parse_query("body:\"hello\"");
+ assert_true(simple_body_quoted is Geary.SearchQuery.TextOperator);
+ var text_body_quoted = simple_body_quoted as Geary.SearchQuery.TextOperator;
+ assert_true(text_body_quoted.target == BODY);
+ assert_true(text_body_quoted.matching_strategy == EXACT);
+ assert_equal(text_body_quoted.term, "hello");
+
+ var simple_attach_name = test_article.parse_query("attachment:hello");
+ assert_true(simple_attach_name is Geary.SearchQuery.TextOperator);
+ var text_attch_name = simple_attach_name as Geary.SearchQuery.TextOperator;
+ assert_true(text_attch_name.target == ATTACHMENT_NAME);
+
+ var simple_bcc = test_article.parse_query("bcc:hello");
+ assert_true(simple_bcc is Geary.SearchQuery.TextOperator);
+ var text_bcc = simple_bcc as Geary.SearchQuery.TextOperator;
+ assert_true(text_bcc.target == BCC);
+
+ var simple_cc = test_article.parse_query("cc:hello");
+ assert_true(simple_cc is Geary.SearchQuery.TextOperator);
+ var text_cc = simple_cc as Geary.SearchQuery.TextOperator;
+ assert_true(text_cc.target == CC);
+
+ var simple_from = test_article.parse_query("from:hello");
+ assert_true(simple_from is Geary.SearchQuery.TextOperator);
+ var text_from = simple_from as Geary.SearchQuery.TextOperator;
+ assert_true(text_from.target == FROM);
+
+ var simple_subject = test_article.parse_query("subject:hello");
+ assert_true(simple_subject is Geary.SearchQuery.TextOperator);
+ var text_subject = simple_subject as Geary.SearchQuery.TextOperator;
+ assert_true(text_subject.target == SUBJECT);
+
+ var simple_to = test_article.parse_query("to:hello");
+ assert_true(simple_to is Geary.SearchQuery.TextOperator);
+ var text_to = simple_to as Geary.SearchQuery.TextOperator;
+ assert_true(text_to.target == TO);
+ }
+
+ public void text_op_single_me_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple_to = test_article.parse_query("to:me");
+ assert_true(simple_to is Geary.SearchQuery.TextOperator);
+ var text_to = simple_to as Geary.SearchQuery.TextOperator;
+ assert_true(text_to.target == TO);
+ assert_true(text_to.matching_strategy == EXACT);
+ assert_equal(text_to.term, "test example com");
+
+ var simple_cc = test_article.parse_query("cc:me");
+ assert_true(simple_cc is Geary.SearchQuery.TextOperator);
+ var text_cc = simple_cc as Geary.SearchQuery.TextOperator;
+ assert_true(text_cc.target == CC);
+ assert_true(text_cc.matching_strategy == EXACT);
+ assert_equal(text_cc.term, "test example com");
+
+ var simple_bcc = test_article.parse_query("bcc:me");
+ assert_true(simple_bcc is Geary.SearchQuery.TextOperator);
+ var text_bcc = simple_bcc as Geary.SearchQuery.TextOperator;
+ assert_true(text_bcc.target == BCC);
+ assert_true(text_bcc.matching_strategy == EXACT);
+ assert_equal(text_bcc.term, "test example com");
+
+ var simple_from = test_article.parse_query("from:me");
+ assert_true(simple_from is Geary.SearchQuery.TextOperator);
+ var text_from = simple_from as Geary.SearchQuery.TextOperator;
+ assert_true(text_from.target == FROM);
+ assert_true(text_from.matching_strategy == EXACT);
+ assert_equal(text_from.term, "test example com");
+ }
+
+ public void text_op_multiple_me_terms() throws GLib.Error {
+ this.account.append_sender(
+ new Geary.RFC822.MailboxAddress("test2", "test2 example com")
+ );
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var to = test_article.parse_query("to:me");
+ assert_true(to is Geary.SearchQuery.OrOperator);
+ var or = to as Geary.SearchQuery.OrOperator;
+ var operands = or.get_operands().to_array();
+ assert_equal<int?>(operands.length, 2);
+ assert_true(operands[0] is Geary.SearchQuery.TextOperator);
+ assert_true(operands[1] is Geary.SearchQuery.TextOperator);
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[0]).term,
+ "test example com"
+ );
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[1]).term,
+ "test2 example com"
+ );
+ }
+
+ public void boolean_op_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple_unread = test_article.parse_query("is:unread");
+ assert_true(simple_unread is Geary.SearchQuery.BooleanOperator);
+ var bool_unread = simple_unread as Geary.SearchQuery.BooleanOperator;
+ assert_true(bool_unread.target == IS_UNREAD);
+ assert_true(bool_unread.value);
+
+ var simple_read = test_article.parse_query("is:read");
+ assert_true(simple_read is Geary.SearchQuery.BooleanOperator);
+ var bool_read = simple_read as Geary.SearchQuery.BooleanOperator;
+ assert_true(bool_read.target == IS_UNREAD);
+ assert_false(bool_read.value);
+
+ var simple_starred = test_article.parse_query("is:starred");
+ assert_true(simple_starred is Geary.SearchQuery.BooleanOperator);
+ var bool_starred = simple_starred as Geary.SearchQuery.BooleanOperator;
+ assert_true(bool_starred.target == IS_FLAGGED);
+ assert_true(bool_starred.value);
+ }
+
+ public void invalid_op_terms() throws GLib.Error {
+ var test_article = new SearchExpressionFactory(this.config, this.account);
+
+ var simple1 = test_article.parse_query("body:");
+ assert_true(simple1 is Geary.SearchQuery.TextOperator);
+ var text1 = simple1 as Geary.SearchQuery.TextOperator;
+ assert_true(text1.target == ALL);
+ assert_true(text1.matching_strategy == CONSERVATIVE);
+ assert_equal(text1.term, "body");
+
+ var simple2 = test_article.parse_query("blarg:");
+ assert_true(simple2 is Geary.SearchQuery.TextOperator);
+ var text2 = simple2 as Geary.SearchQuery.TextOperator;
+ assert_true(text2.target == ALL);
+ assert_true(text2.matching_strategy == CONSERVATIVE);
+ assert_equal(text2.term, "blarg");
+
+ var simple3 = test_article.parse_query("blarg:hello");
+ assert_true(simple3 is Geary.SearchQuery.AndOperator);
+ var and = simple3 as Geary.SearchQuery.AndOperator;
+
+ var operands = and.get_operands().to_array();
+ assert_equal<int?>(operands.length, 2);
+ assert_true(operands[0] is Geary.SearchQuery.TextOperator);
+ assert_true(operands[1] is Geary.SearchQuery.TextOperator);
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[0]).term,
+ "blarg"
+ );
+ assert_equal(
+ ((Geary.SearchQuery.TextOperator) operands[1]).term,
+ "hello"
+ );
+ }
+
private Geary.Email new_email(Geary.RFC822.MailboxAddress? from,
Geary.RFC822.MailboxAddress? sender,
Geary.RFC822.MailboxAddress? reply_to)
diff --git a/test/mock/mock-account.vala b/test/mock/mock-account.vala
index 173b2ecae..85f3aed32 100644
--- a/test/mock/mock-account.vala
+++ b/test/mock/mock-account.vala
@@ -222,12 +222,13 @@ public class Mock.Account : Geary.Account,
);
}
- public override async Geary.SearchQuery
- new_search_query(string raw,
- Geary.SearchQuery.Strategy strategy,
- GLib.Cancellable? cancellable)
- throws GLib.Error {
- return new SearchQuery(this, raw);
+ public override async Geary.SearchQuery new_search_query(
+ Geary.SearchQuery.Operator operator,
+ string raw,
+ Geary.SearchQuery.Strategy strategy,
+ GLib.Cancellable? cancellable
+ ) throws GLib.Error {
+ return new SearchQuery(this, operator, raw);
}
public override async Gee.Collection<Geary.EmailIdentifier>?
diff --git a/test/mock/mock-search-query.vala b/test/mock/mock-search-query.vala
index 6653f96de..1bddf98fd 100644
--- a/test/mock/mock-search-query.vala
+++ b/test/mock/mock-search-query.vala
@@ -7,8 +7,10 @@
public class Mock.SearchQuery : Geary.SearchQuery {
- internal SearchQuery(Geary.Account owner, string raw) {
- base(owner, raw, Geary.SearchQuery.Strategy.EXACT);
+ internal SearchQuery(Account owner,
+ Geary.SearchQuery.Operator expression,
+ string raw) {
+ base(owner, expression, raw, Geary.SearchQuery.Strategy.EXACT);
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]