diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b95653ac..b9c6fc2f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -203,6 +203,7 @@ engine/imap-engine/other/imap-engine-other-account.vala engine/imap-engine/other/imap-engine-other-folder.vala engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala engine/imap-engine/replay-ops/imap-engine-copy-email.vala +engine/imap-engine/replay-ops/imap-engine-create-email.vala engine/imap-engine/replay-ops/imap-engine-expunge-email.vala engine/imap-engine/replay-ops/imap-engine-fetch-email.vala engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index a337142e..ecdc737d 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -182,6 +182,7 @@ public class ComposerWindow : Gtk.Window { private Geary.EmailIdentifier? draft_id = null; private uint draft_save_timeout_id = 0; private Cancellable cancellable_drafts = new Cancellable(); + private Cancellable cancellable_save_draft = new Cancellable(); private WebKit.WebView editor; // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get @@ -764,6 +765,8 @@ public class ComposerWindow : Gtk.Window { // Used internally by on_send() private async void on_send_async() { + cancellable_save_draft.cancel(); + linkify_document(editor.get_dom_document()); // Perform send. @@ -804,12 +807,12 @@ public class ComposerWindow : Gtk.Window { // Save to the draft folder, if available. // Note that drafts are NOT "linkified." private bool save_draft() { - save_async.begin(); + save_async.begin(cancellable_save_draft); return false; } - private async void save_async() { + private async void save_async(Cancellable? cancellable) { if (drafts_folder == null) return; @@ -818,7 +821,7 @@ public class ComposerWindow : Gtk.Window { try { draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email( - get_composed_email()), new Geary.EmailFlags(), null, draft_id, null); + get_composed_email()), new Geary.EmailFlags(), null, draft_id, cancellable); draft_save_label.label = DRAFT_SAVED_TEXT; } catch (Error e) { @@ -852,7 +855,7 @@ public class ComposerWindow : Gtk.Window { make_gui_insensitive(); // Do the save. - yield save_async(); + yield save_async(null); destroy(); } diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala b/src/engine/imap-engine/imap-engine-generic-folder.vala index a977336b..e5df6564 100644 --- a/src/engine/imap-engine/imap-engine-generic-folder.vala +++ b/src/engine/imap-engine/imap-engine-generic-folder.vala @@ -1156,30 +1156,32 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde if (id != null) check_id("create_email_async", id); - // TODO: Move this into a ReplayQueue operation - yield wait_for_open_async(cancellable); - - // use IMAP APPEND command on remote folders, which doesn't require opening a folder - Geary.EmailIdentifier? ret = yield remote_folder.create_email_async(rfc822, flags, - date_received, cancellable); - if (ret != null) { - // TODO: need to prevent gaps that may occur here - Geary.Email created = new Geary.Email(ret); - Gee.Map results = yield local_folder.create_or_merge_email_async( - new Collection.SingleItem(created), cancellable); - if (results.size > 0) - ret = Collection.get_first(results.keys).id; + Error? cancel_error = null; + Geary.EmailIdentifier? ret = null; + try { + CreateEmail create = new CreateEmail(this, rfc822, flags, date_received, cancellable); + replay_queue.schedule(create); + yield create.wait_for_ready_async(cancellable); + + ret = create.created_id; + } catch (Error e) { + if (e is IOError.CANCELLED) + cancel_error = e; else - ret = null; + throw e; } // Remove old message. if (id != null) { Geary.FolderSupport.Remove? remove_folder = this as Geary.FolderSupport.Remove; if (remove_folder != null) - yield remove_folder.remove_single_email_async(id, cancellable); + yield remove_folder.remove_single_email_async(id, null); } + // If the user cancelled the operation, throw the error here. + if (cancel_error != null) + throw cancel_error; + return ret; } } diff --git a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala new file mode 100644 index 00000000..8523c6aa --- /dev/null +++ b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala @@ -0,0 +1,73 @@ +/* Copyright 2013 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperation { + public Geary.EmailIdentifier? created_id { get; private set; default = null; } + + private GenericFolder engine; + private RFC822.Message rfc822; + private Geary.EmailFlags? flags; + private DateTime? date_received; + private Cancellable? cancellable; + + public CreateEmail(GenericFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags, + DateTime? date_received, Cancellable? cancellable) { + base.only_remote("CreateEmail"); + + this.engine = engine; + + this.rfc822 = rfc822; + this.flags = flags; + this.date_received = date_received; + this.cancellable = cancellable; + } + + public override async ReplayOperation.Status replay_local_async() throws Error { + return ReplayOperation.Status.CONTINUE; + } + + public override void notify_remote_removed_ids(Gee.Collection ids) { + } + + public override async void backout_local_async() throws Error { + } + + public override string describe_state() { + return ""; + } + + public override async ReplayOperation.Status replay_remote_async() throws Error { + // Deal with cancellable manually since create_email_async cannot be cancelled. + if (cancellable.is_cancelled()) + throw new IOError.CANCELLED("CreateEmail op cancelled immediately"); + + // use IMAP APPEND command on remote folders, which doesn't require opening a folder + created_id = yield engine.remote_folder.create_email_async(rfc822, flags, date_received); + + if (created_id == null) + return ReplayOperation.Status.FAILED; + + // If the user cancelled the operation, we need to wipe the new message to keep this + // operation atomic. + if (cancellable.is_cancelled()) { + yield engine.remote_folder.remove_email_async( + new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid), null); + + throw new IOError.CANCELLED("CreateEmail op cancelled after create"); + } + + // TODO: need to prevent gaps that may occur here + Geary.Email created = new Geary.Email(created_id); + Gee.Map results = yield engine.local_folder.create_or_merge_email_async( + new Collection.SingleItem(created), cancellable); + if (results.size > 0) + created_id = Collection.get_first(results.keys).id; + else + created_id = null; + + return ReplayOperation.Status.COMPLETED; + } +} diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 985f863e..1b66cbf2 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -862,8 +862,11 @@ private class Geary.Imap.Folder : BaseObject { } // 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, Cancellable? cancellable) throws Error { + DateTime? date_received) throws Error { check_open(); MessageFlags? msg_flags = null; @@ -880,7 +883,7 @@ private class Geary.Imap.Folder : BaseObject { msg_flags, internaldate, message.get_network_buffer(false)); Gee.Map responses = yield exec_commands_async( - new Collection.SingleItem(cmd), null, null, cancellable); + new Collection.SingleItem(cmd), null, null, null); // Grab the response and parse out the UID, if available. StatusResponse response = responses.get(cmd);