diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12c67b6c..b98acb10 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -102,6 +102,9 @@ engine/imap/command/imap-login-command.vala engine/imap/command/imap-logout-command.vala engine/imap/command/imap-message-set.vala engine/imap/command/imap-noop-command.vala +engine/imap/command/imap-search-command.vala +engine/imap/command/imap-search-criteria.vala +engine/imap/command/imap-search-criterion.vala engine/imap/command/imap-select-command.vala engine/imap/command/imap-starttls-command.vala engine/imap/command/imap-status-command.vala @@ -125,6 +128,7 @@ engine/imap/parameter/imap-atom-parameter.vala engine/imap/parameter/imap-list-parameter.vala engine/imap/parameter/imap-literal-parameter.vala engine/imap/parameter/imap-nil-parameter.vala +engine/imap/parameter/imap-number-parameter.vala engine/imap/parameter/imap-parameter.vala engine/imap/parameter/imap-quoted-string-parameter.vala engine/imap/parameter/imap-root-parameters.vala diff --git a/src/console/main.vala b/src/console/main.vala index a0f91fec..f0c4376a 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -96,6 +96,8 @@ class ImapConsole : Gtk.Window { "uid-fetch", "fetch-fields", "append", + "search", + "uid-search", "help", "exit", "quit", @@ -188,6 +190,11 @@ class ImapConsole : Gtk.Window { append(cmd, args); break; + case "search": + case "uid-search": + search(cmd, args); + break; + case "help": foreach (string cmdname in cmdnames) print_console_line(cmdname); @@ -491,6 +498,28 @@ class ImapConsole : Gtk.Window { } } + private void search(string cmd, string[] args) throws Error { + check_min_connected(cmd, args, 1, " ..."); + + status("Searching"); + + Geary.Imap.SearchCriteria criteria = new Geary.Imap.SearchCriteria(); + foreach (string arg in args) + criteria.and(new Geary.Imap.SearchCriterion.simple(arg)); + + cx.send_async.begin(new Geary.Imap.SearchCommand(criteria, cmd == "uid-search"), + null, on_searched); + } + + private void on_searched(Object? source, AsyncResult result) { + try { + cx.send_async.end(result); + status("Searched"); + } catch (Error err) { + exception(err); + } + } + private void close(string cmd, string[] args) throws Error { check_connected(cmd, args, 0, null); diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 2df221eb..cf48f290 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -24,13 +24,27 @@ private class Geary.Imap.Folder : BaseObject { private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex(); private Gee.HashMap fetch_accumulator = new Gee.HashMap< SequenceNumber, FetchedData>(); + private Gee.HashSet search_accumulator = new Gee.HashSet(); + /** + * A (potentially unsolicited) response from the server. + * + * See [[http://tools.ietf.org/html/rfc3501#section-7.3.1]] + */ public signal void exists(int total); + /** + * A (potentially unsolicited) response from the server. + * + * See [[http://tools.ietf.org/html/rfc3501#section-7.4.1]] + */ public signal void expunge(int position); - public signal void fetched(FetchedData fetched_data); - + /** + * A (potentially unsolicited) response from the server. + * + * See [[http://tools.ietf.org/html/rfc3501#section-7.3.2]] + */ public signal void recent(int total); /** @@ -79,6 +93,7 @@ private class Geary.Imap.Folder : BaseObject { session.expunge.connect(on_expunge); session.fetch.connect(on_fetch); session.recent.connect(on_recent); + session.search.connect(on_search); session.status_response_received.connect(on_status_response); session.disconnected.connect(on_disconnected); @@ -120,6 +135,7 @@ private class Geary.Imap.Folder : BaseObject { session.expunge.disconnect(on_expunge); session.fetch.disconnect(on_fetch); session.recent.disconnect(on_recent); + session.search.disconnect(on_search); session.status_response_received.disconnect(on_status_response); session.disconnected.disconnect(on_disconnected); @@ -165,10 +181,6 @@ private class Geary.Imap.Folder : BaseObject { FetchedData? already_present = fetch_accumulator.get(fetched_data.seq_num); fetch_accumulator.set(fetched_data.seq_num, (already_present != null) ? fetched_data.combine(already_present) : fetched_data); - - // don't fire signal until opened - if (is_open) - fetched(fetched_data); } private void on_recent(int total) { @@ -181,6 +193,13 @@ private class Geary.Imap.Folder : BaseObject { recent(total); } + private void on_search(Gee.List seq_or_uid) { + // All SEARCH from this class are UID SEARCH, so can reliably convert and add to + // accumulator + foreach (int uid in seq_or_uid) + search_accumulator.add(new Imap.EmailIdentifier(new UID(uid), path)); + } + private void on_status_response(StatusResponse status_response) { // only interested in ResponseCodes here ResponseCode? response_code = status_response.response_code; @@ -240,8 +259,9 @@ private class Geary.Imap.Folder : BaseObject { } // All commands must executed inside the cmd_mutex; returns FETCH or STORE results - private async Gee.HashMap? exec_commands_async( - Gee.Collection cmds, Cancellable? cancellable) throws Error { + private async void exec_commands_async(Gee.Collection cmds, + out Gee.HashMap? fetched, + out Gee.HashSet? search_results, Cancellable? cancellable) throws Error { int token = yield cmd_mutex.claim_async(cancellable); // execute commands with mutex locked @@ -253,26 +273,31 @@ private class Geary.Imap.Folder : BaseObject { err = store_fetch_err; } - // swap out results and clear accumulator - Gee.HashMap? results = null; + // swap out results and clear accumulators if (fetch_accumulator.size > 0) { - results = fetch_accumulator; + fetched = fetch_accumulator; fetch_accumulator = new Gee.HashMap(); + } else { + fetched = null; } - // unlock after clearing accumulator + if (search_accumulator.size > 0) { + search_results = search_accumulator; + search_accumulator = new Gee.HashSet(); + } else { + search_results = null; + } + + // unlock after clearing accumulators cmd_mutex.release(ref token); if (err != null) throw err; + // process response stati after unlocking and clearing accumulators assert(responses != null); - - // process response stati after unlocking and clearing accumulator foreach (Command cmd in responses.keys) throw_on_failed_status(responses.get(cmd), cmd); - - return results; } private void throw_on_failed_status(StatusResponse response, Command cmd) throws Error { @@ -381,8 +406,8 @@ private class Geary.Imap.Folder : BaseObject { } // Commands prepped, do the fetch and accumulate all the responses - Gee.HashMap? fetched = yield exec_commands_async(cmds, - cancellable); + Gee.HashMap? fetched; + yield exec_commands_async(cmds, out fetched, null, cancellable); if (fetched == null || fetched.size == 0) return null; @@ -436,7 +461,7 @@ private class Geary.Imap.Folder : BaseObject { else cmds.add(new ExpungeCommand()); - yield exec_commands_async(cmds, cancellable); + yield exec_commands_async(cmds, null, null, cancellable); } public async void mark_email_async(MessageSet msg_set, Geary.EmailFlags? flags_to_add, @@ -459,7 +484,7 @@ private class Geary.Imap.Folder : BaseObject { if (msg_flags_remove.size > 0) cmds.add(new StoreCommand(msg_set, msg_flags_remove, false, false)); - yield exec_commands_async(cmds, cancellable); + yield exec_commands_async(cmds, null, null, cancellable); } public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination, @@ -470,7 +495,7 @@ private class Geary.Imap.Folder : BaseObject { new MailboxSpecifier.from_folder_path(destination, null)); Gee.Collection cmds = new Collection.SingleItem(cmd); - yield exec_commands_async(cmds, cancellable); + yield exec_commands_async(cmds, null, null, cancellable); } // TODO: Support MOVE extension @@ -494,7 +519,21 @@ private class Geary.Imap.Folder : BaseObject { else cmds.add(new ExpungeCommand()); - yield exec_commands_async(cmds, cancellable); + yield exec_commands_async(cmds, null, null, cancellable); + } + + public async Gee.Set? search_async(SearchCriteria criteria, + Cancellable? cancellable) throws Error { + check_open(); + + // always perform a UID SEARCH + Gee.Collection cmds = new Gee.ArrayList(); + cmds.add(new SearchCommand(criteria, true)); + + Gee.HashSet? search_results; + yield exec_commands_async(cmds, null, out search_results, cancellable); + + return (search_results != null && search_results.size > 0) ? search_results : null; } // NOTE: If fields are added or removed from this method, BASIC_FETCH_FIELDS *must* be updated diff --git a/src/engine/imap/command/imap-fetch-command.vala b/src/engine/imap/command/imap-fetch-command.vala index 38cf10fa..2bea8ef8 100644 --- a/src/engine/imap/command/imap-fetch-command.vala +++ b/src/engine/imap/command/imap-fetch-command.vala @@ -36,7 +36,7 @@ public class Geary.Imap.FetchCommand : Command { } else if (data_items_length == 0 && body_items_length == 1) { add(body_data_items[0].to_request_parameter()); } else { - ListParameter list = new ListParameter(this); + ListParameter list = new ListParameter(); if (data_items_length > 0) { foreach (FetchDataType data_item in data_items) diff --git a/src/engine/imap/command/imap-id-command.vala b/src/engine/imap/command/imap-id-command.vala index ec693243..d32ccdf5 100644 --- a/src/engine/imap/command/imap-id-command.vala +++ b/src/engine/imap/command/imap-id-command.vala @@ -14,7 +14,7 @@ public class Geary.Imap.IdCommand : Command { public IdCommand(Gee.HashMap fields) { base (NAME); - ListParameter list = new ListParameter(this); + ListParameter list = new ListParameter(); foreach (string key in fields.keys) { list.add(new QuotedStringParameter(key)); list.add(new QuotedStringParameter(fields.get(key))); diff --git a/src/engine/imap/command/imap-search-command.vala b/src/engine/imap/command/imap-search-command.vala new file mode 100644 index 00000000..ecc7a1c7 --- /dev/null +++ b/src/engine/imap/command/imap-search-command.vala @@ -0,0 +1,25 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A representation of the IMAP SEARCH command. + * + * See [[http://tools.ietf.org/html/rfc3501#section-6.4.4]]. + */ + +public class Geary.Imap.SearchCommand : Command { + public const string NAME = "search"; + public const string UID_NAME = "uid search"; + + public SearchCommand(SearchCriteria criteria, bool is_uid) { + base (is_uid ? UID_NAME : NAME); + + // append rather than add the criteria, so the top-level criterion appear in the top-level + // list and not as a child list + append(criteria); + } +} + diff --git a/src/engine/imap/command/imap-search-criteria.vala b/src/engine/imap/command/imap-search-criteria.vala new file mode 100644 index 00000000..4b23d873 --- /dev/null +++ b/src/engine/imap/command/imap-search-criteria.vala @@ -0,0 +1,64 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A collection of one or more {@link SearchCriterion} for a {@link SearchCommand}. + * + * Criterion are added to the SearchCriteria one at a time with the {@link and} and {@link or} + * methods. Both methods return the SearchCriteria object, and so chaining can be used for + * convenience: + * + * SearchCriteria criteria = new SearchCriteria(); + * criteria.is_(SearchCriterion.new_messages()).and(SearchCriterion.has_flag(MessageFlag.DRAFT)); + * + * The or() method requires both criterion be passed: + * + * SearchCriteria criteria = new SearchCriteria(); + * criteria.or(SearchCriterion.old_messages(), SearchCriterion.body("attachment")); + * + * and() and or() can be mixed in a single SearchCriteria. + */ + +public class Geary.Imap.SearchCriteria : ListParameter { + public SearchCriteria() { + } + + /** + * Clears the {@link SearchCriteria} and sets the supplied {@SearchCriterion} to the first in + * the list. + * + * @returns This SearchCriteria for chaining. + */ + public unowned SearchCriteria is_(SearchCriterion first) { + clear(); + add(first.to_parameter()); + + return this; + } + + /** + * AND another {@link SearchCriterion} to the {@link SearchCriteria). + * + * @returns This SearchCriteria for chaining. + */ + public unowned SearchCriteria and(SearchCriterion next) { + add(next.to_parameter()); + + return this; + } + + /** + * OR another {@link SearchCriterion} to the {@link SearchCriteria). + * + * @returns This SearchCriteria for chaining. + */ + public unowned SearchCriteria or(SearchCriterion a, SearchCriterion b) { + add(SearchCriterion.or(a, b).to_parameter()); + + return this; + } +} + diff --git a/src/engine/imap/command/imap-search-criterion.vala b/src/engine/imap/command/imap-search-criterion.vala new file mode 100644 index 00000000..c6736513 --- /dev/null +++ b/src/engine/imap/command/imap-search-criterion.vala @@ -0,0 +1,188 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A representation of a single IMAP SEARCH criteria. + * + * See [[http://tools.ietf.org/html/rfc3501#section-6.4.4]] + * + * The current implementation does not support searching by sent date or arbitrary header values. + * + * @see SearchCommand + */ + +public class Geary.Imap.SearchCriterion : BaseObject { + private Parameter parameter; + + /** + * Create a single simple criterion for the {@link SearchCommand}. + */ + public SearchCriterion(Parameter parameter) { + this.parameter = parameter; + } + + /** + * Creates a simple search criterion. + */ + public SearchCriterion.simple(string name) { + parameter = prep_name(name); + } + + /** + * Create a single criterion with a simple name and custom value. + */ + public SearchCriterion.parameter_value(string name, Parameter value) { + parameter = make_list(prep_name(name), value); + } + + /** + * Create a single criterion with a simple name and custom value. + * + * @throws ImapError.INVALID if name must be transmitted as a {@link LiteralParameter}. + */ + public SearchCriterion.string_value(string name, string value) { + Parameter? valuep = StringParameter.get_best_for(value); + if (valuep == null) + valuep = new LiteralParameter(new Memory.StringBuffer(value)); + + parameter = make_list(prep_name(name), valuep); + } + + private static Parameter prep_name(string name) { + Parameter? namep = StringParameter.get_best_for(name); + if (namep == null) { + warning("Using a search name that requires a literal parameter: %s", name); + namep = new LiteralParameter(new Memory.StringBuffer(name)); + } + + return namep; + } + + private static ListParameter make_list(Parameter namep, Parameter valuep) { + ListParameter listp = new ListParameter(); + listp.add(namep); + listp.add(valuep); + + return listp; + } + + /** + * The IMAP SEARCH ALL criterion. + */ + public static SearchCriterion all() { + return new SearchCriterion.simple("all"); + } + + /** + * The IMAP SEARCH OR criterion, which operates on other {@link SearchCriterion}. + */ + public static SearchCriterion or(SearchCriterion a, SearchCriterion b) { + ListParameter listp = new ListParameter(); + listp.add(StringParameter.get_best_for("or")); + listp.add(a.to_parameter()); + listp.add(b.to_parameter()); + + return new SearchCriterion(listp); + } + + /** + * The IMAP SEARCH NEW criterion. + */ + public static SearchCriterion new_messages() { + return new SearchCriterion.simple("new"); + } + + /** + * The IMAP SEARCH OLD criterion. + */ + public static SearchCriterion old_messages() { + return new SearchCriterion.simple("old"); + } + + /** + * The IMAP SEARCH KEYWORD criterion, or if the {@link MessageFlag} has a macro, that value. + */ + public static SearchCriterion has_flag(MessageFlag flag) { + string? keyword = flag.get_search_keyword(true); + if (keyword != null) + return new SearchCriterion.simple(keyword); + + return new SearchCriterion.parameter_value("keyword", flag.to_parameter()); + } + + /** + * The IMAP SEARCH UNKEYWORD criterion, or if the {@link MessageFlag} has a macro, that value. + */ + public static SearchCriterion has_not_flag(MessageFlag flag) { + string? keyword = flag.get_search_keyword(false); + if (keyword != null) + return new SearchCriterion.simple(keyword); + + return new SearchCriterion.parameter_value("unkeyword", flag.to_parameter()); + } + + /** + * The IMAP SEARCH ON criterion. + */ + public static SearchCriterion on_internaldate(InternalDate internaldate) { + return new SearchCriterion.parameter_value("on", internaldate.to_parameter()); + } + + /** + * The IMAP SEARCH SINCE criterion. + */ + public static SearchCriterion since_internaldate(InternalDate internaldate) { + return new SearchCriterion.parameter_value("since", internaldate.to_parameter()); + } + + /** + * The IMAP SEARCH BODY criterion, which searches the body for the string. + */ + public static SearchCriterion body(string value) { + return new SearchCriterion.string_value("body", value); + } + + /** + * The IMAP SEARCH TEXT criterion, which searches the header and body for the string. + */ + public static SearchCriterion text(string value) { + return new SearchCriterion.string_value("text", value); + } + + /** + * The IMAP SEARCH SMALLER criterion. + */ + public static SearchCriterion smaller(uint32 value) { + return new SearchCriterion.parameter_value("smaller", new NumberParameter.uint32(value)); + } + + /** + * The IMAP SEARCH LARGER criterion. + */ + public static SearchCriterion larger(uint32 value) { + return new SearchCriterion.parameter_value("larger", new NumberParameter.uint32(value)); + } + + /** + * Specifies messages (by sequence number or UID) to limit the IMAP SEARCH to. + */ + public static SearchCriterion message_set(MessageSet msg_set) { + return msg_set.is_uid ? new SearchCriterion.parameter_value("uid", msg_set.to_parameter()) + : new SearchCriterion(msg_set.to_parameter()); + } + + /** + * Returns the {@link SearchCriterion} as an IMAP {@link Parameter}. + */ + public Parameter to_parameter() { + return parameter; + } + + public string to_string() { + return parameter.to_string(); + } +} + diff --git a/src/engine/imap/command/imap-status-command.vala b/src/engine/imap/command/imap-status-command.vala index 62d8332a..037d32ba 100644 --- a/src/engine/imap/command/imap-status-command.vala +++ b/src/engine/imap/command/imap-status-command.vala @@ -19,7 +19,7 @@ public class Geary.Imap.StatusCommand : Command { add(mailbox.to_parameter()); assert(data_items.length > 0); - ListParameter data_item_list = new ListParameter(this); + ListParameter data_item_list = new ListParameter(); foreach (StatusDataType data_item in data_items) data_item_list.add(data_item.to_parameter()); diff --git a/src/engine/imap/command/imap-store-command.vala b/src/engine/imap/command/imap-store-command.vala index 3bac606d..666220f2 100644 --- a/src/engine/imap/command/imap-store-command.vala +++ b/src/engine/imap/command/imap-store-command.vala @@ -22,7 +22,7 @@ public class Geary.Imap.StoreCommand : Command { add(message_set.to_parameter()); add(new AtomParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : ""))); - ListParameter list = new ListParameter(this); + ListParameter list = new ListParameter(); foreach(MessageFlag flag in flag_list) list.add(new AtomParameter(flag.value)); diff --git a/src/engine/imap/message/imap-flags.vala b/src/engine/imap/message/imap-flags.vala index 8c971535..42f09977 100644 --- a/src/engine/imap/message/imap-flags.vala +++ b/src/engine/imap/message/imap-flags.vala @@ -41,7 +41,7 @@ public abstract class Geary.Imap.Flags : Geary.MessageData.AbstractMessageData, * If empty, this returns an empty ListParameter. */ public virtual Parameter to_parameter() { - ListParameter listp = new ListParameter(null); + ListParameter listp = new ListParameter(); foreach (Flag flag in list) listp.add(flag.to_parameter()); diff --git a/src/engine/imap/message/imap-message-flag.vala b/src/engine/imap/message/imap-message-flag.vala index 86e3eec8..58668e15 100644 --- a/src/engine/imap/message/imap-message-flag.vala +++ b/src/engine/imap/message/imap-message-flag.vala @@ -79,6 +79,9 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag { return _load_remote_images; } } + /** + * Creates an IMAP message (email) named flag. + */ public MessageFlag(string value) { base (value); } @@ -92,6 +95,7 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag { to_init = RECENT; to_init = SEEN; to_init = ALLOWS_NEW; + to_init = LOAD_REMOTE_IMAGES; } // Converts a list of email flags to add and remove to a list of message @@ -120,5 +124,36 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag { msg_flags_remove.add(MessageFlag.LOAD_REMOTE_IMAGES); } } + + /** + * Returns a keyword suitable for the IMAP SEARCH command. + * + * See [[http://tools.ietf.org/html/rfc3501#section-6.4.4]]. This only returns a value for + * SEARCH's known flag keywords, all of which are system keywords. + * + * If present is false, the ''negative'' value is returned. So, ANSWERED !present is + * UNANSWERED. One exception: there is no UNRECENT, and so that will return null. + */ + public string? get_search_keyword(bool present) { + if (equal_to(ANSWERED)) + return present ? "answered" : "unanswered"; + + if (equal_to(DELETED)) + return present ? "deleted" : "undeleted"; + + if (equal_to(DRAFT)) + return present ? "draft" : "undraft"; + + if (equal_to(FLAGGED)) + return present ? "flagged" : "unflagged"; + + if (equal_to(RECENT)) + return present ? "recent" : null; + + if (equal_to(SEEN)) + return present ? "seen" : "unseen"; + + return null; + } } diff --git a/src/engine/imap/message/imap-message-flags.vala b/src/engine/imap/message/imap-message-flags.vala index 3b083afc..49a83306 100644 --- a/src/engine/imap/message/imap-message-flags.vala +++ b/src/engine/imap/message/imap-message-flags.vala @@ -22,7 +22,7 @@ public class Geary.Imap.MessageFlags : Geary.Imap.Flags { */ public static MessageFlags from_list(ListParameter listp) throws ImapError { Gee.Collection list = new Gee.ArrayList(); - for (int ctr = 0; ctr < listp.get_count(); ctr++) + for (int ctr = 0; ctr < listp.size; ctr++) list.add(new MessageFlag(listp.get_as_string(ctr).value)); return new MessageFlags(list); diff --git a/src/engine/imap/parameter/imap-list-parameter.vala b/src/engine/imap/parameter/imap-list-parameter.vala index cd58a241..1fb73728 100644 --- a/src/engine/imap/parameter/imap-list-parameter.vala +++ b/src/engine/imap/parameter/imap-list-parameter.vala @@ -17,14 +17,13 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { */ public const int MAX_STRING_LITERAL_LENGTH = 4096; - private weak ListParameter? parent; - private Gee.List list = new Gee.ArrayList(); - - public ListParameter(ListParameter? parent, Parameter? initial = null) { - this.parent = parent; - - if (initial != null) - add(initial); + /** + * Returns the number of {@link Parameter}s held in this {@link ListParameter}. + */ + public int size { + get { + return list.size; + } } /** @@ -33,21 +32,82 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { * In a fully-formed set of {@link Parameter}s, this means this {@link ListParameter} is * probably a {@link RootParameters}. */ - public ListParameter? get_parent() { - return parent; + public weak ListParameter? parent { get; private set; default = null; } + + private Gee.List list = new Gee.ArrayList(); + + /** + * Creates an empty ListParameter with no parent. + */ + public ListParameter() { } /** - * Returns true if added. + * Adds the {@link Parameter} to the end of the {@link ListParameter}. + * + * If the Parameter is itself a ListParameter, it's {@link parent} will be set to this + * ListParameter. * * The same {@link Parameter} can't be added more than once to the same {@link ListParameter}. + * There are no other restrictions, however. + * + * @returns true if added. */ public bool add(Parameter param) { + // if adding a ListParameter, set its parent + ListParameter? listp = param as ListParameter; + if (listp != null) + listp.parent = this; + return list.add(param); } - public int get_count() { - return list.size; + /** + * Appends the {@ListParameter} to the end of this ListParameter. + * + * The difference between this call and {@link add} is that add() will simply insert the + * {@link Parameter} to the tail of the list. Thus, add(ListParameter) will add a child list + * inside this list, i.e. add(ListParameter("three")): + * + * (one two (three)) + * + * append(ListParameter("three")) adds each element of the ListParameter to this one, not + * creating a child: + * + * (one two three) + * + * Thus, each element of the list is moved ("adopted") by this list, and the supplied list + * returns empty. This is slightly different than {@link adopt_children}, which preserves the + * list structure. + * + * @returns Number of added elements. append() will not abort if an element fails to add. + */ + public int append(ListParameter listp) { + // snap the child list off the supplied ListParameter so it's wiped clean + Gee.List to_append = listp.list; + listp.list = new Gee.ArrayList(); + + int count = 0; + foreach (Parameter param in to_append) { + if (add(param)) + count++; + } + + return count; + } + + /** + * Clears the {@link ListParameter} of all its children. + */ + public void clear() { + // sever ties to ListParameter children + foreach (Parameter param in list) { + ListParameter? listp = param as ListParameter; + if (listp != null) + listp.parent = null; + } + + list.clear(); } // @@ -250,11 +310,14 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { /** * Returns [@link ListParameter} at index, an empty list if NIL. + * + * If an empty ListParameter has to be manufactured in place of a NIL parameter, its parent + * will be null. */ public ListParameter get_as_empty_list(int index) throws ImapError { ListParameter? param = get_as_nullable_list(index); - return param ?? new ListParameter(this); + return param ?? new ListParameter(); } /** @@ -352,17 +415,27 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { Parameter old = list[index]; list[index] = parameter; + // add parent to new Parameter if a list + ListParameter? listp = parameter as ListParameter; + if (listp != null) + listp.parent = this; + + // clear parent of old Parameter if a list + listp = old as ListParameter; + if (listp != null) + listp.parent = null; + return old; } /** - * Moves all child parameters from the supplied list into this list. + * Moves all child parameters from the supplied list into this list, clearing this list first. * * The supplied list will be "stripped" of its children. This ListParameter is cleared prior * to adopting the new children. */ public void adopt_children(ListParameter src) { - list.clear(); + clear(); foreach (Parameter param in src.list) { ListParameter? listp = param as ListParameter; @@ -372,7 +445,7 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { list.add(param); } - src.list.clear(); + src.clear(); } protected string stringize_list() { diff --git a/src/engine/imap/parameter/imap-number-parameter.vala b/src/engine/imap/parameter/imap-number-parameter.vala new file mode 100644 index 00000000..894f103b --- /dev/null +++ b/src/engine/imap/parameter/imap-number-parameter.vala @@ -0,0 +1,87 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A representation of a numerical {@link Parameter} in an IMAP {@link Command}. + * + * See [[http://tools.ietf.org/html/rfc3501#section-4.2]] + */ + +public class Geary.Imap.NumberParameter : UnquotedStringParameter { + public NumberParameter(int num) { + base (num.to_string()); + } + + public NumberParameter.uint(uint num) { + base (num.to_string()); + } + + public NumberParameter.int32(int32 num) { + base (num.to_string()); + } + + public NumberParameter.uint32(uint32 num) { + base (num.to_string()); + } + + public NumberParameter.int64(int64 num) { + base (num.to_string()); + } + + public NumberParameter.uint64(uint64 num) { + base (num.to_string()); + } + + /** + * Creates a {@link NumberParameter} for a string representation of a number. + * + * No checking is performed to verify that the string is only composed of numeric characters. + * Use {@link is_numeric}. + */ + public NumberParameter.from_string(string str) { + base (str); + } + + /** + * Returns true if the string is composed of numeric characters. + * + * The only non-numeric character allowed is a dash ('-') at the beginning of the string to + * indicate a negative value. However, note that almost every IMAP use of a number is for a + * positive value. is_negative returns set to true if that's the case. is_negative is only + * a valid value if the method returns true itself. + * + * Empty strings (null or zero-length) are considered non-numeric. Leading and trailing + * whitespace are stripped before evaluating the string. + */ + public static bool is_numeric(string s, out bool is_negative) { + is_negative = false; + + string str = s.strip(); + + if (String.is_empty(str)) + return false; + + bool first_char = true; + int index = 0; + unichar ch; + while (str.get_next_char(ref index, out ch)) { + if (first_char && ch == '-') { + is_negative = true; + first_char = false; + + continue; + } + + first_char = false; + + if (!ch.isdigit()) + return false; + } + + return true; + } +} + diff --git a/src/engine/imap/parameter/imap-root-parameters.vala b/src/engine/imap/parameter/imap-root-parameters.vala index 160adca7..f50c7a7f 100644 --- a/src/engine/imap/parameter/imap-root-parameters.vala +++ b/src/engine/imap/parameter/imap-root-parameters.vala @@ -15,8 +15,7 @@ */ public class Geary.Imap.RootParameters : Geary.Imap.ListParameter { - public RootParameters(Parameter? initial = null) { - base (null, initial); + public RootParameters() { } /** @@ -26,8 +25,6 @@ public class Geary.Imap.RootParameters : Geary.Imap.ListParameter { * The supplied root object is stripped clean by this call. */ public RootParameters.migrate(RootParameters root) { - base (null); - adopt_children(root); } diff --git a/src/engine/imap/parameter/imap-string-parameter.vala b/src/engine/imap/parameter/imap-string-parameter.vala index 903fa5b9..8425679d 100644 --- a/src/engine/imap/parameter/imap-string-parameter.vala +++ b/src/engine/imap/parameter/imap-string-parameter.vala @@ -51,6 +51,9 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter { * @return null if the string must be represented with a {@link LiteralParameter}. */ public static StringParameter? get_best_for(string value) { + if (NumberParameter.is_numeric(value, null)) + return new NumberParameter.from_string(value); + switch (DataFormat.is_quoting_required(value)) { case DataFormat.Quoting.REQUIRED: return new QuotedStringParameter(value); diff --git a/src/engine/imap/response/imap-fetch-data-decoder.vala b/src/engine/imap/response/imap-fetch-data-decoder.vala index 1315d76d..856cbc43 100644 --- a/src/engine/imap/response/imap-fetch-data-decoder.vala +++ b/src/engine/imap/response/imap-fetch-data-decoder.vala @@ -96,7 +96,7 @@ public class Geary.Imap.MessageFlagsDecoder : Geary.Imap.FetchDataDecoder { protected override MessageData decode_list(ListParameter listp) throws ImapError { Gee.List flags = new Gee.ArrayList(); - for (int ctr = 0; ctr < listp.get_count(); ctr++) + for (int ctr = 0; ctr < listp.size; ctr++) flags.add(new MessageFlag(listp.get_as_string(ctr).value)); return new MessageFlags(flags); @@ -159,7 +159,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { // ImapError.TYPE_ERROR if this occurs. private Geary.RFC822.MailboxAddresses? parse_addresses(ListParameter listp) throws ImapError { Gee.List list = new Gee.ArrayList(); - for (int ctr = 0; ctr < listp.get_count(); ctr++) { + for (int ctr = 0; ctr < listp.size; ctr++) { ListParameter fields = listp.get_as_empty_list(ctr); StringParameter? name = fields.get_as_nullable_string(0); StringParameter? source_route = fields.get_as_nullable_string(1); diff --git a/src/engine/imap/response/imap-fetched-data.vala b/src/engine/imap/response/imap-fetched-data.vala index 9d0c0577..2002fa3e 100644 --- a/src/engine/imap/response/imap-fetched-data.vala +++ b/src/engine/imap/response/imap-fetched-data.vala @@ -59,11 +59,11 @@ public class Geary.Imap.FetchedData : Object { // walk the list for each returned fetch data item, which is paired by its data item name // and the structured data itself ListParameter list = server_data.get_as_list(3); - for (int ctr = 0; ctr < list.get_count(); ctr += 2) { + for (int ctr = 0; ctr < list.size; ctr += 2) { StringParameter data_item_param = list.get_as_string(ctr); // watch for truncated lists, which indicate an empty return value - bool has_value = (ctr < (list.get_count() - 1)); + bool has_value = (ctr < (list.size - 1)); if (FetchBodyDataType.is_fetch_body(data_item_param)) { // "fake" the identifier by merely dropping in the StringParameter wholesale ... diff --git a/src/engine/imap/response/imap-mailbox-attributes.vala b/src/engine/imap/response/imap-mailbox-attributes.vala index 97784328..831a4a7d 100644 --- a/src/engine/imap/response/imap-mailbox-attributes.vala +++ b/src/engine/imap/response/imap-mailbox-attributes.vala @@ -21,7 +21,7 @@ public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags { */ public static MailboxAttributes from_list(ListParameter listp) throws ImapError { Gee.Collection list = new Gee.ArrayList(); - for (int ctr = 0; ctr < listp.get_count(); ctr++) + for (int ctr = 0; ctr < listp.size; ctr++) list.add(new MailboxAttribute(listp.get_as_string(ctr).value)); return new MailboxAttributes(list); diff --git a/src/engine/imap/response/imap-response-code.vala b/src/engine/imap/response/imap-response-code.vala index 7a1ec5dc..32bc6dcf 100644 --- a/src/engine/imap/response/imap-response-code.vala +++ b/src/engine/imap/response/imap-response-code.vala @@ -11,8 +11,7 @@ */ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter { - public ResponseCode(ListParameter parent, Parameter? initial = null) { - base (parent, initial); + public ResponseCode() { } public ResponseCodeType get_response_code_type() throws ImapError { @@ -81,7 +80,7 @@ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter { throw new ImapError.INVALID("Not CAPABILITY response code: %s", to_string()); Capabilities capabilities = new Capabilities(next_revision++); - for (int ctr = 1; ctr < get_count(); ctr++) { + for (int ctr = 1; ctr < size; ctr++) { StringParameter? param = get_if_string(ctr); if (param != null) capabilities.add_parameter(param); diff --git a/src/engine/imap/response/imap-server-data.vala b/src/engine/imap/response/imap-server-data.vala index 3ef9a0c5..32528bc3 100644 --- a/src/engine/imap/response/imap-server-data.vala +++ b/src/engine/imap/response/imap-server-data.vala @@ -61,7 +61,7 @@ public class Geary.Imap.ServerData : ServerResponse { throw new ImapError.INVALID("Not CAPABILITY data: %s", to_string()); Capabilities capabilities = new Capabilities(next_revision++); - for (int ctr = 2; ctr < get_count(); ctr++) { + for (int ctr = 2; ctr < size; ctr++) { StringParameter? param = get_if_string(ctr); if (param != null) capabilities.add_parameter(param); @@ -142,6 +142,23 @@ public class Geary.Imap.ServerData : ServerResponse { return get_as_string(1).as_int(0); } + /** + * Parses the {@link ServerData} into a {@link ServerDataType.RECENT} value, if possible. + * + * @throws ImapError.INVALID if not a {@link ServerDataType.RECENT} value. + */ + public Gee.List get_search() throws ImapError { + if (server_data_type != ServerDataType.SEARCH) + throw new ImapError.INVALID("Not SEARCH data: %s", to_string()); + + Gee.List results = new Gee.ArrayList(); + for (int ctr = 2; ctr < size; ctr++) { + results.add(get_as_string(ctr).as_int(0)); + } + + return results; + } + /** * Parses the {@link ServerData} into {@link StatusData}, if possible. * diff --git a/src/engine/imap/response/imap-status-data.vala b/src/engine/imap/response/imap-status-data.vala index 327b91ed..e8b7c9d0 100644 --- a/src/engine/imap/response/imap-status-data.vala +++ b/src/engine/imap/response/imap-status-data.vala @@ -85,7 +85,7 @@ public class Geary.Imap.StatusData : Object { int unseen = UNSET; ListParameter values = server_data.get_as_list(3); - for (int ctr = 0; ctr < values.get_count(); ctr += 2) { + for (int ctr = 0; ctr < values.size; ctr += 2) { try { StringParameter typep = values.get_as_string(ctr); StringParameter valuep = values.get_as_string(ctr + 1); diff --git a/src/engine/imap/response/imap-status-response.vala b/src/engine/imap/response/imap-status-response.vala index 31c5f8c4..66fdf567 100644 --- a/src/engine/imap/response/imap-status-response.vala +++ b/src/engine/imap/response/imap-status-response.vala @@ -85,11 +85,11 @@ public class Geary.Imap.StatusResponse : ServerResponse { // build text from all StringParameters ... this will skip any ResponseCode or ListParameter // (or NilParameter, for that matter) StringBuilder builder = new StringBuilder(); - for (int index = 2; index < get_count(); index++) { + for (int index = 2; index < size; index++) { StringParameter? strparam = get_if_string(index); if (strparam != null) { builder.append(strparam.value); - if (index < (get_count() - 1)) + if (index < (size - 1)) builder.append_c(' '); } } diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index 5c503d2e..c9eec5d2 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -218,7 +218,7 @@ public class Geary.Imap.ClientSession : BaseObject { public signal void recent(int count); - // TODO: SEARCH results + public signal void search(Gee.List seq_or_uid); public signal void status(StatusData status_data); @@ -1534,9 +1534,12 @@ public class Geary.Imap.ClientSession : BaseObject { status(server_data.get_status()); break; - // TODO: LSUB and SEARCH - case ServerDataType.LSUB: case ServerDataType.SEARCH: + search(server_data.get_search()); + break; + + // TODO: LSUB + case ServerDataType.LSUB: default: // do nothing debug("[%s] Not notifying of unhandled server data: %s", to_string(), diff --git a/src/engine/imap/transport/imap-deserializer.vala b/src/engine/imap/transport/imap-deserializer.vala index 643618dc..a66d73d1 100644 --- a/src/engine/imap/transport/imap-deserializer.vala +++ b/src/engine/imap/transport/imap-deserializer.vala @@ -459,7 +459,6 @@ public class Geary.Imap.Deserializer : BaseObject { // ListParameter's parent *must* be current context private void push(ListParameter child) { - assert(child.get_parent() == context); context.add(child); context = child; @@ -470,7 +469,7 @@ public class Geary.Imap.Deserializer : BaseObject { } private State pop() { - ListParameter? parent = context.get_parent(); + ListParameter? parent = context.parent; if (parent == null) { warning("Attempt to close unopened list/response code"); @@ -520,7 +519,7 @@ public class Geary.Imap.Deserializer : BaseObject { switch (ch) { case '[': // open response code - ResponseCode response_code = new ResponseCode(context); + ResponseCode response_code = new ResponseCode(); push(response_code); return State.START_PARAM; @@ -533,7 +532,7 @@ public class Geary.Imap.Deserializer : BaseObject { case '(': // open list - ListParameter list = new ListParameter(context); + ListParameter list = new ListParameter(); push(list); return State.START_PARAM;