Geary.Imap: Make command cancellable a property of the command object

Since both submitting a command no longer requires a cancellable, and it
is desirable to avoid sending a queued command that has already been
cancelled beforehand, add a new `Command.should_send` Cancellable
property to specify if a command should (still) be sent or not, and stop
passing a cancellable to ClientSession when submitting commands.

Allow call sites to pass in existing cancellable objects, and thus also
add it it as a ctor property to the Command class and all subclasses.

Lastly, throw a cancelled exception in `wait_until_complete` if send
was cancelled so that the caller knows what happened.

Remove redundant cancellable argument from
`Imap.Client.command_transaction_async` and rename it to
`submit_command` to make it more obvious about what it does.
This commit is contained in:
Michael Gratton 2020-09-01 22:42:41 +10:00 committed by Michael James Gratton
parent 67f0678b1d
commit 85e9046c71
33 changed files with 382 additions and 189 deletions

View file

@ -290,13 +290,13 @@ class ImapConsole : Gtk.Window {
private void capabilities(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
this.cx.send_command(new Geary.Imap.CapabilityCommand());
this.cx.send_command(new Geary.Imap.CapabilityCommand(null));
}
private void noop(string cmd, string[] args) throws Error {
private void noop(string cmd, string[] args) throws GLib.Error {
check_connected(cmd, args, 0, null);
this.cx.send_command(new Geary.Imap.NoopCommand());
this.cx.send_command(new Geary.Imap.NoopCommand(null));
}
private void connect_cmd(string cmd, string[] args) throws Error {
@ -370,7 +370,7 @@ class ImapConsole : Gtk.Window {
}
private async void do_starttls_async() throws Error {
Geary.Imap.StarttlsCommand cmd = new Geary.Imap.StarttlsCommand();
Geary.Imap.StarttlsCommand cmd = new Geary.Imap.StarttlsCommand(null);
this.cx.send_command(cmd);
Geary.Imap.StatusResponse response = yield wait_for_response_async(cmd.tag);
@ -394,14 +394,18 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 2, "user pass");
status("Logging in...");
this.cx.send_command(new Geary.Imap.LoginCommand(args[0], args[1]));
this.cx.send_command(
new Geary.Imap.LoginCommand(args[0], args[1], null)
);
}
private void logout(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
status("Logging out...");
this.cx.send_command(new Geary.Imap.LogoutCommand());
this.cx.send_command(
new Geary.Imap.LogoutCommand(null)
);
}
private void id(string cmd, string[] args) throws Error {
@ -413,14 +417,16 @@ class ImapConsole : Gtk.Window {
fields.set("name", "geary-console");
fields.set("version", _VERSION);
this.cx.send_command(new Geary.Imap.IdCommand(fields));
this.cx.send_command(
new Geary.Imap.IdCommand(fields, null)
);
}
private void namespace(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
status("Retrieving NAMESPACE...");
this.cx.send_command(new Geary.Imap.NamespaceCommand());
this.cx.send_command(new Geary.Imap.NamespaceCommand(null));
}
private void list(string cmd, string[] args) throws Error {
@ -437,7 +443,8 @@ class ImapConsole : Gtk.Window {
args[0],
new Geary.Imap.MailboxSpecifier(args[1]),
(cmd.down() == "xlist"),
return_param
return_param,
null
)
);
}
@ -446,7 +453,12 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 1, "<mailbox>");
status("Opening %s read-only".printf(args[0]));
this.cx.send_command(new Geary.Imap.ExamineCommand(new Geary.Imap.MailboxSpecifier(args[0])));
this.cx.send_command(
new Geary.Imap.ExamineCommand(
new Geary.Imap.MailboxSpecifier(args[0]),
null
)
);
}
private void create(string cmd, string[] args) throws Error {
@ -455,7 +467,8 @@ class ImapConsole : Gtk.Window {
status("Creating %s".printf(args[0]));
this.cx.send_command(
new Geary.Imap.CreateCommand(
new Geary.Imap.MailboxSpecifier(args[0])
new Geary.Imap.MailboxSpecifier(args[0]),
null
)
);
}
@ -466,7 +479,8 @@ class ImapConsole : Gtk.Window {
status("Deleting %s".printf(args[0]));
this.cx.send_command(
new Geary.Imap.DeleteCommand(
new Geary.Imap.MailboxSpecifier(args[0])
new Geary.Imap.MailboxSpecifier(args[0]),
null
)
);
}
@ -487,7 +501,9 @@ class ImapConsole : Gtk.Window {
data_items.add(data_type);
}
this.cx.send_command(new Geary.Imap.FetchCommand(msg_set, data_items, null));
this.cx.send_command(
new Geary.Imap.FetchCommand(msg_set, data_items, null, null)
);
}
private void fetch_fields(string cmd, string[] args) throws Error {
@ -501,8 +517,11 @@ class ImapConsole : Gtk.Window {
Gee.List<Geary.Imap.FetchBodyDataSpecifier> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataSpecifier>();
list.add(fields);
this.cx.send_command(new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, list));
this.cx.send_command(
new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, list, null
)
);
}
private void append(string cmd, string[] args) throws Error {
@ -510,8 +529,15 @@ class ImapConsole : Gtk.Window {
status("Appending %s to %s".printf(args[1], args[0]));
this.cx.send_command(new Geary.Imap.AppendCommand(new Geary.Imap.MailboxSpecifier(args[0]),
null, null, new Geary.Memory.FileBuffer(File.new_for_path(args[1]), true)));
this.cx.send_command(
new Geary.Imap.AppendCommand(
new Geary.Imap.MailboxSpecifier(args[0]),
null,
null,
new Geary.Memory.FileBuffer(File.new_for_path(args[1]), true),
null
)
);
}
private void search(string cmd, string[] args) throws Error {
@ -525,9 +551,9 @@ class ImapConsole : Gtk.Window {
Geary.Imap.SearchCommand search;
if (cmd == "uid-search")
search = new Geary.Imap.SearchCommand.uid(criteria);
search = new Geary.Imap.SearchCommand.uid(criteria, null);
else
search = new Geary.Imap.SearchCommand(criteria);
search = new Geary.Imap.SearchCommand(criteria, null);
this.cx.send_command(search);
}
@ -537,7 +563,9 @@ class ImapConsole : Gtk.Window {
status("Closing");
this.cx.send_command(new Geary.Imap.CloseCommand());
this.cx.send_command(
new Geary.Imap.CloseCommand(null)
);
}
private void folder_status(string cmd, string[] args) throws Error {
@ -551,8 +579,13 @@ class ImapConsole : Gtk.Window {
data_items += Geary.Imap.StatusDataType.from_parameter(stringp);
}
this.cx.send_command(new Geary.Imap.StatusCommand(new Geary.Imap.MailboxSpecifier(args[0]),
data_items));
this.cx.send_command(
new Geary.Imap.StatusCommand(
new Geary.Imap.MailboxSpecifier(args[0]),
data_items,
null
)
);
}
private void preview(string cmd, string[] args) throws Error {
@ -567,8 +600,11 @@ class ImapConsole : Gtk.Window {
Gee.ArrayList<Geary.Imap.FetchBodyDataSpecifier> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataSpecifier>();
list.add(preview_data_type);
this.cx.send_command(new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, list));
this.cx.send_command(
new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, list, null
)
);
}
private void quit(string cmd, string[] args) throws Error {

View file

@ -97,9 +97,11 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
ClientSession session = claim_session();
MailboxSpecifier mailbox = session.get_mailbox_for_path(path);
bool can_create_special = session.capabilities.has_capability(Capabilities.CREATE_SPECIAL_USE);
CreateCommand cmd = (use != null && can_create_special)
? new CreateCommand.special_use(mailbox, use)
: new CreateCommand(mailbox);
CreateCommand cmd = (
use != null && can_create_special
? new CreateCommand.special_use(mailbox, use, cancellable)
: new CreateCommand(mailbox, cancellable)
);
StatusResponse response = yield send_command_async(
session, cmd, null, null, cancellable
@ -187,7 +189,9 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
// Mailbox needs a SELECT
info_map.set(mailbox_info.mailbox, mailbox_info);
cmd_map.set(
new StatusCommand(mailbox_info.mailbox, StatusDataType.all()),
new StatusCommand(
mailbox_info.mailbox, StatusDataType.all(), cancellable
),
mailbox_info.mailbox
);
} else {
@ -323,7 +327,10 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
if (folder.is_root) {
// List the server root
cmd = new ListCommand.wildcarded(
"", new MailboxSpecifier("%"), use_xlist, return_param
"", new MailboxSpecifier("%"),
use_xlist,
return_param,
cancellable
);
} else {
// List either the given folder or its children
@ -335,7 +342,12 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
}
specifier = specifier + delim + "%";
}
cmd = new ListCommand(new MailboxSpecifier(specifier), use_xlist, return_param);
cmd = new ListCommand(
new MailboxSpecifier(specifier),
use_xlist,
return_param,
cancellable
);
}
Gee.List<MailboxInformation> list_results = new Gee.ArrayList<MailboxInformation>();
@ -372,7 +384,7 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
StatusResponse response = yield send_command_async(
session,
new StatusCommand(mailbox, status_types),
new StatusCommand(mailbox, status_types, cancellable),
null,
status_results,
cancellable

View file

@ -430,7 +430,7 @@ public class Geary.Imap.ClientService : Geary.ClientService {
try {
debug("Sending NOOP when claiming a session");
yield target.send_command_async(
new NoopCommand(), this.close_cancellable
new NoopCommand(this.close_cancellable)
);
} catch (Error err) {
debug("Error sending NOOP: %s", err.message);

View file

@ -159,7 +159,7 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
public async void send_noop(GLib.Cancellable? cancellable)
throws GLib.Error {
yield exec_commands_async(
Collection.single(new NoopCommand()),
Collection.single(new NoopCommand(cancellable)),
null,
null,
cancellable
@ -334,12 +334,13 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
}
// Utility method for listing UIDs on the remote within the supplied range
public async Gee.Set<Imap.UID>? list_uids_async(MessageSet msg_set, Cancellable? cancellable)
throws Error {
public async Gee.Set<Imap.UID>? list_uids_async(MessageSet msg_set,
GLib.Cancellable? cancellable)
throws GLib.Error {
// Although FETCH could be used, SEARCH is more efficient in returning pure UID results,
// which is all we're interested in here
SearchCriteria criteria = new SearchCriteria(SearchCriterion.message_set(msg_set));
SearchCommand cmd = new SearchCommand.uid(criteria);
SearchCommand cmd = new SearchCommand.uid(criteria, cancellable);
Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>();
yield exec_commands_async(
@ -355,6 +356,7 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
private Gee.Collection<FetchCommand> assemble_list_commands(
Imap.MessageSet msg_set,
Geary.Email.Field fields,
GLib.Cancellable? cancellable,
out FetchBodyDataSpecifier[]? header_specifiers,
out FetchBodyDataSpecifier? body_specifier,
out FetchBodyDataSpecifier? preview_specifier,
@ -369,8 +371,13 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
// pulled down, not a guarantee); if request is for NONE, that guarantees that the
// EmailIdentifier will be set, and so fetch UIDs (which looks funny but works when
// listing a range for contents: UID FETCH x:y UID)
if (!msg_set.is_uid || fields == Geary.Email.Field.NONE)
cmds.add(new FetchCommand.data_type(msg_set, FetchDataSpecifier.UID));
if (!msg_set.is_uid || fields == Geary.Email.Field.NONE) {
cmds.add(
new FetchCommand.data_type(
msg_set, FetchDataSpecifier.UID, cancellable
)
);
}
// convert bulk of the "basic" fields into a one or two FETCH commands (some servers have
// exhibited bugs or return NO when too many FETCH data types are combined on a single
@ -385,7 +392,9 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
// Add all simple data types as one FETCH command
if (!basic_types.is_empty) {
cmds.add(new FetchCommand(msg_set, basic_types, null));
cmds.add(
new FetchCommand(msg_set, basic_types, null, cancellable)
);
}
// Add all header field requests as separate FETCH
@ -424,7 +433,11 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
if (this.quirks.fetch_header_part_no_space) {
header.omit_request_header_fields_space();
}
cmds.add(new FetchCommand.body_data_type(msg_set, header));
cmds.add(
new FetchCommand.body_data_type(
msg_set, header, cancellable
)
);
}
}
}
@ -434,7 +447,11 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
body_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.TEXT,
null, -1, -1, null);
cmds.add(new FetchCommand.body_data_type(msg_set, body_specifier));
cmds.add(
new FetchCommand.body_data_type(
msg_set, body_specifier, cancellable
)
);
} else {
body_specifier = null;
}
@ -453,12 +470,20 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
preview_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.NONE,
{ 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null);
cmds.add(new FetchCommand.body_data_type(msg_set, preview_specifier));
cmds.add(
new FetchCommand.body_data_type(
msg_set, preview_specifier, cancellable
)
);
// Also get the character set to properly decode it
preview_charset_specifier = new FetchBodyDataSpecifier.peek(
FetchBodyDataSpecifier.SectionPart.MIME, { 1 }, -1, -1, null);
cmds.add(new FetchCommand.body_data_type(msg_set, preview_charset_specifier));
cmds.add(
new FetchCommand.body_data_type(
msg_set, preview_charset_specifier, cancellable
)
);
} else {
preview_specifier = null;
preview_charset_specifier = null;
@ -476,7 +501,7 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
if (fields.require(Geary.Email.Field.FLAGS))
data_types.add(FetchDataSpecifier.FLAGS);
cmds.add(new FetchCommand(msg_set, data_types, null));
cmds.add(new FetchCommand(msg_set, data_types, null, cancellable));
}
return cmds;
@ -498,6 +523,7 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
Gee.Collection<FetchCommand> cmds = assemble_list_commands(
msg_set,
fields,
cancellable,
out header_specifiers,
out body_specifier,
out preview_specifier,
@ -593,7 +619,11 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
}
Gee.List<Command> cmds = new Gee.ArrayList<Command>();
cmds.add(new FetchCommand.data_type(msg_set, FetchDataSpecifier.UID));
cmds.add(
new FetchCommand.data_type(
msg_set, FetchDataSpecifier.UID, cancellable
)
);
Gee.HashMap<SequenceNumber, FetchedData> fetched =
new Gee.HashMap<SequenceNumber, FetchedData>();
@ -613,8 +643,9 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
return map;
}
public async void remove_email_async(Gee.List<MessageSet> msg_sets, Cancellable? cancellable)
throws Error {
public async void remove_email_async(Gee.List<MessageSet> msg_sets,
GLib.Cancellable? cancellable)
throws GLib.Error {
ClientSession session = claim_session();
Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
flags.add(MessageFlag.DELETED);
@ -627,7 +658,9 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
if (!msg_set.is_uid)
all_uid = false;
cmds.add(new StoreCommand(msg_set, flags, StoreCommand.Option.ADD_FLAGS));
cmds.add(
new StoreCommand(msg_set, flags, StoreCommand.Option.ADD_FLAGS, cancellable)
);
}
// TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work
@ -638,10 +671,11 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
// shuts down, which means deleted messages return at application start. See:
// http://redmine.yorba.org/issues/6865
if (all_uid && session.capabilities.supports_uidplus()) {
foreach (MessageSet msg_set in msg_sets)
cmds.add(new ExpungeCommand.uid(msg_set));
foreach (MessageSet msg_set in msg_sets) {
cmds.add(new ExpungeCommand.uid(msg_set, cancellable));
}
} else {
cmds.add(new ExpungeCommand());
cmds.add(new ExpungeCommand(cancellable));
}
yield exec_commands_async(cmds, null, null, cancellable);
@ -659,11 +693,21 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
foreach (MessageSet msg_set in msg_sets) {
if (msg_flags_add.size > 0)
cmds.add(new StoreCommand(msg_set, msg_flags_add, StoreCommand.Option.ADD_FLAGS));
if (msg_flags_add.size > 0) {
cmds.add(
new StoreCommand(
msg_set, msg_flags_add, ADD_FLAGS, cancellable
)
);
}
if (msg_flags_remove.size > 0)
cmds.add(new StoreCommand(msg_set, msg_flags_remove, StoreCommand.Option.REMOVE_FLAGS));
if (msg_flags_remove.size > 0) {
cmds.add(
new StoreCommand(
msg_set, msg_flags_remove, REMOVE_FLAGS, cancellable
)
);
}
}
yield exec_commands_async(cmds, null, null, cancellable);
@ -671,12 +715,14 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
// Returns a mapping of the source UID to the destination UID. If the MessageSet is not for
// UIDs, then null is returned. If the server doesn't support COPYUID, null is returned.
public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set, FolderPath destination,
Cancellable? cancellable) throws Error {
public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set,
FolderPath destination,
GLib.Cancellable? cancellable)
throws GLib.Error {
ClientSession session = claim_session();
MailboxSpecifier mailbox = session.get_mailbox_for_path(destination);
CopyCommand cmd = new CopyCommand(msg_set, mailbox);
CopyCommand cmd = new CopyCommand(msg_set, mailbox, cancellable);
Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async(
Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable);
@ -718,11 +764,12 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
return null;
}
public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria, Cancellable? cancellable)
throws Error {
public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria,
GLib.Cancellable? cancellable)
throws GLib.Error {
// always perform a UID SEARCH
Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
cmds.add(new SearchCommand.uid(criteria));
cmds.add(new SearchCommand.uid(criteria, cancellable));
Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>();
yield exec_commands_async(cmds, null, search_results, cancellable);
@ -1044,12 +1091,22 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
return email;
}
// Returns a no-message-id ImapDB.EmailIdentifier with the UID stored in it.
// This method does not take a cancellable; there is currently no way to tell if an email was
// created or not if exec_commands_async() is cancelled during the append. For atomicity's sake,
// callers need to remove the returned email ID if a cancel occurred.
public async Geary.EmailIdentifier? create_email_async(RFC822.Message message, Geary.EmailFlags? flags,
DateTime? date_received) throws Error {
/**
* Stores a new message in the remote mailbox.
*
* Returns a no-message-id ImapDB.EmailIdentifier with the UID
* stored in it.
*
* This method does not take a cancellable; there is currently no
* way to tell if an email was created or not if {@link
* exec_commands_async} is cancelled during the append. For
* atomicity's sake, callers need to remove the returned email ID
* if a cancel occurred.
*/
public async Geary.EmailIdentifier? create_email_async(RFC822.Message message,
Geary.EmailFlags? flags,
GLib.DateTime? date_received)
throws GLib.Error {
ClientSession session = claim_session();
MessageFlags? msg_flags = null;
@ -1066,11 +1123,16 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
MailboxSpecifier mailbox = session.get_mailbox_for_path(this.folder.path);
AppendCommand cmd = new AppendCommand(
mailbox, msg_flags, internaldate, message.get_rfc822_buffer()
mailbox,
msg_flags,
internaldate,
message.get_rfc822_buffer(),
null
);
Gee.Map<Command, StatusResponse> responses = yield exec_commands_async(
Geary.iterate<AppendCommand>(cmd).to_array_list(), null, null, null);
Geary.iterate<AppendCommand>(cmd).to_array_list(), null, null, null
);
// Grab the response and parse out the UID, if available.
StatusResponse response = responses.get(cmd);

View file

@ -14,9 +14,12 @@ public class Geary.Imap.AppendCommand : Command {
public const string NAME = "append";
public AppendCommand(MailboxSpecifier mailbox, MessageFlags? flags, InternalDate? internal_date,
Memory.Buffer message) {
base (NAME);
public AppendCommand(MailboxSpecifier mailbox,
MessageFlags? flags,
InternalDate? internal_date,
Memory.Buffer message,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.args.add(mailbox.to_parameter());

View file

@ -27,17 +27,21 @@ public class Geary.Imap.AuthenticateCommand : Command {
private GLib.Cancellable error_cancellable = new GLib.Cancellable();
private AuthenticateCommand(string method, string data) {
base(NAME, { method, data });
private AuthenticateCommand(string method,
string data,
GLib.Cancellable? should_send) {
base(NAME, { method, data }, should_send);
this.method = method;
this.error_lock = new Geary.Nonblocking.Spinlock(this.error_cancellable);
}
public AuthenticateCommand.oauth2(string user, string token) {
public AuthenticateCommand.oauth2(string user,
string token,
GLib.Cancellable? should_send) {
string encoded_token = Base64.encode(
OAUTH2_RESP.printf(user, token).data
);
this(OAUTH2_METHOD, encoded_token);
this(OAUTH2_METHOD, encoded_token, should_send);
}
internal override async void send(Serializer ser,

View file

@ -11,10 +11,11 @@
*/
public class Geary.Imap.CapabilityCommand : Command {
public const string NAME = "capability";
public CapabilityCommand() {
base (NAME);
public CapabilityCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}
}

View file

@ -9,10 +9,11 @@
*/
public class Geary.Imap.CloseCommand : Command {
public const string NAME = "close";
public CloseCommand() {
base (NAME);
public CloseCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}
}

View file

@ -60,6 +60,20 @@ public abstract class Geary.Imap.Command : BaseObject {
/** The status response for the command, once it has been received. */
public StatusResponse? status { get; private set; default = null; }
/**
* A guard to allow cancelling a command before it is sent.
*
* Since IMAP does not allow commands that have been sent to the
* server to be cancelled, cancelling a command before sending it
* is the last opportunity to prevent it from being executed. A
* command queued to be sent will be sent as long as the
* connection it was queued is open and this cancellable is null
* or is not cancelled.
*
* @see Command.Command
*/
public GLib.Cancellable? should_send { get; private set; default = null; }
/**
* The command's arguments as parameters.
*
@ -93,11 +107,15 @@ public abstract class Geary.Imap.Command : BaseObject {
* Constructs a new command with an unassigned tag.
*
* Any arguments provided here will be converted to appropriate
* string arguments
* string arguments. The given cancellable will be set as {@link
* should_send}.
*
* @see Tag
* @see should_send
*/
protected Command(string name, string[]? args = null) {
protected Command(string name,
string[]? args,
GLib.Cancellable? should_send) {
this.tag = Tag.get_unassigned();
this.name = name;
if (args != null) {
@ -105,6 +123,7 @@ public abstract class Geary.Imap.Command : BaseObject {
this.args.add(Parameter.get_for_string(arg));
}
}
this.should_send = should_send;
this.response_timer = new TimeoutManager.seconds(
this._response_timeout, on_response_timeout
@ -269,6 +288,15 @@ public abstract class Geary.Imap.Command : BaseObject {
this.status.to_string()
);
}
// If everything else looks fine, but sending was cancelled,
// throw an error here so the caller knows that was the case.
if (this.should_send != null &&
this.should_send.is_cancelled()) {
throw new GLib.IOError.CANCELLED(
"Sent command was cancelled: %s", to_brief_string()
);
}
}
public virtual string to_string() {

View file

@ -9,12 +9,14 @@
*/
public class Geary.Imap.CompressCommand : Command {
public const string NAME = "compress";
public const string ALGORITHM_DEFLATE = "deflate";
public CompressCommand(string algorithm) {
base (NAME, { algorithm });
}
}
public CompressCommand(string algorithm, GLib.Cancellable? should_send) {
base(NAME, { algorithm }, should_send);
}
}

View file

@ -13,8 +13,10 @@ public class Geary.Imap.CopyCommand : Command {
public const string NAME = "copy";
public const string UID_NAME = "uid copy";
public CopyCommand(MessageSet message_set, MailboxSpecifier destination) {
base(message_set.is_uid ? UID_NAME : NAME);
public CopyCommand(MessageSet message_set,
MailboxSpecifier destination,
GLib.Cancellable? should_send) {
base(message_set.is_uid ? UID_NAME : NAME, null, should_send);
this.args.add(message_set.to_parameter());
this.args.add(destination.to_parameter());

View file

@ -55,15 +55,16 @@ public class Geary.Imap.CreateCommand : Command {
}
}
public CreateCommand(MailboxSpecifier mailbox) {
base(NAME_ATOM);
public CreateCommand(MailboxSpecifier mailbox, GLib.Cancellable? should_send) {
base(NAME_ATOM, null, should_send);
this.mailbox = mailbox;
this.args.add(mailbox.to_parameter());
}
public CreateCommand.special_use(MailboxSpecifier mailbox,
Geary.Folder.SpecialUse use) {
this(mailbox);
Geary.Folder.SpecialUse use,
GLib.Cancellable? should_send) {
this(mailbox, should_send);
this.use = use;
MailboxAttribute? attr = get_special_folder_type(use);

View file

@ -18,8 +18,9 @@ public class Geary.Imap.DeleteCommand : Command {
public const string NAME = "DELETE";
public DeleteCommand(MailboxSpecifier mailbox) {
base(NAME);
public DeleteCommand(MailboxSpecifier mailbox,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.args.add(mailbox.to_parameter());
}

View file

@ -16,8 +16,9 @@ public class Geary.Imap.ExamineCommand : Command {
public MailboxSpecifier mailbox { get; private set; }
public ExamineCommand(MailboxSpecifier mailbox) {
base(NAME);
public ExamineCommand(MailboxSpecifier mailbox,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.mailbox = mailbox;
this.args.add(mailbox.to_parameter());
}

View file

@ -14,12 +14,13 @@ public class Geary.Imap.ExpungeCommand : Command {
public const string NAME = "expunge";
public const string UID_NAME = "uid expunge";
public ExpungeCommand() {
base(NAME);
public ExpungeCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
public ExpungeCommand.uid(MessageSet message_set) {
base(UID_NAME);
public ExpungeCommand.uid(MessageSet message_set,
GLib.Cancellable? should_send) {
base(UID_NAME, null, should_send);
assert(message_set.is_uid);
this.args.add(message_set.to_parameter());
}

View file

@ -34,9 +34,11 @@ public class Geary.Imap.FetchCommand : Command {
public Gee.List<FetchBodyDataSpecifier> for_body_data_specifiers { get; private set;
default = new Gee.ArrayList<FetchBodyDataSpecifier>(); }
public FetchCommand(MessageSet msg_set, Gee.List<FetchDataSpecifier>? data_items,
Gee.List<FetchBodyDataSpecifier>? body_data_items) {
base (msg_set.is_uid ? UID_NAME : NAME);
public FetchCommand(MessageSet msg_set,
Gee.List<FetchDataSpecifier>? data_items,
Gee.List<FetchBodyDataSpecifier>? body_data_items,
GLib.Cancellable? should_send) {
base(msg_set.is_uid ? UID_NAME : NAME, null, should_send);
this.args.add(msg_set.to_parameter());
@ -71,8 +73,10 @@ public class Geary.Imap.FetchCommand : Command {
for_body_data_specifiers.add_all(body_data_items);
}
public FetchCommand.data_type(MessageSet msg_set, FetchDataSpecifier data_type) {
base (msg_set.is_uid ? UID_NAME : NAME);
public FetchCommand.data_type(MessageSet msg_set,
FetchDataSpecifier data_type,
GLib.Cancellable? should_send) {
base(msg_set.is_uid ? UID_NAME : NAME, null, should_send);
for_data_types.add(data_type);
@ -80,8 +84,10 @@ public class Geary.Imap.FetchCommand : Command {
this.args.add(data_type.to_parameter());
}
public FetchCommand.body_data_type(MessageSet msg_set, FetchBodyDataSpecifier body_data_specifier) {
base (msg_set.is_uid ? UID_NAME : NAME);
public FetchCommand.body_data_type(MessageSet msg_set,
FetchBodyDataSpecifier body_data_specifier,
GLib.Cancellable? should_send) {
base(msg_set.is_uid ? UID_NAME : NAME, null, should_send);
for_body_data_specifiers.add(body_data_specifier);

View file

@ -12,8 +12,9 @@ public class Geary.Imap.IdCommand : Command {
public const string NAME = "id";
public IdCommand(Gee.HashMap<string, string> fields) {
base(NAME);
public IdCommand(Gee.HashMap<string, string> fields,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
ListParameter list = new ListParameter();
foreach (string key in fields.keys) {
@ -24,8 +25,8 @@ public class Geary.Imap.IdCommand : Command {
this.args.add(list);
}
public IdCommand.nil() {
base(NAME);
public IdCommand.nil(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.args.add(NilParameter.instance);
}

View file

@ -25,8 +25,8 @@ public class Geary.Imap.IdleCommand : Command {
private GLib.Cancellable? exit_cancellable = new GLib.Cancellable();
public IdleCommand() {
base(NAME);
public IdleCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.exit_lock = new Geary.Nonblocking.Semaphore(this.exit_cancellable);
}

View file

@ -38,16 +38,22 @@ public class Geary.Imap.ListCommand : Command {
*
* See [[http://redmine.yorba.org/issues/7624]] for more information.
*/
public ListCommand(MailboxSpecifier mailbox, bool use_xlist, ListReturnParameter? return_param) {
base(use_xlist ? XLIST_NAME : NAME, { "" });
public ListCommand(MailboxSpecifier mailbox,
bool use_xlist,
ListReturnParameter? return_param,
GLib.Cancellable? should_send) {
base(use_xlist ? XLIST_NAME : NAME, { "" }, should_send);
this.args.add(mailbox.to_parameter());
add_return_parameter(return_param);
}
public ListCommand.wildcarded(string reference, MailboxSpecifier mailbox, bool use_xlist,
ListReturnParameter? return_param) {
base(use_xlist ? XLIST_NAME : NAME, { reference });
public ListCommand.wildcarded(string reference,
MailboxSpecifier mailbox,
bool use_xlist,
ListReturnParameter? return_param,
GLib.Cancellable? should_send) {
base(use_xlist ? XLIST_NAME : NAME, { reference }, should_send);
this.args.add(mailbox.to_parameter());
add_return_parameter(return_param);

View file

@ -9,14 +9,17 @@
*/
public class Geary.Imap.LoginCommand : Command {
public const string NAME = "login";
public LoginCommand(string user, string pass) {
base (NAME, { user, pass });
public LoginCommand(string user,
string pass,
GLib.Cancellable? should_send) {
base(NAME, { user, pass }, should_send);
}
public override string to_string() {
return "%s %s <user> <pass>".printf(tag.to_string(), name);
}
}
}

View file

@ -9,10 +9,11 @@
*/
public class Geary.Imap.LogoutCommand : Command {
public const string NAME = "logout";
public LogoutCommand() {
base (NAME);
public LogoutCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}
}

View file

@ -17,8 +17,8 @@ public class Geary.Imap.NamespaceCommand : Command {
public const string NAME = "NAMESPACE";
public NamespaceCommand() {
base(NAME);
public NamespaceCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}

View file

@ -11,10 +11,11 @@
*/
public class Geary.Imap.NoopCommand : Command {
public const string NAME = "noop";
public NoopCommand() {
base (NAME);
public NoopCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}
}

View file

@ -15,8 +15,9 @@ public class Geary.Imap.SearchCommand : Command {
public const string NAME = "search";
public const string UID_NAME = "uid search";
public SearchCommand(SearchCriteria criteria) {
base(NAME);
public SearchCommand(SearchCriteria criteria,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
// Extend rather than append the criteria, so the top-level
// criterion appear in the top-level list and not as a child
@ -24,8 +25,9 @@ public class Geary.Imap.SearchCommand : Command {
this.args.extend(criteria);
}
public SearchCommand.uid(SearchCriteria criteria) {
base(UID_NAME);
public SearchCommand.uid(SearchCriteria criteria,
GLib.Cancellable? should_send) {
base(UID_NAME, null, should_send);
// Extend rather than append the criteria, so the top-level
// criterion appear in the top-level list and not as a child

View file

@ -16,8 +16,9 @@ public class Geary.Imap.SelectCommand : Command {
public MailboxSpecifier mailbox { get; private set; }
public SelectCommand(MailboxSpecifier mailbox) {
base(NAME);
public SelectCommand(MailboxSpecifier mailbox,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.mailbox = mailbox;
this.args.add(mailbox.to_parameter());
}

View file

@ -9,10 +9,11 @@
*/
public class Geary.Imap.StarttlsCommand : Command {
public const string NAME = "STARTTLS";
public StarttlsCommand() {
base (NAME);
public StarttlsCommand(GLib.Cancellable? should_send) {
base(NAME, null, should_send);
}
}
}

View file

@ -18,8 +18,10 @@ public class Geary.Imap.StatusCommand : Command {
public const string NAME = "STATUS";
public StatusCommand(MailboxSpecifier mailbox, StatusDataType[] data_items) {
base (NAME);
public StatusCommand(MailboxSpecifier mailbox,
StatusDataType[] data_items,
GLib.Cancellable? should_send) {
base(NAME, null, should_send);
this.args.add(mailbox.to_parameter());

View file

@ -30,8 +30,11 @@ public class Geary.Imap.StoreCommand : Command {
SILENT
}
public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, Option options) {
base (message_set.is_uid ? UID_NAME : NAME);
public StoreCommand(MessageSet message_set,
Gee.List<MessageFlag> flag_list,
Option options,
GLib.Cancellable? should_send) {
base(message_set.is_uid ? UID_NAME : NAME, null, should_send);
bool add_flag = (options & Option.ADD_FLAGS) != 0;
bool silent = (options & Option.SILENT) != 0;

View file

@ -576,8 +576,8 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source {
private void on_idle_timeout() {
debug("Initiating IDLE");
try {
this.send_command(new IdleCommand());
} catch (ImapError err) {
this.send_command(new IdleCommand(this.open_cancellable));
warning("Error sending IDLE: %s", err.message);
}
}

View file

@ -159,10 +159,13 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
this.cmd = cmd;
}
public override async Object? execute_async(Cancellable? cancellable) throws Error {
response = yield owner.command_transaction_async(cmd, cancellable);
return response;
public override async Object? execute_async(GLib.Cancellable? cancellable)
throws GLib.Error {
// The command's should_send cancellable will be used to
// cancel the command if needed, so don't need to check or
// pass this method's cancellable through.
this.response = yield owner.submit_command(cmd);
return this.response;
}
}
@ -903,7 +906,9 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
switch (credentials.supported_method) {
case Geary.Credentials.Method.PASSWORD:
cmd = new LoginCommand(
credentials.user, credentials.token
credentials.user,
credentials.token,
cancellable
);
break;
@ -915,7 +920,9 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
);
}
cmd = new AuthenticateCommand.oauth2(
credentials.user, credentials.token
credentials.user,
credentials.token,
cancellable
);
break;
@ -936,10 +943,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// should always proceed; only an Error could change this
assert(params.proceed);
StatusResponse response = yield command_transaction_async(
cmd, cancellable
);
StatusResponse response = yield submit_command(cmd);
if (response.status != Status.OK) {
// Throw an error indicating auth failed here, unless
// there is a status response and it indicates that the
@ -987,7 +991,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
throws GLib.Error {
// If no capabilities available, get them now
if (this.capabilities.is_empty()) {
yield send_command_async(new CapabilityCommand(), cancellable);
yield send_command_async(new CapabilityCommand(cancellable));
}
var last_capabilities = this.capabilities.revision;
@ -1000,7 +1004,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
debug("Attempting STARTTLS...");
StatusResponse resp = yield send_command_async(
new StarttlsCommand(), cancellable
new StarttlsCommand(cancellable)
);
if (resp.status == Status.OK) {
@ -1021,7 +1025,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// mitigate main-in-the-middle attacks. If the TLS
// command response did not update capabilities,
// explicitly do so now.
yield send_command_async(new CapabilityCommand(), cancellable);
yield send_command_async(new CapabilityCommand(cancellable));
last_capabilities = this.capabilities.revision;
}
}
@ -1031,7 +1035,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// if new capabilities not offered after login, get them now
if (last_capabilities == capabilities.revision) {
yield send_command_async(new CapabilityCommand(), cancellable);
yield send_command_async(new CapabilityCommand(cancellable));
}
var list_results = new Gee.ArrayList<MailboxInformation>();
@ -1041,8 +1045,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
try {
// Determine what this connection calls the inbox
Imap.StatusResponse response = yield send_command_async(
new ListCommand(MailboxSpecifier.inbox, false, null),
cancellable
new ListCommand(MailboxSpecifier.inbox, false, null, cancellable)
);
if (response.status == Status.OK && !list_results.is_empty) {
this.inbox = list_results[0];
@ -1055,8 +1058,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// Try to determine what the connection's namespaces are
if (this.capabilities.has_capability(Capabilities.NAMESPACE)) {
response = yield send_command_async(
new NamespaceCommand(),
cancellable
new NamespaceCommand(cancellable)
);
if (response.status != Status.OK) {
warning("NAMESPACE command failed");
@ -1082,8 +1084,12 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// it. In particular, uw-imap sends a null prefix
// for the inbox.
response = yield send_command_async(
new ListCommand(new MailboxSpecifier(prefix), false, null),
cancellable
new ListCommand(
new MailboxSpecifier(prefix),
false,
null,
cancellable
)
);
if (response.status == Status.OK && !list_results.is_empty) {
MailboxInformation list = list_results[0];
@ -1243,7 +1249,10 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// is now dead
keepalive_id = 0;
send_command_async.begin(new NoopCommand(), null, on_keepalive_completed);
send_command_async.begin(
new NoopCommand(null),
on_keepalive_completed
);
debug("Sending keepalive...");
// No need to reschedule keepalive, as the notification that the command was sent should
@ -1264,8 +1273,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// send commands
//
public async StatusResponse send_command_async(Command cmd,
GLib.Cancellable? cancellable)
public async StatusResponse send_command_async(Command cmd)
throws GLib.Error {
check_unsupported_send_command(cmd);
@ -1277,7 +1285,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
assert(params.proceed);
return yield command_transaction_async(cmd, cancellable);
return yield submit_command(cmd);
}
public async Gee.Map<Command, StatusResponse>
@ -1402,9 +1410,9 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// Ternary troubles
Command cmd;
if (is_select)
cmd = new SelectCommand(mailbox);
cmd = new SelectCommand(mailbox, cancellable);
else
cmd = new ExamineCommand(mailbox);
cmd = new ExamineCommand(mailbox, cancellable);
MachineParams params = new MachineParams(cmd);
fsm.issue(Event.SELECT, null, params);
@ -1414,7 +1422,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
assert(params.proceed);
return yield command_transaction_async(cmd, cancellable);
return yield submit_command(cmd);
}
private uint on_select(uint state, uint event, void *user, Object? object) {
@ -1471,7 +1479,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
public async StatusResponse close_mailbox_async(GLib.Cancellable? cancellable)
throws GLib.Error {
CloseCommand cmd = new CloseCommand();
CloseCommand cmd = new CloseCommand(cancellable);
MachineParams params = new MachineParams(cmd);
fsm.issue(Event.CLOSE_MAILBOX, null, params);
@ -1479,7 +1487,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
if (params.err != null)
throw params.err;
return yield command_transaction_async(cmd, cancellable);
return yield submit_command(cmd);
}
private uint on_close_mailbox(uint state, uint event, void *user, Object? object) {
@ -1527,7 +1535,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
*/
public async void logout_async(GLib.Cancellable? cancellable)
throws GLib.Error {
LogoutCommand cmd = new LogoutCommand();
LogoutCommand cmd = new LogoutCommand(cancellable);
MachineParams params = new MachineParams(cmd);
fsm.issue(Event.LOGOUT, null, params);
@ -1536,7 +1544,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
throw params.err;
if (params.proceed) {
yield command_transaction_async(cmd, cancellable);
yield submit_command(cmd);
yield do_disconnect(DisconnectReason.LOCAL_CLOSE);
}
}
@ -1779,11 +1787,13 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
// command submission
//
private async StatusResponse command_transaction_async(Command cmd, Cancellable? cancellable)
throws Error {
if (this.cx == null)
throw new ImapError.NOT_CONNECTED("Not connected to %s", imap_endpoint.to_string());
private async StatusResponse submit_command(Command cmd)
throws GLib.Error {
if (this.cx == null) {
throw new ImapError.NOT_CONNECTED(
"Not connected to %s", imap_endpoint.to_string()
);
}
this.cx.send_command(cmd);
// Once a command has been sent over the wire, it can't be

View file

@ -16,7 +16,7 @@ class Geary.Imap.CreateCommandTest : TestCase {
public void basic_create() throws Error {
assert_equal(
new CreateCommand(new MailboxSpecifier("owatagusiam/")).to_string(),
new CreateCommand(new MailboxSpecifier("owatagusiam/"), null).to_string(),
"---- create owatagusiam/"
);
}
@ -25,7 +25,8 @@ class Geary.Imap.CreateCommandTest : TestCase {
assert_equal(
new CreateCommand.special_use(
new MailboxSpecifier("Everything"),
ALL_MAIL
ALL_MAIL,
null
).to_string(),
"---- create Everything (use (\\All))"
);

View file

@ -30,7 +30,7 @@ class Geary.Imap.FetchCommandTest : TestCase {
data_items.add(FetchDataSpecifier.UID);
assert_equal(
new FetchCommand(this.msg_set, data_items, null).to_string(),
new FetchCommand(this.msg_set, data_items, null, null).to_string(),
"---- fetch 1 uid"
);
}
@ -45,7 +45,7 @@ class Geary.Imap.FetchCommandTest : TestCase {
);
assert_equal(
new FetchCommand(this.msg_set, null, body_items).to_string(),
new FetchCommand(this.msg_set, null, body_items, null).to_string(),
"---- fetch 1 body[text]"
);
}
@ -57,7 +57,7 @@ class Geary.Imap.FetchCommandTest : TestCase {
data_items.add(FetchDataSpecifier.BODY);
assert_equal(
new FetchCommand(this.msg_set, data_items, null).to_string(),
new FetchCommand(this.msg_set, data_items, null, null).to_string(),
"---- fetch 1 (uid body)"
);
}
@ -77,7 +77,7 @@ class Geary.Imap.FetchCommandTest : TestCase {
);
assert_equal(
new FetchCommand(this.msg_set, null, body_items).to_string(),
new FetchCommand(this.msg_set, null, body_items, null).to_string(),
"---- fetch 1 (body[header] body[text])"
);
}
@ -102,7 +102,7 @@ class Geary.Imap.FetchCommandTest : TestCase {
);
assert_equal(
new FetchCommand(this.msg_set, data_items, body_items).to_string(),
new FetchCommand(this.msg_set, data_items, body_items, null).to_string(),
"---- fetch 1 (uid flags body[header] body[text])"
);
}

View file

@ -11,7 +11,7 @@ class Geary.Imap.ClientConnectionTest : TestCase {
private class TestCommand : Command {
public TestCommand() {
base("TEST");
base("TEST", null, null);
}
}