Implement IMAP SEARCH: Closes #7172

This implements the basics of the SEARCH command and response in
the IMAP stack and exposes it up to Imap.Folder.
This commit is contained in:
Jim Nelson 2013-06-27 17:21:55 -07:00
parent a11838e40b
commit f83fd64601
26 changed files with 630 additions and 68 deletions

View file

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

View file

@ -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, "<arg> ...");
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);

View file

@ -24,13 +24,27 @@ private class Geary.Imap.Folder : BaseObject {
private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
private Gee.HashMap<SequenceNumber, FetchedData> fetch_accumulator = new Gee.HashMap<
SequenceNumber, FetchedData>();
private Gee.HashSet<Geary.EmailIdentifier> search_accumulator = new Gee.HashSet<Geary.EmailIdentifier>();
/**
* 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<int> 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<SequenceNumber, FetchedData>? exec_commands_async(
Gee.Collection<Command> cmds, Cancellable? cancellable) throws Error {
private async void exec_commands_async(Gee.Collection<Command> cmds,
out Gee.HashMap<SequenceNumber, FetchedData>? fetched,
out Gee.HashSet<Geary.EmailIdentifier>? 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<SequenceNumber, FetchedData>? results = null;
// swap out results and clear accumulators
if (fetch_accumulator.size > 0) {
results = fetch_accumulator;
fetched = fetch_accumulator;
fetch_accumulator = new Gee.HashMap<SequenceNumber, FetchedData>();
} else {
fetched = null;
}
// unlock after clearing accumulator
if (search_accumulator.size > 0) {
search_results = search_accumulator;
search_accumulator = new Gee.HashSet<Geary.EmailIdentifier>();
} 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<SequenceNumber, FetchedData>? fetched = yield exec_commands_async(cmds,
cancellable);
Gee.HashMap<SequenceNumber, FetchedData>? 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<Command> cmds = new Collection.SingleItem<Command>(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<Geary.EmailIdentifier>? search_async(SearchCriteria criteria,
Cancellable? cancellable) throws Error {
check_open();
// always perform a UID SEARCH
Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
cmds.add(new SearchCommand(criteria, true));
Gee.HashSet<Geary.EmailIdentifier>? 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

View file

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

View file

@ -14,7 +14,7 @@ public class Geary.Imap.IdCommand : Command {
public IdCommand(Gee.HashMap<string, string> 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)));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ public class Geary.Imap.MessageFlags : Geary.Imap.Flags {
*/
public static MessageFlags from_list(ListParameter listp) throws ImapError {
Gee.Collection<MessageFlag> list = new Gee.ArrayList<MessageFlag>();
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);

View file

@ -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<Parameter> list = new Gee.ArrayList<Parameter>();
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<Parameter> list = new Gee.ArrayList<Parameter>();
/**
* 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<Parameter> to_append = listp.list;
listp.list = new Gee.ArrayList<Parameter>();
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() {

View file

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

View file

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

View file

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

View file

@ -96,7 +96,7 @@ public class Geary.Imap.MessageFlagsDecoder : Geary.Imap.FetchDataDecoder {
protected override MessageData decode_list(ListParameter listp) throws ImapError {
Gee.List<Flag> flags = new Gee.ArrayList<Flag>();
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<Geary.RFC822.MailboxAddress> list = new Gee.ArrayList<Geary.RFC822.MailboxAddress>();
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);

View file

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

View file

@ -21,7 +21,7 @@ public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
*/
public static MailboxAttributes from_list(ListParameter listp) throws ImapError {
Gee.Collection<MailboxAttribute> list = new Gee.ArrayList<MailboxAttribute>();
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);

View file

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

View file

@ -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<int> get_search() throws ImapError {
if (server_data_type != ServerDataType.SEARCH)
throw new ImapError.INVALID("Not SEARCH data: %s", to_string());
Gee.List<int> results = new Gee.ArrayList<int>();
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.
*

View file

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

View file

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

View file

@ -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<int> 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(),

View file

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