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:
parent
a11838e40b
commit
f83fd64601
26 changed files with 630 additions and 68 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
25
src/engine/imap/command/imap-search-command.vala
Normal file
25
src/engine/imap/command/imap-search-command.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
64
src/engine/imap/command/imap-search-criteria.vala
Normal file
64
src/engine/imap/command/imap-search-criteria.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
188
src/engine/imap/command/imap-search-criterion.vala
Normal file
188
src/engine/imap/command/imap-search-criterion.vala
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
87
src/engine/imap/parameter/imap-number-parameter.vala
Normal file
87
src/engine/imap/parameter/imap-number-parameter.vala
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ...
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue