[geary/mjog/imap-command-cancellation: 2/6] Geary.Imap: Make command cancellable a property of the command object




commit 85e9046c717e520ee55e927e8709251f1ec1ff4e
Author: Michael Gratton <mike vee net>
Date:   Tue Sep 1 22:42:41 2020 +1000

    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.

 src/console/main.vala                              |  84 +++++++++----
 src/engine/imap/api/imap-account-session.vala      |  26 ++--
 src/engine/imap/api/imap-client-service.vala       |   2 +-
 src/engine/imap/api/imap-folder-session.vala       | 136 +++++++++++++++------
 src/engine/imap/command/imap-append-command.vala   |   9 +-
 .../imap/command/imap-authenticate-command.vala    |  12 +-
 .../imap/command/imap-capability-command.vala      |   7 +-
 src/engine/imap/command/imap-close-command.vala    |   7 +-
 src/engine/imap/command/imap-command.vala          |  32 ++++-
 src/engine/imap/command/imap-compress-command.vala |   8 +-
 src/engine/imap/command/imap-copy-command.vala     |   6 +-
 src/engine/imap/command/imap-create-command.vala   |   9 +-
 src/engine/imap/command/imap-delete-command.vala   |   5 +-
 src/engine/imap/command/imap-examine-command.vala  |   5 +-
 src/engine/imap/command/imap-expunge-command.vala  |   9 +-
 src/engine/imap/command/imap-fetch-command.vala    |  20 +--
 src/engine/imap/command/imap-id-command.vala       |   9 +-
 src/engine/imap/command/imap-idle-command.vala     |   4 +-
 src/engine/imap/command/imap-list-command.vala     |  16 ++-
 src/engine/imap/command/imap-login-command.vala    |   9 +-
 src/engine/imap/command/imap-logout-command.vala   |   7 +-
 .../imap/command/imap-namespace-command.vala       |   4 +-
 src/engine/imap/command/imap-noop-command.vala     |   7 +-
 src/engine/imap/command/imap-search-command.vala   |  10 +-
 src/engine/imap/command/imap-select-command.vala   |   5 +-
 src/engine/imap/command/imap-starttls-command.vala |   7 +-
 src/engine/imap/command/imap-status-command.vala   |   6 +-
 src/engine/imap/command/imap-store-command.vala    |   7 +-
 .../imap/transport/imap-client-connection.vala     |   2 +-
 src/engine/imap/transport/imap-client-session.vala |  82 +++++++------
 .../imap/command/imap-create-command-test.vala     |   5 +-
 .../imap/command/imap-fetch-command-test.vala      |  10 +-
 .../transport/imap-client-connection-test.vala     |   2 +-
 33 files changed, 381 insertions(+), 188 deletions(-)
---
diff --git a/src/console/main.vala b/src/console/main.vala
index b114c7e3a..015859862 100644
--- a/src/console/main.vala
+++ b/src/console/main.vala
@@ -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 {
diff --git a/src/engine/imap/api/imap-account-session.vala b/src/engine/imap/api/imap-account-session.vala
index 7c12f4d18..238bd85cc 100644
--- a/src/engine/imap/api/imap-account-session.vala
+++ b/src/engine/imap/api/imap-account-session.vala
@@ -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
diff --git a/src/engine/imap/api/imap-client-service.vala b/src/engine/imap/api/imap-client-service.vala
index b20da56c0..e0aad41b3 100644
--- a/src/engine/imap/api/imap-client-service.vala
+++ b/src/engine/imap/api/imap-client-service.vala
@@ -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);
diff --git a/src/engine/imap/api/imap-folder-session.vala b/src/engine/imap/api/imap-folder-session.vala
index 9bc61aca8..092c06cde 100644
--- a/src/engine/imap/api/imap-folder-session.vala
+++ b/src/engine/imap/api/imap-folder-session.vala
@@ -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);
diff --git a/src/engine/imap/command/imap-append-command.vala 
b/src/engine/imap/command/imap-append-command.vala
index d0cc6c780..44dec83f6 100644
--- a/src/engine/imap/command/imap-append-command.vala
+++ b/src/engine/imap/command/imap-append-command.vala
@@ -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());
 
diff --git a/src/engine/imap/command/imap-authenticate-command.vala 
b/src/engine/imap/command/imap-authenticate-command.vala
index 041de90e3..a7a0c1866 100644
--- a/src/engine/imap/command/imap-authenticate-command.vala
+++ b/src/engine/imap/command/imap-authenticate-command.vala
@@ -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,
diff --git a/src/engine/imap/command/imap-capability-command.vala 
b/src/engine/imap/command/imap-capability-command.vala
index 0f353997d..f32731553 100644
--- a/src/engine/imap/command/imap-capability-command.vala
+++ b/src/engine/imap/command/imap-capability-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-close-command.vala b/src/engine/imap/command/imap-close-command.vala
index 1dac6a97b..96f154270 100644
--- a/src/engine/imap/command/imap-close-command.vala
+++ b/src/engine/imap/command/imap-close-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-command.vala b/src/engine/imap/command/imap-command.vala
index 581636710..94d877620 100644
--- a/src/engine/imap/command/imap-command.vala
+++ b/src/engine/imap/command/imap-command.vala
@@ -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() {
diff --git a/src/engine/imap/command/imap-compress-command.vala 
b/src/engine/imap/command/imap-compress-command.vala
index 98ca05fff..20ea0aa5a 100644
--- a/src/engine/imap/command/imap-compress-command.vala
+++ b/src/engine/imap/command/imap-compress-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-copy-command.vala b/src/engine/imap/command/imap-copy-command.vala
index 2b25264c1..45a864751 100644
--- a/src/engine/imap/command/imap-copy-command.vala
+++ b/src/engine/imap/command/imap-copy-command.vala
@@ -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());
diff --git a/src/engine/imap/command/imap-create-command.vala 
b/src/engine/imap/command/imap-create-command.vala
index 3cbf3feee..4a56712b6 100644
--- a/src/engine/imap/command/imap-create-command.vala
+++ b/src/engine/imap/command/imap-create-command.vala
@@ -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);
diff --git a/src/engine/imap/command/imap-delete-command.vala 
b/src/engine/imap/command/imap-delete-command.vala
index e183f5107..dc35f58c2 100644
--- a/src/engine/imap/command/imap-delete-command.vala
+++ b/src/engine/imap/command/imap-delete-command.vala
@@ -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());
     }
 
diff --git a/src/engine/imap/command/imap-examine-command.vala 
b/src/engine/imap/command/imap-examine-command.vala
index 5b2e28376..4820bf334 100644
--- a/src/engine/imap/command/imap-examine-command.vala
+++ b/src/engine/imap/command/imap-examine-command.vala
@@ -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());
     }
diff --git a/src/engine/imap/command/imap-expunge-command.vala 
b/src/engine/imap/command/imap-expunge-command.vala
index be5b3445a..7eb533160 100644
--- a/src/engine/imap/command/imap-expunge-command.vala
+++ b/src/engine/imap/command/imap-expunge-command.vala
@@ -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());
     }
diff --git a/src/engine/imap/command/imap-fetch-command.vala b/src/engine/imap/command/imap-fetch-command.vala
index 498b6779e..601494d75 100644
--- a/src/engine/imap/command/imap-fetch-command.vala
+++ b/src/engine/imap/command/imap-fetch-command.vala
@@ -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);
 
diff --git a/src/engine/imap/command/imap-id-command.vala b/src/engine/imap/command/imap-id-command.vala
index b3be2451c..459c3c2f2 100644
--- a/src/engine/imap/command/imap-id-command.vala
+++ b/src/engine/imap/command/imap-id-command.vala
@@ -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);
     }
 
diff --git a/src/engine/imap/command/imap-idle-command.vala b/src/engine/imap/command/imap-idle-command.vala
index 069bea5b9..df36b2df8 100644
--- a/src/engine/imap/command/imap-idle-command.vala
+++ b/src/engine/imap/command/imap-idle-command.vala
@@ -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);
     }
 
diff --git a/src/engine/imap/command/imap-list-command.vala b/src/engine/imap/command/imap-list-command.vala
index 48eba3323..5ca6d232f 100644
--- a/src/engine/imap/command/imap-list-command.vala
+++ b/src/engine/imap/command/imap-list-command.vala
@@ -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);
diff --git a/src/engine/imap/command/imap-login-command.vala b/src/engine/imap/command/imap-login-command.vala
index c31147806..48bb4d8ef 100644
--- a/src/engine/imap/command/imap-login-command.vala
+++ b/src/engine/imap/command/imap-login-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-logout-command.vala 
b/src/engine/imap/command/imap-logout-command.vala
index 7e81fc541..9ff7d83c2 100644
--- a/src/engine/imap/command/imap-logout-command.vala
+++ b/src/engine/imap/command/imap-logout-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-namespace-command.vala 
b/src/engine/imap/command/imap-namespace-command.vala
index 8cf26b264..8e80a5d0e 100644
--- a/src/engine/imap/command/imap-namespace-command.vala
+++ b/src/engine/imap/command/imap-namespace-command.vala
@@ -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);
     }
 
 }
diff --git a/src/engine/imap/command/imap-noop-command.vala b/src/engine/imap/command/imap-noop-command.vala
index 4d894723d..ccf440b7d 100644
--- a/src/engine/imap/command/imap-noop-command.vala
+++ b/src/engine/imap/command/imap-noop-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-search-command.vala 
b/src/engine/imap/command/imap-search-command.vala
index 86448b741..4ea44f807 100644
--- a/src/engine/imap/command/imap-search-command.vala
+++ b/src/engine/imap/command/imap-search-command.vala
@@ -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
diff --git a/src/engine/imap/command/imap-select-command.vala 
b/src/engine/imap/command/imap-select-command.vala
index 5e7013a01..cb6844bae 100644
--- a/src/engine/imap/command/imap-select-command.vala
+++ b/src/engine/imap/command/imap-select-command.vala
@@ -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());
     }
diff --git a/src/engine/imap/command/imap-starttls-command.vala 
b/src/engine/imap/command/imap-starttls-command.vala
index 74e5ad714..24ba4d700 100644
--- a/src/engine/imap/command/imap-starttls-command.vala
+++ b/src/engine/imap/command/imap-starttls-command.vala
@@ -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);
     }
-}
 
+}
diff --git a/src/engine/imap/command/imap-status-command.vala 
b/src/engine/imap/command/imap-status-command.vala
index 388588b56..da2b3c23e 100644
--- a/src/engine/imap/command/imap-status-command.vala
+++ b/src/engine/imap/command/imap-status-command.vala
@@ -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());
 
diff --git a/src/engine/imap/command/imap-store-command.vala b/src/engine/imap/command/imap-store-command.vala
index 4264e03bb..4fbd614a1 100644
--- a/src/engine/imap/command/imap-store-command.vala
+++ b/src/engine/imap/command/imap-store-command.vala
@@ -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;
diff --git a/src/engine/imap/transport/imap-client-connection.vala 
b/src/engine/imap/transport/imap-client-connection.vala
index 927ac3bdc..82822d311 100644
--- a/src/engine/imap/transport/imap-client-connection.vala
+++ b/src/engine/imap/transport/imap-client-connection.vala
@@ -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);
         }
     }
diff --git a/src/engine/imap/transport/imap-client-session.vala 
b/src/engine/imap/transport/imap-client-session.vala
index 6edbacf09..f42112f26 100644
--- a/src/engine/imap/transport/imap-client-session.vala
+++ b/src/engine/imap/transport/imap-client-session.vala
@@ -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
diff --git a/test/engine/imap/command/imap-create-command-test.vala 
b/test/engine/imap/command/imap-create-command-test.vala
index 91933c7a7..03cd9b63e 100644
--- a/test/engine/imap/command/imap-create-command-test.vala
+++ b/test/engine/imap/command/imap-create-command-test.vala
@@ -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))"
         );
diff --git a/test/engine/imap/command/imap-fetch-command-test.vala 
b/test/engine/imap/command/imap-fetch-command-test.vala
index fdb56d4d0..8731943c3 100644
--- a/test/engine/imap/command/imap-fetch-command-test.vala
+++ b/test/engine/imap/command/imap-fetch-command-test.vala
@@ -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])"
         );
     }
diff --git a/test/engine/imap/transport/imap-client-connection-test.vala 
b/test/engine/imap/transport/imap-client-connection-test.vala
index 9bc745dab..2038221ff 100644
--- a/test/engine/imap/transport/imap-client-connection-test.vala
+++ b/test/engine/imap/transport/imap-client-connection-test.vala
@@ -11,7 +11,7 @@ class Geary.Imap.ClientConnectionTest : TestCase {
     private class TestCommand : Command {
 
         public TestCommand() {
-            base("TEST");
+            base("TEST", null, null);
         }
 
     }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]