[geary] New DraftManager to save/delete drafts w/o orphans



commit a751f66fbbcb0b7419a5dedf76032ac63ae7c27d
Author: Jim Nelson <jim yorba org>
Date:   Mon Jan 12 16:06:09 2015 -0800

    New DraftManager to save/delete drafts w/o orphans
    
    Introduces a DraftManager class which handles saving and deleting
    drafts for the composer and error situations.  Not perfect yet w/o
    better support in the Engine for dealing with network disconnects
    and offline mode, but much better than before for ensuring that drafts
    are not orphaned in the Drafts folder.

 src/CMakeLists.txt                                 |    1 +
 src/client/composer/composer-widget.vala           |  259 ++++++-----
 src/engine/api/geary-composed-email.vala           |    4 +
 src/engine/api/geary-email-flags.vala              |   12 +
 src/engine/app/app-draft-manager.vala              |  465 ++++++++++++++++++++
 .../app-conversation-operation-queue.vala          |    2 +-
 src/engine/nonblocking/nonblocking-mailbox.vala    |    2 +-
 7 files changed, 629 insertions(+), 116 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b0db690..052131d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -44,6 +44,7 @@ engine/api/geary-special-folder-type.vala
 
 engine/app/app-conversation.vala
 engine/app/app-conversation-monitor.vala
+engine/app/app-draft-manager.vala
 engine/app/app-email-store.vala
 
 engine/app/conversation-monitor/app-append-operation.vala
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 5a6e422..0767dc0 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -237,12 +237,10 @@ public class ComposerWidget : Gtk.EventBox {
     private string reply_message_id = "";
     private bool top_posting = true;
     
-    private Geary.FolderSupport.Create? drafts_folder = null;
-    private Geary.EmailIdentifier? draft_id = null;
+    private Geary.App.DraftManager? draft_manager = null;
+    private Geary.EmailIdentifier? editing_draft_id = null;
+    private Geary.EmailFlags draft_flags = new Geary.EmailFlags.with(Geary.EmailFlags.DRAFT);
     private uint draft_save_timeout_id = 0;
-    private Cancellable cancellable_drafts = new Cancellable();
-    private Cancellable cancellable_save_draft = new Cancellable();
-    private bool in_draft_save = false;
     
     public WebKit.WebView editor;
     // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get
@@ -443,7 +441,7 @@ public class ComposerWidget : Gtk.EventBox {
                     }
                     
                     if (is_referred_draft)
-                        draft_id = referred.id;
+                        editing_draft_id = referred.id;
                     
                     add_attachments(referred.attachments);
                 break;
@@ -558,12 +556,12 @@ public class ComposerWidget : Gtk.EventBox {
         chain.append(attachments_box);
         box.set_focus_chain(chain);
         
-        // If there's only one account, open the drafts folder.  If there's more than one account,
-        // the drafts folder will be opened by on_from_changed().
+        // If there's only one account, open the drafts manager.  If there's more than one account,
+        // the drafts manager will be opened by on_from_changed().
         if (!from_multiple.visible)
-            open_drafts_folder_async.begin(cancellable_drafts);
+            open_draft_manager_async.begin(null);
         
-        destroy.connect(() => { close_drafts_folder_async.begin(); });
+        destroy.connect(() => { close_draft_manager_async.begin(null); });
     }
     
     public ComposerWidget.from_mailto(Geary.Account account, string mailto) {
@@ -891,9 +889,10 @@ public class ComposerWidget : Gtk.EventBox {
     }
     
     private bool can_save() {
-        return (drafts_folder != null && drafts_folder.get_open_state() == Geary.Folder.OpenState.BOTH
-            && !drafts_folder.properties.create_never_returns_id && editor.can_undo()
-            && account.information.save_drafts);
+        return draft_manager != null
+            && draft_manager.is_open
+            && editor.can_undo()
+            && account.information.save_drafts;
     }
 
     public CloseStatus should_close() {
@@ -902,10 +901,7 @@ public class ComposerWidget : Gtk.EventBox {
         container.present();
         AlertDialog dialog;
         
-        if (drafts_folder == null && try_to_save) {
-            dialog = new ConfirmationDialog(container.top_window,
-                _("Do you want to discard the unsaved message?"), null, Stock._DISCARD);
-        } else if (try_to_save) {
+        if (try_to_save) {
             dialog = new TernaryConfirmationDialog(container.top_window,
                 _("Do you want to discard this message?"), null, Stock._KEEP, Stock._DISCARD,
                 Gtk.ResponseType.CLOSE);
@@ -919,13 +915,13 @@ public class ComposerWidget : Gtk.EventBox {
             return CloseStatus.CANCEL_CLOSE; // Cancel
         } else if (response == Gtk.ResponseType.OK) {
             if (try_to_save) {
-                save_and_exit.begin(); // Save
+                save_and_exit(); // Save
                 return CloseStatus.PENDING_CLOSE;
             } else {
                 return CloseStatus.DO_CLOSE;
             }
         } else {
-            delete_and_exit.begin(); // Discard
+            discard_and_exit(); // Discard
             return CloseStatus.PENDING_CLOSE;
         }
     }
@@ -1049,8 +1045,6 @@ public class ComposerWidget : Gtk.EventBox {
     
     // Used internally by on_send()
     private async void on_send_async() {
-        cancellable_save_draft.cancel();
-        
         container.vanish();
         
         linkify_document(editor.get_dom_document());
@@ -1062,83 +1056,159 @@ public class ComposerWidget : Gtk.EventBox {
             GLib.message("Error sending email: %s", e.message);
         }
         
-        yield delete_draft_async();
-        container.close_container(); // Only close window after draft is deleted; this closes the drafts 
folder.
+        Geary.Nonblocking.Semaphore? semaphore = discard_draft();
+        if (semaphore != null) {
+            try {
+                yield semaphore.wait_async();
+            } catch (Error err) {
+                // ignored
+            }
+        }
+        
+        // Only close window after draft is deleted; this closes the drafts folder.
+        container.close_container();
+    }
+    
+    private void on_draft_state_changed() {
+        switch (draft_manager.draft_state) {
+            case Geary.App.DraftManager.DraftState.STORED:
+                draft_save_text = DRAFT_SAVED_TEXT;
+            break;
+            
+            case Geary.App.DraftManager.DraftState.STORING:
+                draft_save_text = DRAFT_SAVING_TEXT;
+            break;
+            
+            case Geary.App.DraftManager.DraftState.NOT_STORED:
+                draft_save_text = "";
+            break;
+            
+            case Geary.App.DraftManager.DraftState.ERROR:
+                draft_save_text = DRAFT_ERROR_TEXT;
+            break;
+            
+            default:
+                assert_not_reached();
+        }
     }
     
-    private void on_drafts_opened(Geary.Folder.OpenState open_state, int count) {
-        if (open_state == Geary.Folder.OpenState.BOTH)
-            reset_draft_timer();
+    private void on_draft_manager_fatal(Error err) {
+        draft_save_text = DRAFT_ERROR_TEXT;
+    }
+    
+    private void connect_to_draft_manager() {
+        draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].connect(on_draft_state_changed);
+        draft_manager.fatal.connect(on_draft_manager_fatal);
+    }
+    
+    // This code is in a separate method due to https://bugzilla.gnome.org/show_bug.cgi?id=742621
+    // connect_to_draft_manager() is simply for symmetry.  When above bug is fixed, this code can
+    // be moved back into open/close methods
+    private void disconnect_from_draft_manager() {
+        draft_manager.notify[Geary.App.DraftManager.PROP_DRAFT_STATE].disconnect(on_draft_state_changed);
+        draft_manager.fatal.disconnect(on_draft_manager_fatal);
     }
     
     // Returns the drafts folder for the current From account.
-    private async void open_drafts_folder_async(Cancellable cancellable) throws Error {
-        yield close_drafts_folder_async(cancellable);
+    private async void open_draft_manager_async(Cancellable? cancellable) throws Error {
+        yield close_draft_manager_async(cancellable);
         
         if (!account.information.save_drafts)
             return;
         
-        Geary.FolderSupport.Create? folder = (yield account.get_required_special_folder_async(
-            Geary.SpecialFolderType.DRAFTS, cancellable)) as Geary.FolderSupport.Create;
+        draft_manager = new Geary.App.DraftManager(account);
+        try {
+            yield draft_manager.open_async(editing_draft_id, cancellable);
+        } catch (Error err) {
+            debug("Unable to open draft manager %s: %s", draft_manager.to_string(), err.message);
+            
+            draft_manager = null;
+            
+            throw err;
+        }
+        
+        // clear now, as it was only needed to open draft manager
+        editing_draft_id = null;
+        
+        connect_to_draft_manager();
+    }
+    
+    private async void close_draft_manager_async(Cancellable? cancellable) throws Error {
+        // clear status text
+        draft_save_text = "";
+        
+        // only clear editing_draft_id if associated with prior draft_manager, not due to this
+        // widget being initialized with it
+        if (draft_manager == null)
+            return;
         
-        if (folder == null)
-            return; // No drafts folder.
+        disconnect_from_draft_manager();
         
-        yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN | Geary.Folder.OpenFlags.NO_DELAY,
-            cancellable);
+        // drop ref even if close failed
+        try {
+            yield draft_manager.close_async(cancellable);
+        } finally {
+            draft_manager = null;
+            editing_draft_id = null;
+        }
+    }
+    
+    // Resets the draft save timeout.
+    private void reset_draft_timer() {
+        draft_save_text = "";
+        cancel_draft_timer();
         
-        drafts_folder = folder;
-        drafts_folder.opened.connect(on_drafts_opened);
+        if (can_save())
+            draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
     }
     
-    private async void close_drafts_folder_async(Cancellable? cancellable = null) throws Error {
-        if (drafts_folder == null)
+    // Cancels the draft save timeout
+    private void cancel_draft_timer() {
+        if (draft_save_timeout_id == 0)
             return;
         
-        // Close existing folder.
-        drafts_folder.opened.disconnect(on_drafts_opened);
-        yield drafts_folder.close_async(cancellable);
-        drafts_folder = null;
+        Source.remove(draft_save_timeout_id);
+        draft_save_timeout_id = 0;
     }
     
-    // Save to the draft folder, if available.
-    // Note that drafts are NOT "linkified."
     private bool on_save_draft_timeout() {
-        // since all control paths return false, this is not rescheduled by the event loop, so
-        // kill the timeout id
+        // this is not rescheduled by the event loop, so kill the timeout id
         draft_save_timeout_id = 0;
         
-        if (in_draft_save)
-            return false;
-        
-        in_draft_save = true;
-        save_async.begin(cancellable_save_draft, () => { in_draft_save = false; });
+        save_draft();
         
         return false;
     }
     
-    private async void save_async(Cancellable? cancellable) {
-        if (drafts_folder == null || !can_save())
-            return;
-        
+    // Note that drafts are NOT "linkified."
+    private Geary.Nonblocking.Semaphore? save_draft() {
+        // cancel timer in favor of just doing it now
         cancel_draft_timer();
         
-        draft_save_text = DRAFT_SAVING_TEXT;
+        try {
+            if (draft_manager != null) {
+                return draft_manager.update(get_composed_email(null, true).to_rfc822_message(),
+                    draft_flags, null);
+            }
+        } catch (Error err) {
+            GLib.message("Unable to save draft: %s", err.message);
+        }
         
-        Geary.EmailFlags flags = new Geary.EmailFlags();
-        flags.add(Geary.EmailFlags.DRAFT);
+        return null;
+    }
+    
+    private Geary.Nonblocking.Semaphore? discard_draft() {
+        // cancel timer in favor of this operation
+        cancel_draft_timer();
         
         try {
-            // only save HTML drafts to avoid resetting the DOM (which happens when converting the
-            // HTML to flowed text)
-            draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email(
-                get_composed_email(null, true), null), flags, null, draft_id, cancellable);
-            
-            draft_save_text = DRAFT_SAVED_TEXT;
-        } catch (Error e) {
-            GLib.message("Error saving draft: %s", e.message);
-            draft_save_text = DRAFT_ERROR_TEXT;
+            if (draft_manager != null)
+                return draft_manager.discard();
+        } catch (Error err) {
+            GLib.message("Unable to discard draft: %s", err.message);
         }
+        
+        return null;
     }
     
     // Used while waiting for draft to save before closing widget.
@@ -1147,42 +1217,24 @@ public class ComposerWidget : Gtk.EventBox {
         cancel_draft_timer();
     }
     
-    private async void save_and_exit() {
+    private void save_and_exit() {
         make_gui_insensitive();
         
-        // Do the save.
-        yield save_async(null);
+        save_draft();
         
         container.close_container();
     }
     
-    private async void delete_and_exit() {
+    private void discard_and_exit() {
         make_gui_insensitive();
         
-        // Do the delete.
-        yield delete_draft_async();
+        discard_draft();
+        if (draft_manager != null)
+            draft_manager.discard_on_close = true;
         
         container.close_container();
     }
     
-    private async void delete_draft_async() {
-        if (drafts_folder == null || draft_id == null)
-            return;
-        
-        Geary.FolderSupport.Remove? removable_drafts = drafts_folder as Geary.FolderSupport.Remove;
-        if (removable_drafts == null) {
-            debug("Draft folder does not support remove.\n");
-            
-            return;
-        }
-        
-        try {
-            yield removable_drafts.remove_single_email_async(draft_id);
-        } catch (Error e) {
-            debug("Unable to delete draft: %s", e.message);
-        }
-    }
-    
     private void on_add_attachment_button_clicked() {
         AttachmentDialog dialog = null;
         do {
@@ -1903,27 +1955,6 @@ public class ComposerWidget : Gtk.EventBox {
         return false;
     }
     
-    // Resets the draft save timeout.
-    private void reset_draft_timer() {
-        if (!can_save())
-            return;
-        
-        draft_save_text = "";
-        cancel_draft_timer();
-        
-        if (drafts_folder != null)
-            draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
-    }
-    
-    // Cancels the draft save timeout
-    private void cancel_draft_timer() {
-        if (draft_save_timeout_id == 0)
-            return;
-        
-        Source.remove(draft_save_timeout_id);
-        draft_save_timeout_id = 0;
-    }
-    
     private void update_actions() {
         // Undo/redo.
         actions.get_action(ACTION_UNDO).sensitive = editor.can_undo();
@@ -2056,10 +2087,10 @@ public class ComposerWidget : Gtk.EventBox {
         // if the Geary.Account didn't change and the drafts folder is open(ing), do nothing more;
         // need to check for the drafts folder because opening it in the case of multiple From:
         // is handled here alone, so need to open it if not already
-        if (!changed && drafts_folder != null)
+        if (!changed && draft_manager != null)
             return;
         
-        open_drafts_folder_async.begin(cancellable_drafts);
+        open_draft_manager_async.begin(null);
         reset_draft_timer();
     }
     
diff --git a/src/engine/api/geary-composed-email.vala b/src/engine/api/geary-composed-email.vala
index a9f9403..e668141 100644
--- a/src/engine/api/geary-composed-email.vala
+++ b/src/engine/api/geary-composed-email.vala
@@ -45,5 +45,9 @@ public class Geary.ComposedEmail : BaseObject {
         this.body_text = body_text;
         this.body_html = body_html;
     }
+    
+    public Geary.RFC822.Message to_rfc822_message(string? message_id = null) {
+        return new RFC822.Message.from_composed_email(this, message_id);
+    }
 }
 
diff --git a/src/engine/api/geary-email-flags.vala b/src/engine/api/geary-email-flags.vala
index 2eddfec..b67c72e 100644
--- a/src/engine/api/geary-email-flags.vala
+++ b/src/engine/api/geary-email-flags.vala
@@ -41,6 +41,18 @@ public class Geary.EmailFlags : Geary.NamedFlags {
     public EmailFlags() {
     }
     
+    /**
+     * Create a new { link EmailFlags} container initialized with one or more flags.
+     */
+    public EmailFlags.with(Geary.NamedFlag flag1, ...) {
+        va_list args = va_list();
+        NamedFlag? flag = flag1;
+        
+        do {
+            add(flag);
+        } while((flag = args.arg()) != null);
+    }
+    
     // Convenience method to check if the unread flag is set.
     public inline bool is_unread() {
         return contains(UNREAD);
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
new file mode 100644
index 0000000..985f700
--- /dev/null
+++ b/src/engine/app/app-draft-manager.vala
@@ -0,0 +1,465 @@
+/* Copyright 2014 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.
+ */
+
+/**
+ * Manage saving, replacing, and deleting the various versions of a draft message while the user is
+ * editing it.
+ *
+ * Each composer should create a single DraftManager object for the lifetime of the compose
+ * session.  The DraftManager interface offers "fire-and-forget" nonblocking (but not
+ * asynchronous) methods for the composer to schedule remote operations without worrying about
+ * synchronization, operation ordering, error-handling, and so forth.
+ *
+ * If successive drafts are submitted for storage, drafts waiting in the queue (i.e. not yet sent
+ * to the server) are dropped without further consideration.  This prevents needless I/O with the
+ * server saving drafts that are only to be replaced by later versions.
+ *
+ * Important: This object should be used ''per'' composed email and not to manage multiple emails
+ * being composed to the same { link Account}.  DraftManager's internal state is solely for managing
+ * the lifecycle of a single email being composed by the user.
+ *
+ * The only async calls for DraftManager is { link open_async} and { link close_async}, which give
+ * it a chance to initialize and tear-down in an orderly manner.
+ */
+
+public class Geary.App.DraftManager : BaseObject {
+    public const string PROP_IS_OPEN = "is-open";
+    public const string PROP_DRAFT_STATE = "draft-state";
+    public const string PROP_VERSIONS_SAVED = "versions-saved";
+    public const string PROP_VERSIONS_DROPPED = "versions-dropped";
+    public const string PROP_DISCARD_ON_CLOSE = "discard-on-close";
+    
+    /**
+     * Current saved state of the draft.
+     */
+    public enum DraftState {
+        /**
+         * Indicates not stored on the remote server (either not uploaded or discarded).
+         */
+        NOT_STORED,
+        /**
+         * A save (upload) is in process.
+         */
+        STORING,
+        /**
+         * Draft is stored on the remote.
+         */
+        STORED,
+        /**
+         * A non-fatal error occurred attempting to store the draft.
+         *
+         * @see draft_failed
+         */
+        ERROR
+    }
+    
+    private enum OperationType {
+        PUSH,
+        CLOSE
+    }
+    
+    private class Operation : BaseObject {
+        public OperationType op_type;
+        public RFC822.Message? draft;
+        public EmailFlags? flags;
+        public DateTime? date_received;
+        public Nonblocking.Semaphore? semaphore;
+        
+        public Operation(OperationType op_type, RFC822.Message? draft, EmailFlags? flags,
+            DateTime? date_received, Nonblocking.Semaphore? semaphore) {
+            this.op_type = op_type;
+            this.draft = draft;
+            this.flags = flags;
+            this.date_received = date_received;
+            this.semaphore = semaphore;
+        }
+    }
+    
+    /**
+     * Indicates the { link DraftsManager} is open and ready for service.
+     *
+     * Although this property can be monitored, the object is considered "open" when
+     * { link open_async} completes, not when this property changes to true.
+     */
+    public bool is_open { get; private set; default = false; }
+    
+    /**
+     * The current saved state of the draft.
+     */
+    public DraftState draft_state { get; private set; default = DraftState.NOT_STORED; }
+    
+    /**
+     * The { link Geary.EmailIdentifier} of the last saved draft.
+     */
+    public Geary.EmailIdentifier? current_draft_id { get; private set; default = null; }
+    
+    /**
+     * The version number of the most recently saved draft.
+     *
+     * Even if an initial draft is supplied (with { link open_async}, this always starts at zero.
+     * It merely represents the number of times a draft was successfully saved.
+     *
+     * A { link discard} operation will reset this counter to zero.
+     */
+    public int versions_saved { get; private set; default = 0; }
+    
+    /**
+     * The number of drafts dropped as new ones are added to the queue.
+     *
+     * @see dropped
+     */
+    public int versions_dropped { get; private set; default = 0; }
+    
+    /**
+     * When set, the draft will be discarded when { link close_async} is called.
+     *
+     * In addition, when set all future { link update}s will result in the draft being dropped.
+     */
+    public bool discard_on_close { get; set; default = false; }
+    
+    private Account account;
+    private Folder? drafts_folder = null;
+    private FolderSupport.Create? create_support = null;
+    private FolderSupport.Remove? remove_support = null;
+    private Nonblocking.Mailbox<Operation?> mailbox = new Nonblocking.Mailbox<Operation?>();
+    private bool was_opened = false;
+    private Error? fatal_err = null;
+    
+    /**
+     * Fired when a draft is successfully saved.
+     */
+    public signal void stored(Geary.RFC822.Message draft);
+    
+    /**
+     * Fired when a draft is discarded.
+     */
+    public signal void discarded();
+    
+    /**
+     * Fired when a draft is dropped.
+     *
+     * This occurs when a draft is scheduled for { link update} while another draft is queued
+     * to be pushed to the server.  The queued draft is dropped in favor of the new one.
+     */
+    public signal void dropped(Geary.RFC822.Message draft);
+    
+    /**
+     * Fired when unable to save a draft but the { link DraftManager} remains open.
+     *
+     * Due to unpredictability of errors being reported, it's possible this signal will fire after
+     * { link fatal}.  It should not be assumed this signal firing means DraftManager is still
+     * operational, but if fatal fires, it definitely is not operational.
+     */
+    public virtual signal void draft_failed(Geary.RFC822.Message draft, Error err) {
+        debug("%s: Unable to create draft: %s", to_string(), err.message);
+    }
+    
+    /**
+     * Fired if an unrecoverable error occurs while processing drafts.
+     *
+     * The { link DraftManager} will be unable to process future drafts.
+     */
+    public virtual signal void fatal(Error err) {
+        fatal_err = err;
+        
+        debug("%s: Irrecoverable failure: %s", to_string(), err.message);
+    }
+    
+    public DraftManager(Geary.Account account) {
+        this.account = account;
+    }
+    
+    protected virtual void notify_stored(Geary.RFC822.Message draft) {
+        versions_saved++;
+        stored(draft);
+    }
+    
+    protected virtual void notify_discarded() {
+        versions_saved = 0;
+        discarded();
+    }
+    
+    /**
+     * Open the { link DraftManager} and prepare it for handling composed messages.
+     *
+     * An initial draft EmailIdentifier may be supplied indicating the starting draft (when editing,
+     * not creating a new draft).  No checking is performed to ensure this EmailIdentifier is valid
+     * for the drafts folder, nor is it downloaded by DraftManager.  In essence, this email is
+     * deleted by the manager when the first { link update} occurs or the draft is
+     * { link discard}ed.
+     *
+     * If an initial_draft is supplied, { link draft_state} will be set to { link DraftState.STORED}.
+     *
+     * Other method calls should not be invoked until this completes successfully.
+     *
+     * @see is_open
+     */
+    public async void open_async(Geary.EmailIdentifier? initial_draft_id, Cancellable? cancellable = null)
+        throws Error {
+        if (is_open)
+            throw new EngineError.ALREADY_OPEN("%s is already open", to_string());
+        else if (was_opened)
+            throw new EngineError.UNSUPPORTED("%s cannot be re-opened", to_string());
+        
+        was_opened = true;
+        
+        current_draft_id = initial_draft_id;
+        if (current_draft_id != null)
+            draft_state = DraftState.STORED;
+        
+        drafts_folder = account.get_special_folder(SpecialFolderType.DRAFTS);
+        if (drafts_folder == null)
+            throw new EngineError.NOT_FOUND("%s: No drafts folder found", to_string());
+        
+        // if drafts folder doesn't support create and remove, call it quits
+        create_support = drafts_folder as Geary.FolderSupport.Create;
+        remove_support = drafts_folder as Geary.FolderSupport.Remove;
+        if (create_support == null || remove_support == null) {
+            throw new EngineError.UNSUPPORTED("%s: Drafts folder %s does not support create and remove",
+                to_string(), drafts_folder.to_string());
+        }
+        
+        drafts_folder.closed.connect(on_folder_closed);
+        
+        yield drafts_folder.open_async(Folder.OpenFlags.NONE, cancellable);
+        
+        // if drafts folder doesn't return the identifier of newly created emails, then this object
+        // can't do it's work ... wait until open to check for this, to be absolutely sure
+        if (drafts_folder.properties.create_never_returns_id) {
+            try {
+                yield drafts_folder.close_async();
+            } catch (Error err) {
+                // ignore
+            }
+            
+            throw new EngineError.UNSUPPORTED("%s: Drafts folder %s does not return created mail ID",
+                to_string(), drafts_folder.to_string());
+        }
+        
+        // start the operation message loop, which ensures commands are handled in orderly fashion
+        operation_loop_async.begin();
+        
+        // done
+        is_open = true;
+    }
+    
+    private void on_folder_closed(Folder.CloseReason reason) {
+        if (reason == Folder.CloseReason.FOLDER_CLOSED) {
+            fatal(new EngineError.SERVER_UNAVAILABLE("%s: Unexpected drafts folder closed (%s)",
+                to_string(), reason.to_string()));
+        }
+    }
+    
+    /**
+     * Flush pending operations and close the { link DraftsManager}.
+     *
+     * Once closed, the object cannot be opened again.  Create a new object in that case.
+     *
+     * @see open_async
+     * @see is_open
+     */
+    public async void close_async(Cancellable? cancellable = null) throws Error {
+        if (!is_open || drafts_folder == null)
+            return;
+        
+        // prevent further operations
+        is_open = false;
+        
+        // don't flush a CLOSE down the pipe if failed, the operation loop is closed for business
+        if (fatal_err == null) {
+            // if discarding on close, do so now
+            if (discard_on_close) {
+                // don't use discard(), which checks if open, but submit_push() directly,
+                // which doesn't
+                submit_push(null, null, null);
+            }
+            
+            // flush pending I/O
+            Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore(cancellable);
+            mailbox.send(new Operation(OperationType.CLOSE, null, null, null, semaphore));
+            
+            // wait for close to complete
+            try {
+                yield semaphore.wait_async(cancellable);
+            } catch (Error err) {
+                if (err is IOError.CANCELLED)
+                    throw err;
+                
+                // fall through
+            }
+        }
+        
+        // Disconnect before closing, as signal handler is for unexpected closes
+        drafts_folder.closed.disconnect(on_folder_closed);
+        
+        try {
+            yield drafts_folder.close_async(cancellable);
+        } finally {
+            drafts_folder = null;
+            create_support = null;
+            remove_support = null;
+        }
+    }
+    
+    private void check_open() throws EngineError {
+        if (!is_open)
+            throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
+    }
+    
+    /**
+     * Save draft on the server, potentially replacing (deleting) an already-existing version.
+     *
+     * See { link FolderSupport.Create.create_email_async} for more information on the flags and
+     * date_received arguments.
+     *
+     * @returns A { link Semaphore} that is notified when the operation completes (with or without
+     * error)
+     */
+    public Geary.Nonblocking.Semaphore? update(Geary.RFC822.Message draft, Geary.EmailFlags? flags,
+        DateTime? date_received) throws Error {
+        check_open();
+        
+        return submit_push(draft, flags, date_received);
+    }
+    
+    /**
+     * Delete all versions of the composed email from the server.
+     *
+     * This is appropriate both for the user cancelling (discarding) a composed message or if the
+     * user sends it.
+     *
+     * Note: Replaced drafts are deleted, but on some services (i.e. Gmail) those deleted messages
+     * are actually moved to the Trash.  This call does not currently solve that problem.
+     *
+     * @returns A { link Semaphore} that is notified when the operation completes (with or without
+     * error)
+     */
+    public Geary.Nonblocking.Semaphore? discard() throws Error {
+        check_open();
+        
+        return submit_push(null, null, null);
+    }
+    
+    // Note that this call doesn't check_open(), important when used within close_async()
+    private Nonblocking.Semaphore? submit_push(RFC822.Message? draft, EmailFlags? flags,
+        DateTime? date_received) {
+        // no drafts are pushed when discarding on close
+        if (draft != null && discard_on_close) {
+            versions_dropped++;
+            dropped(draft);
+            
+            return null;
+        }
+        
+        // clear out pending pushes (which can be updates or discards)
+        mailbox.revoke_matching((op) => {
+            // count and notify of dropped drafts
+            if (op.op_type == OperationType.PUSH && op.draft != null) {
+                versions_dropped++;
+                dropped(op.draft);
+            }
+            
+            return op.op_type == OperationType.PUSH;
+        });
+        
+        Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore();
+        
+        // schedule this draft for update (if null, it's a discard)
+        mailbox.send(new Operation(OperationType.PUSH, draft, flags, date_received, semaphore));
+        
+        return semaphore;
+    }
+    
+    private async void operation_loop_async() {
+        for (;;) {
+            // if a fatal error occurred (it can happen outside the loop), shutdown without
+            // reporting it again
+            if (fatal_err != null)
+                break;
+            
+            Operation op;
+            try {
+                op = yield mailbox.recv_async(null);
+            } catch (Error err) {
+                fatal(err);
+                
+                break;
+            }
+            
+            bool continue_loop = yield operation_loop_iteration_async(op);
+            
+            // fire semaphore, if present
+            if (op.semaphore != null)
+                op.semaphore.blind_notify();
+            
+            if (!continue_loop)
+                break;
+        }
+    }
+    
+    // Returns false if time to exit.
+    private async bool operation_loop_iteration_async(Operation op) {
+        // watch for CLOSE
+        if (op.op_type == OperationType.CLOSE)
+            return false;
+        
+        // make sure there's a folder to work with
+        if (drafts_folder == null || drafts_folder.get_open_state() == Folder.OpenState.CLOSED) {
+            fatal(new EngineError.SERVER_UNAVAILABLE("%s: premature drafts folder close", to_string()));
+            
+            return false;
+        }
+        
+        // at this point, only operation left is PUSH
+        assert(op.op_type == OperationType.PUSH);
+        
+        draft_state = DraftState.STORING;
+        
+        // delete old draft for all PUSHes: best effort ... since create_email_async() will handle
+        // replacement in a transactional-kinda-way, only outright delete if not using create
+        if (current_draft_id != null && op.draft == null) {
+            bool success = false;
+            try {
+                yield remove_support.remove_single_email_async(current_draft_id);
+                success = true;
+            } catch (Error err) {
+                debug("%s: Unable to remove existing draft %s: %s", to_string(), 
current_draft_id.to_string(),
+                    err.message);
+            }
+            
+            // always clear draft id (assuming that retrying a failed remove is unnecessary), but
+            // only signal the discard if it actually was removed
+            current_draft_id = null;
+            if (success)
+                notify_discarded();
+        }
+        
+        // if draft supplied, save it
+        if (op.draft != null) {
+            try {
+                current_draft_id = yield create_support.create_email_async(op.draft, op.flags,
+                    op.date_received, current_draft_id, null);
+                
+                draft_state = DraftState.STORED;
+                notify_stored(op.draft);
+            } catch (Error err) {
+                draft_state = DraftState.ERROR;
+                
+                // notify subscribers
+                draft_failed(op.draft, err);
+            }
+        } else {
+            draft_state = DraftState.NOT_STORED;
+        }
+        
+        return true;
+    }
+    
+    public string to_string() {
+        return "%s DraftManager".printf(account.to_string());
+    }
+}
+
diff --git a/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala 
b/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
index 5d951ae..7577e4f 100644
--- a/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
+++ b/src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
@@ -23,7 +23,7 @@ private class Geary.App.ConversationOperationQueue : BaseObject {
         FillWindowOperation? fill_op = op as FillWindowOperation;
         if (fill_op != null) {
             Gee.Collection<ConversationOperation> removed
-                = mailbox.remove_matching(o => o is FillWindowOperation);
+                = mailbox.revoke_matching(o => o is FillWindowOperation);
             
             // If there were any "insert" fill window ops, preserve that flag,
             // as otherwise we might miss some data.
diff --git a/src/engine/nonblocking/nonblocking-mailbox.vala b/src/engine/nonblocking/nonblocking-mailbox.vala
index 52d45db..3337b44 100644
--- a/src/engine/nonblocking/nonblocking-mailbox.vala
+++ b/src/engine/nonblocking/nonblocking-mailbox.vala
@@ -73,7 +73,7 @@ public class Geary.Nonblocking.Mailbox<G> : BaseObject {
     /**
      * Remove messages matching the given predicate.  Return the removed messages.
      */
-    public Gee.Collection<G> remove_matching(owned Gee.Predicate<G> predicate) {
+    public Gee.Collection<G> revoke_matching(owned Gee.Predicate<G> predicate) {
         Gee.ArrayList<G> removed = new Gee.ArrayList<G>();
         // Iterate over a copy so we can modify the original.
         foreach (G msg in queue.to_array()) {


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