[geary] Retry commands properly if connection lost: Bug #714540



commit d4cc681e91ab1c5353f2516254c0b703ddd5d03c
Author: Jim Nelson <jim yorba org>
Date:   Mon Jan 26 17:07:10 2015 -0800

    Retry commands properly if connection lost: Bug #714540
    
    If a command fails due to a hard error (connection dropped, network
    loss, etc.), the command is preserved in the in-memory Folder queue
    and retried when the connection is reestablished.  This makes Geary
    more robust and resistant to simple errors due to connection loss,
    i.e. archiving a message and having it return later because the
    archive command was dropped and, when the connection reestablished,
    the message "reappears" because it's still on the server.
    
    Note that this is *not* offline mode or a replacement for it, merely
    a way to make Geary more robust when a user's connection is flaky.

 src/client/application/geary-controller.vala       |    8 +-
 src/engine/api/geary-folder-supports-archive.vala  |   14 +--
 src/engine/api/geary-folder-supports-copy.vala     |    1 +
 src/engine/api/geary-folder-supports-create.vala   |    3 +-
 src/engine/api/geary-folder-supports-mark.vala     |   17 +--
 src/engine/api/geary-folder-supports-move.vala     |    1 +
 src/engine/api/geary-folder-supports-remove.vala   |   14 +--
 src/engine/api/geary-folder.vala                   |    4 +
 src/engine/app/app-draft-manager.vala              |    3 +-
 .../imap-engine/imap-engine-generic-folder.vala    |    6 +-
 .../imap-engine/imap-engine-minimal-folder.vala    |  139 ++++++++++----------
 .../imap-engine/imap-engine-replay-operation.vala  |   38 ++++--
 .../imap-engine/imap-engine-replay-queue.vala      |   36 +++++-
 .../imap-engine-send-replay-operation.vala         |    8 +-
 src/engine/imap-engine/imap-engine.vala            |   19 +++
 .../imap-engine-abstract-list-email.vala           |    2 +-
 .../replay-ops/imap-engine-copy-email.vala         |    2 +-
 .../replay-ops/imap-engine-create-email.vala       |   23 +++-
 .../replay-ops/imap-engine-empty-folder.vala       |    2 +-
 .../replay-ops/imap-engine-fetch-email.vala        |    3 +-
 .../replay-ops/imap-engine-mark-email.vala         |    8 +-
 .../replay-ops/imap-engine-move-email.vala         |   27 +++-
 .../replay-ops/imap-engine-remove-email.vala       |    5 +-
 .../replay-ops/imap-engine-replay-append.vala      |   10 +-
 .../replay-ops/imap-engine-replay-disconnect.vala  |   15 ++-
 .../replay-ops/imap-engine-replay-removal.vala     |    9 +-
 .../imap-engine-server-search-email.vala           |    3 +
 src/engine/imap/api/imap-folder.vala               |   17 ++-
 src/engine/imap/command/imap-store-command.vala    |   20 +++-
 .../nonblocking-abstract-semaphore.vala            |   12 --
 src/engine/nonblocking/nonblocking-mailbox.vala    |    3 +-
 .../nonblocking-reporting-semaphore.vala           |    4 +-
 32 files changed, 279 insertions(+), 197 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 2bae336..3ed92e9 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1262,7 +1262,7 @@ public class GearyController : Geary.BaseObject {
         
         debug("Switching to %s...", folder.to_string());
         
-        cancel_folder();
+        closed_folder();
         
         // This function is not reentrant.  It should be, because it can be
         // called reentrant-ly if you select folders quickly enough.  This
@@ -1525,6 +1525,12 @@ public class GearyController : Geary.BaseObject {
         old_cancellable.cancel();
     }
     
+    // Like cancel_folder() but doesn't cancel outstanding operations, allowing them to complete
+    // in the background
+    private void closed_folder() {
+        cancellable_folder = new Cancellable();
+    }
+    
     private void cancel_inbox(Geary.Account account) {
         if (!inbox_cancellables.has_key(account)) {
             debug("Unable to cancel inbox operation for %s", account.to_string());
diff --git a/src/engine/api/geary-folder-supports-archive.vala 
b/src/engine/api/geary-folder-supports-archive.vala
index 9796a76..02bcde8 100644
--- a/src/engine/api/geary-folder-supports-archive.vala
+++ b/src/engine/api/geary-folder-supports-archive.vala
@@ -13,6 +13,7 @@
  * usually in an All Mail folder and perhaps others.  It does not imply that the mail message was
  * moved to the Trash folder.
  */
+
 public interface Geary.FolderSupport.Archive : Geary.Folder {
     /**
      * Archives the specified emails from the folder.
@@ -21,18 +22,5 @@ public interface Geary.FolderSupport.Archive : Geary.Folder {
      */
     public abstract async void archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
         Cancellable? cancellable = null) throws Error;
-    
-    /**
-     * Archive one email from the folder.
-     *
-     * The { link Geary.Folder} must be opened prior to attempting this operation.
-     */
-    public virtual async void archive_single_email_async(Geary.EmailIdentifier email_id,
-        Cancellable? cancellable = null) throws Error {
-        Gee.ArrayList<Geary.EmailIdentifier> ids = new Gee.ArrayList<Geary.EmailIdentifier>();
-        ids.add(email_id);
-        
-        yield archive_email_async(ids, cancellable);
-    }
 }
 
diff --git a/src/engine/api/geary-folder-supports-copy.vala b/src/engine/api/geary-folder-supports-copy.vala
index 351a16a..f3fcdab 100644
--- a/src/engine/api/geary-folder-supports-copy.vala
+++ b/src/engine/api/geary-folder-supports-copy.vala
@@ -12,6 +12,7 @@
  *
  * Copy does not imply { link Geary.FolderSupport.Move}, or vice-versa.
  */
+
 public interface Geary.FolderSupport.Copy : Geary.Folder {
     /**
      * Copies messages into another folder.
diff --git a/src/engine/api/geary-folder-supports-create.vala 
b/src/engine/api/geary-folder-supports-create.vala
index babffe6..6b74581 100644
--- a/src/engine/api/geary-folder-supports-create.vala
+++ b/src/engine/api/geary-folder-supports-create.vala
@@ -13,6 +13,7 @@
  * Note that creating an email in the Outbox will queue it for sending.  Thus, it may be removed
  * without user interaction at some point in the future.
  */
+
 public interface Geary.FolderSupport.Create : Geary.Folder {
     /**
      *  Creates (appends) the message to this folder.
@@ -29,6 +30,6 @@ public interface Geary.FolderSupport.Create : Geary.Folder {
      * message is created.  The new message's ID is returned.
      */
     public abstract async Geary.EmailIdentifier? create_email_async(Geary.RFC822.Message rfc822, EmailFlags? 
flags,
-        DateTime? date_received, Geary.EmailIdentifier? id = null, Cancellable? cancellable = null) throws 
Error;
+        DateTime? date_received, Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error;
 }
 
diff --git a/src/engine/api/geary-folder-supports-mark.vala b/src/engine/api/geary-folder-supports-mark.vala
index 928aa10..5b1be59 100644
--- a/src/engine/api/geary-folder-supports-mark.vala
+++ b/src/engine/api/geary-folder-supports-mark.vala
@@ -8,6 +8,7 @@
  * The addition of the Geary.FolderSupport.Mark interface indicates the { link Geary.Folder}
  * supports marking and unmarking messages with system and user-defined flags.
  */
+
 public interface Geary.FolderSupport.Mark : Geary.Folder {
     /**
      * Adds and removes flags from a list of messages.
@@ -15,21 +16,7 @@ public interface Geary.FolderSupport.Mark : Geary.Folder {
      * The { link Geary.Folder} must be opened prior to attempting this operation.
      */
     public abstract async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, 
-        Cancellable? cancellable = null) throws Error;
-    
-    /**
-     * Adds and removes flags from a single message.
-     *
-     * The { link Geary.Folder} must be opened prior to attempting this operation.
-     */
-    public virtual async void mark_single_email_async(Geary.EmailIdentifier to_mark,
         Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
-        Cancellable? cancellable = null) throws Error {
-        Gee.ArrayList<Geary.EmailIdentifier> list = new Gee.ArrayList<Geary.EmailIdentifier>();
-        list.add(to_mark);
-        
-        yield mark_email_async(list, flags_to_add, flags_to_remove, cancellable);
-    }
+        Cancellable? cancellable = null) throws Error;
 }
 
diff --git a/src/engine/api/geary-folder-supports-move.vala b/src/engine/api/geary-folder-supports-move.vala
index 58ededf..e4f45c3 100644
--- a/src/engine/api/geary-folder-supports-move.vala
+++ b/src/engine/api/geary-folder-supports-move.vala
@@ -11,6 +11,7 @@
  *
  * Move does not imply { link Geary.FolderSupport.Copy}, or vice-versa.
  */
+
 public interface Geary.FolderSupport.Move : Geary.Folder {
     /**
      * Moves messages to another folder.
diff --git a/src/engine/api/geary-folder-supports-remove.vala 
b/src/engine/api/geary-folder-supports-remove.vala
index 8326fb5..0dcd37b 100644
--- a/src/engine/api/geary-folder-supports-remove.vala
+++ b/src/engine/api/geary-folder-supports-remove.vala
@@ -19,6 +19,7 @@
  * A Folder that does not support Remove does not imply that email might not be removed later,
  * such as by the server.
  */
+
 public interface Geary.FolderSupport.Remove : Geary.Folder {
     /**
      * Removes the specified emails from the folder.
@@ -27,18 +28,5 @@ public interface Geary.FolderSupport.Remove : Geary.Folder {
      */
     public abstract async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
         Cancellable? cancellable = null) throws Error;
-    
-    /**
-     * Removes one email from the folder.
-     *
-     * The { link Geary.Folder} must be opened prior to attempting this operation.
-     */
-    public virtual async void remove_single_email_async(Geary.EmailIdentifier email_id,
-        Cancellable? cancellable = null) throws Error {
-        Gee.ArrayList<Geary.EmailIdentifier> ids = new Gee.ArrayList<Geary.EmailIdentifier>();
-        ids.add(email_id);
-        
-        yield remove_email_async(ids, cancellable);
-    }
 }
 
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index bf5b1cf..8f76fb3 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -483,6 +483,8 @@ public abstract class Geary.Folder : BaseObject {
      * EmailIdentifier implies that the top most email is included in the result (i.e.
      * ListFlags.INCLUDING_ID is not required);
      *
+     * If the remote connection fails, this call will return locally-available Email without error.
+     *
      * There's no guarantee of the returned messages' order.
      *
      * The Folder must be opened prior to attempting this operation.
@@ -499,6 +501,8 @@ public abstract class Geary.Folder : BaseObject {
      * one email for each requested; duplicates are ignored.  ListFlags.INCLUDING_ID is ignored
      * for this call.
      *
+     * If the remote connection fails, this call will return locally-available Email without error.
+     *
      * The Folder must be opened prior to attempting this operation.
      */
     public abstract async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
index 860248f..ff03b41 100644
--- a/src/engine/app/app-draft-manager.vala
+++ b/src/engine/app/app-draft-manager.vala
@@ -424,7 +424,8 @@ public class Geary.App.DraftManager : BaseObject {
         if (current_draft_id != null && op.draft == null) {
             bool success = false;
             try {
-                yield remove_support.remove_single_email_async(current_draft_id);
+                yield remove_support.remove_email_async(
+                    iterate<EmailIdentifier>(current_draft_id).to_array_list());
                 success = true;
             } catch (Error err) {
                 debug("%s: Unable to remove existing draft %s: %s", to_string(), 
current_draft_id.to_string(),
diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala 
b/src/engine/imap-engine/imap-engine-generic-folder.vala
index 2f234af..fb1d328 100644
--- a/src/engine/imap-engine/imap-engine-generic-folder.vala
+++ b/src/engine/imap-engine/imap-engine-generic-folder.vala
@@ -20,9 +20,9 @@ private class Geary.ImapEngine.GenericFolder : MinimalFolder, Geary.FolderSuppor
         yield expunge_all_async(cancellable);
     }
     
-    public new async Geary.EmailIdentifier? create_email_async(
-        RFC822.Message rfc822, Geary.EmailFlags? flags, DateTime? date_received,
-        Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error {
+    public new async Geary.EmailIdentifier? create_email_async(RFC822.Message rfc822,
+        Geary.EmailFlags? flags, DateTime? date_received, Geary.EmailIdentifier? id,
+        Cancellable? cancellable = null) throws Error {
         return yield base.create_email_async(rfc822, flags, date_received, id, cancellable);
     }
 }
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 66d39b7..4d784f2 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -43,8 +43,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     private Folder.OpenFlags open_flags = OpenFlags.NONE;
     private int open_count = 0;
     private bool remote_opened = false;
-    private Nonblocking.ReportingSemaphore<bool>? remote_semaphore = null;
-    private ReplayQueue? replay_queue = null;
+    private Nonblocking.ReportingSemaphore<bool> remote_semaphore =
+        new Nonblocking.ReportingSemaphore<bool>(false);
+    private ReplayQueue replay_queue;
     private int remote_count = -1;
     private uint open_remote_timer_id = 0;
     private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
@@ -59,6 +60,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         this.local_folder = local_folder;
         _special_folder_type = special_folder_type;
         _properties.add(local_folder.get_properties());
+        replay_queue = new ReplayQueue(this);
         
         email_flag_watcher = new EmailFlagWatcher(this);
         email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
@@ -484,7 +486,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     }
     
     public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
-        if (open_count == 0 || remote_semaphore == null)
+        if (open_count == 0)
             throw new EngineError.OPEN_REQUIRED("wait_for_open_async() can only be called after 
open_async()");
         
         // if remote has not yet been opened, do it now ... this bool can go true only once after
@@ -519,10 +521,8 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         // first open gets to name the flags, but see note above
         this.open_flags = open_flags;
         
-        remote_semaphore = new Geary.Nonblocking.ReportingSemaphore<bool>(false);
-        
-        // start the replay queue
-        replay_queue = new ReplayQueue(this);
+        // reset to force waiting in wait_for_open_async()
+        remote_semaphore.reset();
         
         // Unless NO_DELAY is set, do NOT open the remote side here; wait for the ReplayQueue to
         // require a remote connection or wait_for_open_async() to be called ... this allows for
@@ -670,10 +670,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
                 // "hard" error in the sense of network conditions make connection impossible
                 // at the moment, "soft" error in the sense that some logical error prevented
                 // connect (like bad credentials)
-                hard_failure = open_err is ImapError.NOT_CONNECTED
-                    || open_err is ImapError.TIMED_OUT
-                    || open_err is ImapError.SERVER_ERROR
-                    || open_err is EngineError.SERVER_UNAVAILABLE;
+                hard_failure = is_hard_failure(open_err);
             } else if (open_err is IOError.CANCELLED) {
                 // user cancelled open, treat like soft error
                 hard_failure = false;
@@ -728,44 +725,23 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         // open success, reset reestablishment delay
         reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
         
-        int count;
-        try {
-            count = (remote_folder != null)
-                ? remote_count
-                : yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, cancellable);
-        } catch (Error count_err) {
-            debug("Unable to fetch count from local folder %s: %s", to_string(), count_err.message);
-            
-            count = 0;
-        }
+        // at this point, remote_folder should be set; there's no notion of a local-only open (yet)
+        assert(remote_folder != null);
         
         // notify any threads of execution waiting for the remote folder to open that the result
         // of that operation is ready
         try {
-            remote_semaphore.notify_result(remote_folder != null, null);
+            remote_semaphore.notify_result(true, null);
         } catch (Error notify_err) {
-            debug("%s: Unable to fire semaphore notifying remote folder ready/not ready: %s",
+            // This should only happen if cancelled, which can't happen without a Cancellable
+            warning("%s: Unable to fire semaphore notifying remote folder ready/not ready: %s",
                 to_string(), notify_err.message);
-            
-            // do this now rather than wait for close_internal_async() to execute to ensure that
-            // any replay operations already queued don't attempt to run
-            clear_remote_folder();
-            
-            notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, notify_err);
-            
-            // schedule immediate close
-            close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
-                cancellable);
-            
-            return;
         }
         
         _properties.add(remote_folder.properties);
         
         // notify any subscribers with similar information
-        notify_opened(
-            (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL,
-            count);
+        notify_opened(Geary.Folder.OpenState.BOTH, remote_count);
     }
     
     public override async void close_async(Cancellable? cancellable = null) throws Error {
@@ -775,17 +751,31 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         if (remote_folder != null)
             _properties.remove(remote_folder.properties);
         
-        yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, true,
-            cancellable);
+        // block anyone from wait_until_open_async(), as this is no longer open
+        remote_semaphore.reset();
+        
+        ReplayDisconnect disconnect_op = new ReplayDisconnect(this,
+            Imap.ClientSession.DisconnectReason.REMOTE_CLOSE, true, cancellable);
+        replay_queue.schedule(disconnect_op);
+        
+        yield disconnect_op.wait_for_ready_async(cancellable);
     }
     
     // Close the remote connection and, if open_count is zero, the Folder itself.  A Mutex is used
     // to prevent concurrency.
     //
+    // This is best called using a ReplayDisconnect operation, which ensures an orderly disconnect
+    // by going through the ReplayQueue.  There are certain situations in open_remote_async() where
+    // this is not possible (because the queue hasn't been started).
+    //
     // NOTE: This bypasses open_count and forces the Folder closed, reestablishing a connection if
     // open_count is greater than zero
     internal async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason 
remote_reason,
         bool flush_pending, Cancellable? cancellable) {
+        // make sure no open is waiting in the wings to start; close_internal_locked_async() will
+        // reestablish a connection if necessary
+        cancel_remote_open_timer();
+        
         int token;
         try {
             token = yield close_mutex.claim_async(cancellable);
@@ -804,8 +794,6 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     // Should only be called when close_mutex is locked, i.e. use close_internal_async()
     private async void close_internal_locked_async(Folder.CloseReason local_reason,
         Folder.CloseReason remote_reason, bool flush_pending, Cancellable? cancellable) {
-        cancel_remote_open_timer();
-        
         // only flushing pending ReplayOperations if this is a "clean" close, not forced due to
         // error and if specified by caller (could be a non-error close on the server, i.e. "BYE",
         // but the connection is dropping, so don't flush pending)
@@ -823,21 +811,26 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         if (!flush_pending)
             closing_remote_folder = clear_remote_folder();
         
-        // Close the replay queues; if a "clean" close, flush pending operations so everything
-        // gets a chance to run; if forced close, drop everything outstanding
-        try {
-            if (replay_queue != null) {
-                debug("Closing replay queue for %s... (flush_pending=%s)", to_string(),
-                    flush_pending.to_string());
-                yield replay_queue.close_async(flush_pending);
-                debug("Closed replay queue for %s", to_string());
+        // That said, only flush, close, and destroy the ReplayQueue if fully closing and not
+        // preparing for a connection reestablishment
+        if (open_count <= 0) {
+            // Close the replay queues; if a "clean" close, flush pending operations so everything
+            // gets a chance to run; if forced close, drop everything outstanding
+            try {
+                // swap out the ReplayQueue while closing so, if re-opened, future commands can
+                // be queued on the new queue
+                ReplayQueue closing_replay_queue = replay_queue;
+                replay_queue = new ReplayQueue(this);
+                
+                debug("Closing replay queue for %s (flush_pending=%s): %s", to_string(),
+                    flush_pending.to_string(), closing_replay_queue.to_string());
+                yield closing_replay_queue.close_async(flush_pending);
+                debug("Closed replay queue for %s: %s", to_string(), closing_replay_queue.to_string());
+            } catch (Error replay_queue_err) {
+                debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
             }
-        } catch (Error replay_queue_err) {
-            debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
         }
         
-        replay_queue = new ReplayQueue(this);
-        
         // if a "clean" close, now go ahead and close the folder
         if (flush_pending)
             closing_remote_folder = clear_remote_folder();
@@ -901,12 +894,15 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         remote_folder = null;
         remote_count = -1;
         
-        remote_semaphore.reset();
-        try {
-            remote_semaphore.notify_result(false, null);
-        } catch (Error err) {
-            debug("Error attempting to notify that remote folder %s is now closed: %s", to_string(),
-                err.message);
+        // only signal waiters in wait_for_open_async() that the open failed if there is no cx
+        // reestablishment to occur
+        if (open_count <= 0) {
+            try {
+                remote_semaphore.notify_result(false, null);
+            } catch (Error err) {
+                debug("Error attempting to notify that remote folder %s is now closed: %s", to_string(),
+                    err.message);
+            }
         }
         
         return old_remote_folder;
@@ -955,6 +951,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
             // prevent driving the value up
             folder.open_count = Numeric.int_floor(folder.open_count - 1, 0);
             
+            debug("Reestablishing broken connection to %s", folder.to_string());
             yield folder.open_async(OpenFlags.NO_DELAY, null);
         } catch (Error err) {
             debug("Error reestablishing broken connection to %s: %s", folder.to_string(), err.message);
@@ -1209,7 +1206,11 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     private void on_remote_disconnected(Imap.ClientSession.DisconnectReason reason) {
         debug("on_remote_disconnected: reason=%s", reason.to_string());
         
-        replay_queue.schedule(new ReplayDisconnect(this, reason));
+        // reset remote_semaphore to indicate that callers must again wait for the remote to open...
+        // do this now to avoid race conditions w/ wait_for_open_async()
+        remote_semaphore.reset();
+        
+        replay_queue.schedule(new ReplayDisconnect(this, reason, false, null));
     }
     
     //
@@ -1293,7 +1294,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     // Helper function for child classes dealing with the delete/archive question.  This method will
     // mark the message as deleted and expunge it.
     protected async void expunge_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
-        Cancellable? cancellable = null) throws Error {
+        Cancellable? cancellable) throws Error {
         check_open("expunge_email_async");
         check_ids("expunge_email_async", email_ids);
         
@@ -1336,12 +1337,13 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
     }
     
     public virtual async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, 
+        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
         Cancellable? cancellable = null) throws Error {
         check_open("mark_email_async");
         
         MarkEmail mark = new MarkEmail(this, to_mark, flags_to_add, flags_to_remove, cancellable);
         replay_queue.schedule(mark);
+        
         yield mark.wait_for_ready_async(cancellable);
     }
     
@@ -1381,6 +1383,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         
         MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
         replay_queue.schedule(move);
+        
         yield move.wait_for_ready_async(cancellable);
     }
     
@@ -1439,9 +1442,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         return earliest_id;
     }
     
-    protected async Geary.EmailIdentifier? create_email_async(
-        RFC822.Message rfc822, Geary.EmailFlags? flags, DateTime? date_received,
-        Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error {
+    protected async Geary.EmailIdentifier? create_email_async(RFC822.Message rfc822,
+        Geary.EmailFlags? flags, DateTime? date_received, Geary.EmailIdentifier? id,
+        Cancellable? cancellable = null) throws Error {
         check_open("create_email_async");
         if (id != null)
             check_id("create_email_async", id);
@@ -1465,7 +1468,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         
         // Remove old message.
         if (id != null && remove_folder != null)
-            yield remove_folder.remove_single_email_async(id, null);
+            yield remove_folder.remove_email_async(iterate<EmailIdentifier>(id).to_array_list());
         
         // If the user cancelled the operation, throw the error here.
         if (cancel_error != null)
@@ -1474,7 +1477,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         // If the caller cancelled during the remove operation, delete the newly created message to
         // safely back out.
         if (cancellable != null && cancellable.is_cancelled() && ret != null && remove_folder != null)
-            yield remove_folder.remove_single_email_async(ret, null);
+            yield remove_folder.remove_email_async(iterate<EmailIdentifier>(ret).to_array_list());
         
         return ret;
     }
diff --git a/src/engine/imap-engine/imap-engine-replay-operation.vala 
b/src/engine/imap-engine/imap-engine-replay-operation.vala
index 5662521..84b87ad 100644
--- a/src/engine/imap-engine/imap-engine-replay-operation.vala
+++ b/src/engine/imap-engine/imap-engine-replay-operation.vala
@@ -4,7 +4,7 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
+private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject, Gee.Comparable<ReplayOperation> {
     /**
      * Scope specifies what type of operations (remote, local, or both) are needed by this operation.
      *
@@ -29,20 +29,26 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
         CONTINUE
     }
     
-    private static int next_opnum = 0;
+    public enum OnError {
+        THROW,
+        RETRY,
+        IGNORE
+    }
     
     public string name { get; set; }
-    public int opnum { get; private set; }
+    public int64 submission_number { get; set; default = -1; }
     public Scope scope { get; private set; }
+    public OnError on_remote_error { get; protected set; }
+    public int remote_retry_count { get; set; default = 0; }
     public Error? err { get; private set; default = null; }
-    public bool notified { get; private set; default = false; }
+    public bool notified { get { return semaphore.is_passed(); } }
     
     private Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore();
     
-    public ReplayOperation(string name, Scope scope) {
+    public ReplayOperation(string name, Scope scope, OnError on_remote_error = OnError.THROW) {
         this.name = name;
-        opnum = next_opnum++;
         this.scope = scope;
+        this.on_remote_error = on_remote_error;
     }
     
     /**
@@ -132,10 +138,9 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
             throw err;
     }
     
+    // Can only be called once
     internal void notify_ready(Error? err) {
-        assert(!notified);
-        
-        notified = true;
+        assert(!semaphore.is_passed());
         
         this.err = err;
         
@@ -148,11 +153,22 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
     
     public abstract string describe_state();
     
+    // The Comparable interface is merely to ensure the ReplayQueue sorts operations by their
+    // submission order, ensuring that retry operations are retried in order of submissions
+    public int compare_to(ReplayOperation other) {
+        assert(submission_number >= 0);
+        assert(other.submission_number >= 0);
+        
+        return (int) (submission_number - other.submission_number).clamp(-1, 1);
+    }
+    
     public string to_string() {
         string state = describe_state();
         
-        return (String.is_empty(state)) ? "[%d] %s".printf(opnum, name)
-            : "[%d] %s: %s".printf(opnum, name, state);
+        return String.is_empty(state)
+            ? "[%s] %s remote_retry_count=%d".printf(submission_number.to_string(), name, remote_retry_count)
+            : "[%s] %s: %s remote_retry_count=%d".printf(submission_number.to_string(), name, state,
+                remote_retry_count);
     }
 }
 
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala 
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index e376774..af43bad 100644
--- a/src/engine/imap-engine/imap-engine-replay-queue.vala
+++ b/src/engine/imap-engine/imap-engine-replay-queue.vala
@@ -12,7 +12,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
     private class CloseReplayQueue : ReplayOperation {
         public CloseReplayQueue() {
             // LOCAL_AND_REMOTE to make sure this operation is flushed all the way down the pipe
-            base ("CloseReplayQueue", ReplayOperation.Scope.LOCAL_AND_REMOTE);
+            base ("CloseReplayQueue", ReplayOperation.Scope.LOCAL_AND_REMOTE, OnError.IGNORE);
         }
         
         public override void notify_remote_removed_position(Imap.SequenceNumber removed) {
@@ -56,6 +56,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
     private ReplayOperation? remote_op_active = null;
     private Gee.ArrayList<ReplayOperation> notification_queue = new Gee.ArrayList<ReplayOperation>();
     private Scheduler.Scheduled? notification_timer = null;
+    private int64 next_submission_number = 0;
     
     private bool is_closed = false;
     
@@ -149,6 +150,10 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
             return false;
         }
         
+        // assign a submission number to operation ... this *must* happen before it's submitted to
+        // any Mailbox
+        op.submission_number = next_submission_number++;
+        
         // note that in order for this to work (i.e. for sent and received operations to be handled
         // in order), it's *vital* that even REMOTE_ONLY operations go through the local queue,
         // only being scheduled on the remote queue *after* local operations ahead of it have
@@ -483,13 +488,36 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
             
             Error? remote_err = null;
             if (folder_opened || is_close_op) {
+                if (op.remote_retry_count > 0)
+                    debug("Retrying op %s on %s", op.to_string(), to_string());
+                
                 try {
                     yield op.replay_remote_async();
                 } catch (Error replay_err) {
-                    debug("Replay remote error for %s on %s: %s", op.to_string(), to_string(),
-                        replay_err.message);
+                    debug("Replay remote error for %s on %s: %s (%s)", op.to_string(), to_string(),
+                        replay_err.message, op.on_remote_error.to_string());
                     
-                    remote_err = replay_err;
+                    // If a hard failure and operation allows remote replay, schedule now
+                    if ((op.on_remote_error == ReplayOperation.OnError.RETRY) && 
is_hard_failure(replay_err)) {
+                        debug("Schedule op retry %s on %s", op.to_string(), to_string());
+                        
+                        // the Folder will disconnect and reconnect due to the hard error and
+                        // wait_for_open_async() will block this command until reconnected and
+                        // normalized
+                        op.remote_retry_count++;
+                        remote_queue.send(op);
+                        
+                        continue;
+                    } else if (op.on_remote_error == ReplayOperation.OnError.IGNORE) {
+                        // ignoring error, simply notify as completed and continue
+                        debug("Ignoring op %s on %s", op.to_string(), to_string());
+                    } else {
+                        debug("Throwing remote error for op %s on %s: %s", op.to_string(), to_string(),
+                            replay_err.message);
+                        
+                        // store for notification
+                        remote_err = replay_err;
+                    }
                 }
             } else if (!is_close_op) {
                 remote_err = new EngineError.SERVER_UNAVAILABLE("Folder %s not available", 
owner.to_string());
diff --git a/src/engine/imap-engine/imap-engine-send-replay-operation.vala 
b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
index e36cc52..5ae0dce 100644
--- a/src/engine/imap-engine/imap-engine-send-replay-operation.vala
+++ b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -5,12 +5,12 @@
  */
 
 private abstract class Geary.ImapEngine.SendReplayOperation : Geary.ImapEngine.ReplayOperation {
-    public SendReplayOperation(string name) {
-        base (name, ReplayOperation.Scope.LOCAL_AND_REMOTE);
+    public SendReplayOperation(string name, ReplayOperation.OnError on_remote_error = OnError.THROW) {
+        base (name, ReplayOperation.Scope.LOCAL_AND_REMOTE, on_remote_error);
     }
     
-    public SendReplayOperation.only_remote(string name) {
-        base (name, ReplayOperation.Scope.REMOTE_ONLY);
+    public SendReplayOperation.only_remote(string name, ReplayOperation.OnError on_remote_error = 
OnError.THROW) {
+        base (name, ReplayOperation.Scope.REMOTE_ONLY, on_remote_error);
     }
     
     public override void notify_remote_removed_position(Imap.SequenceNumber removed) {
diff --git a/src/engine/imap-engine/imap-engine.vala b/src/engine/imap-engine/imap-engine.vala
index 6a21e59..3c7d18f 100644
--- a/src/engine/imap-engine/imap-engine.vala
+++ b/src/engine/imap-engine/imap-engine.vala
@@ -58,5 +58,24 @@ private void on_synchronizer_stopped(Object? source, AsyncResult result) {
     assert(removed);
 }
 
+/**
+ * A hard failure is defined as one due to hardware or connectivity issues, where a soft failure
+ * is due to software reasons, like credential failure or protocol violation.
+ */
+private static bool is_hard_failure(Error err) {
+    // CANCELLED is not a hard error
+    if (err is IOError.CANCELLED)
+        return false;
+    
+    // Treat other errors -- most likely IOErrors -- as hard failures
+    if (!(err is ImapError) && !(err is EngineError))
+        return true;
+    
+    return err is ImapError.NOT_CONNECTED
+        || err is ImapError.TIMED_OUT
+        || err is ImapError.SERVER_ERROR
+        || err is EngineError.SERVER_UNAVAILABLE;
+}
+
 }
 
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
index 758c29b..15243a7 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
@@ -66,7 +66,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
     
     public AbstractListEmail(string name, MinimalFolder owner, Geary.Email.Field required_fields,
         Folder.ListFlags flags, Cancellable? cancellable) {
-        base(name);
+        base(name, OnError.IGNORE);
         
         this.owner = owner;
         this.required_fields = required_fields;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
index f054780..703a0c3 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
@@ -14,7 +14,7 @@ private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation
 
     public CopyEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_copy, 
         Geary.FolderPath destination, Cancellable? cancellable = null) {
-        base("CopyEmail");
+        base("CopyEmail", OnError.RETRY);
         
         this.engine = engine;
         
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
index 3750ff0..f171357 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
@@ -8,14 +8,14 @@ private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperatio
     public Geary.EmailIdentifier? created_id { get; private set; default = null; }
     
     private MinimalFolder engine;
-    private RFC822.Message rfc822;
+    private RFC822.Message? rfc822;
     private Geary.EmailFlags? flags;
     private DateTime? date_received;
     private Cancellable? cancellable;
     
     public CreateEmail(MinimalFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags,
         DateTime? date_received, Cancellable? cancellable) {
-        base.only_remote("CreateEmail");
+        base.only_remote("CreateEmail", OnError.RETRY);
         
         this.engine = engine;
         
@@ -47,18 +47,29 @@ private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperatio
         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);
+        // use IMAP APPEND command on remote folders, which doesn't require opening a folder ...
+        // if retrying after a successful create, rfc822 will be null
+        if (rfc822 != null)
+            created_id = yield engine.remote_folder.create_email_async(rfc822, flags, date_received);
+        
+        // because this command retries, the create completed, remove the RFC822 message to prevent
+        // creating it twice
+        rfc822 = null;
         
         // 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).to_list(), null);
+            if (created_id != null) {
+                yield engine.remote_folder.remove_email_async(
+                    new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid).to_list(), null);
+            }
             
             throw new IOError.CANCELLED("CreateEmail op cancelled after create");
         }
         
+        if (created_id == null)
+            return ReplayOperation.Status.COMPLETED;
+        
         // TODO: need to prevent gaps that may occur here
         Geary.Email created = new Geary.Email(created_id);
         Gee.Map<Geary.Email, bool> results = yield engine.local_folder.create_or_merge_email_async(
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
index 1cf774a..05aedb4 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
@@ -16,7 +16,7 @@ private class Geary.ImapEngine.EmptyFolder : Geary.ImapEngine.SendReplayOperatio
     private int original_count = 0;
     
     public EmptyFolder(MinimalFolder engine, Cancellable? cancellable) {
-        base("EmptyFolder");
+        base("EmptyFolder", OnError.RETRY);
         
         this.engine = engine;
         this.cancellable = cancellable;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
index a6090fd..133d8a5 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
@@ -18,7 +18,8 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
     
     public FetchEmail(MinimalFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
         Folder.ListFlags flags, Cancellable? cancellable) {
-        base ("FetchEmail");
+        // Unlike the list operations, fetch needs to retry remote
+        base ("FetchEmail", OnError.RETRY);
         
         this.engine = engine;
         this.id = id;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
index 35b51c2..aa3c019 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
@@ -15,7 +15,7 @@ private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation
     public MarkEmail(MinimalFolder engine, Gee.List<Geary.EmailIdentifier> to_mark, 
         Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, 
         Cancellable? cancellable = null) {
-        base("MarkEmail");
+        base("MarkEmail", OnError.RETRY);
         
         this.engine = engine;
         
@@ -65,10 +65,8 @@ private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation
         
         Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(
             ImapDB.EmailIdentifier.to_uids(original_flags.keys));
-        foreach (Imap.MessageSet msg_set in msg_sets) {
-            yield engine.remote_folder.mark_email_async(msg_set, flags_to_add, flags_to_remove,
-                cancellable);
-        }
+        yield engine.remote_folder.mark_email_async(msg_sets, flags_to_add, flags_to_remove,
+            cancellable);
         
         return ReplayOperation.Status.COMPLETED;
     }
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
index 6be265d..d0ae775 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
@@ -11,10 +11,11 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
     private Cancellable? cancellable;
     private Gee.Set<ImapDB.EmailIdentifier>? moved_ids = null;
     private int original_count = 0;
+    private Gee.List<Imap.MessageSet>? remaining_msg_sets = null;
 
     public MoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move, 
         Geary.FolderPath destination, Cancellable? cancellable = null) {
-        base("MoveEmail");
+        base("MoveEmail", OnError.RETRY);
 
         this.engine = engine;
 
@@ -62,16 +63,26 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
         if (moved_ids.size == 0)
             return ReplayOperation.Status.COMPLETED;
         
-        // don't use Cancellable throughout I/O operations in order to assure transaction completes
-        // fully
-        if (cancellable != null && cancellable.is_cancelled())
-            throw new IOError.CANCELLED("Move email to %s cancelled", engine.remote_folder.to_string());
+        // Remaining MessageSets are persisted in case of network retries
+        if (remaining_msg_sets == null)
+            remaining_msg_sets = Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(moved_ids));
         
-        Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(
-            ImapDB.EmailIdentifier.to_uids(moved_ids));
-        foreach (Imap.MessageSet msg_set in msg_sets) {
+        if (remaining_msg_sets == null || remaining_msg_sets.size == 0)
+            return ReplayOperation.Status.COMPLETED;
+        
+        Gee.Iterator<Imap.MessageSet> iter = remaining_msg_sets.iterator();
+        while (iter.next()) {
+            // don't use Cancellable throughout I/O operations in order to assure transaction completes
+            // fully
+            if (cancellable != null && cancellable.is_cancelled())
+                throw new IOError.CANCELLED("Move email to %s cancelled", engine.remote_folder.to_string());
+            
+            Imap.MessageSet msg_set = iter.get();
             yield engine.remote_folder.copy_email_async(msg_set, destination, null);
             yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
+            
+            // completed successfully, remove from list in case of retry
+            iter.remove();
         }
         
         return ReplayOperation.Status.COMPLETED;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
index 95cbed8..979c6f9 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
@@ -13,7 +13,7 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
     
     public RemoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
         Cancellable? cancellable = null) {
-        base("RemoveEmail");
+        base("RemoveEmail", OnError.RETRY);
         
         this.engine = engine;
         
@@ -57,6 +57,9 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
     }
     
     public override async ReplayOperation.Status replay_remote_async() throws Error {
+        if (removed_ids.size == 0)
+            return ReplayOperation.Status.COMPLETED;
+        
         // Remove from server. Note that this causes the receive replay queue to kick into
         // action, removing the e-mail but *NOT* firing a signal; the "remove marker" indicates
         // that the signal has already been fired.
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
index 6d561be..9de7fad 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
@@ -5,12 +5,14 @@
  */
 
 private class Geary.ImapEngine.ReplayAppend : Geary.ImapEngine.ReplayOperation {
-    public MinimalFolder owner;
-    public int remote_count;
-    public Gee.List<Imap.SequenceNumber> positions;
+    private MinimalFolder owner;
+    private int remote_count;
+    private Gee.List<Imap.SequenceNumber> positions;
     
     public ReplayAppend(MinimalFolder owner, int remote_count, Gee.List<Imap.SequenceNumber> positions) {
-        base ("Append", Scope.REMOTE_ONLY);
+        // IGNORE remote errors because the reconnect will re-normalize the folder, making this
+        // append moot
+        base ("Append", Scope.REMOTE_ONLY, OnError.IGNORE);
         
         this.owner = owner;
         this.remote_count = remote_count;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
index 2a20990..2acf58f 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
@@ -5,14 +5,19 @@
  */
 
 private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperation {
-    public MinimalFolder owner;
-    public Imap.ClientSession.DisconnectReason reason;
+    private MinimalFolder owner;
+    private Imap.ClientSession.DisconnectReason reason;
+    private bool flush_pending;
+    private Cancellable? cancellable;
     
-    public ReplayDisconnect(MinimalFolder owner, Imap.ClientSession.DisconnectReason reason) {
+    public ReplayDisconnect(MinimalFolder owner, Imap.ClientSession.DisconnectReason reason,
+        bool flush_pending, Cancellable? cancellable) {
         base ("Disconnect", Scope.LOCAL_ONLY);
         
         this.owner = owner;
         this.reason = reason;
+        this.flush_pending = flush_pending;
+        this.cancellable = cancellable;
     }
     
     public override void notify_remote_removed_position(Imap.SequenceNumber removed) {
@@ -37,7 +42,7 @@ private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperati
             // ReplayDisconnect is only used when remote disconnects, so never flush pending, the
             // connection is down or going down
             owner.close_internal_async.begin(Geary.Folder.CloseReason.LOCAL_CLOSE, remote_reason,
-                false, null);
+                flush_pending, cancellable);
             
             return false;
         });
@@ -49,7 +54,7 @@ private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperati
     }
     
     public override async ReplayOperation.Status replay_remote_async() throws Error {
-        // shot not be called
+        // should not be called
         return ReplayOperation.Status.COMPLETED;
     }
     
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
index eeea5e2..4fc652b 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
@@ -5,12 +5,13 @@
  */
 
 private class Geary.ImapEngine.ReplayRemoval : Geary.ImapEngine.ReplayOperation {
-    public MinimalFolder owner;
-    public int remote_count;
-    public Imap.SequenceNumber position;
+    private MinimalFolder owner;
+    private int remote_count;
+    private Imap.SequenceNumber position;
     
     public ReplayRemoval(MinimalFolder owner, int remote_count, Imap.SequenceNumber position) {
-        base ("Removal", Scope.LOCAL_AND_REMOTE);
+        // remote error will cause folder to reconnect and re-normalize, making this remove moot
+        base ("Removal", Scope.LOCAL_AND_REMOTE, OnError.IGNORE);
         
         this.owner = owner;
         this.remote_count = remote_count;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
index 5b1443d..b8d6f5d 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
@@ -19,6 +19,9 @@ private class Geary.ImapEngine.ServerSearchEmail : Geary.ImapEngine.AbstractList
         base ("ServerSearchEmail", owner, required_fields, Geary.Folder.ListFlags.OLDEST_TO_NEWEST,
             cancellable);
         
+        // unlike list, need to retry this as there's no local component to return
+        on_remote_error = OnError.RETRY;
+        
         this.criteria = criteria;
     }
     
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index b4b6ab1..f6dbee3 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -641,7 +641,7 @@ private class Geary.Imap.Folder : BaseObject {
             if (!msg_set.is_uid)
                 all_uid = false;
             
-            cmds.add(new StoreCommand(msg_set, flags, true, false));
+            cmds.add(new StoreCommand(msg_set, flags, StoreCommand.Option.ADD_FLAGS));
         }
         
         // TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work
@@ -661,7 +661,7 @@ private class Geary.Imap.Folder : BaseObject {
         yield exec_commands_async(cmds, null, null, cancellable);
     }
     
-    public async void mark_email_async(MessageSet msg_set, Geary.EmailFlags? flags_to_add,
+    public async void mark_email_async(Gee.List<MessageSet> msg_sets, Geary.EmailFlags? flags_to_add,
         Geary.EmailFlags? flags_to_remove, Cancellable? cancellable) throws Error {
         check_open();
         
@@ -674,12 +674,13 @@ private class Geary.Imap.Folder : BaseObject {
             return;
         
         Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
-        
-        if (msg_flags_add.size > 0)
-            cmds.add(new StoreCommand(msg_set, msg_flags_add, true, false));
-        
-        if (msg_flags_remove.size > 0)
-            cmds.add(new StoreCommand(msg_set, msg_flags_remove, false, false));
+        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_remove.size > 0)
+                cmds.add(new StoreCommand(msg_set, msg_flags_remove, StoreCommand.Option.REMOVE_FLAGS));
+        }
         
         yield exec_commands_async(cmds, null, null, cancellable);
     }
diff --git a/src/engine/imap/command/imap-store-command.vala b/src/engine/imap/command/imap-store-command.vala
index f25365d..1bed913 100644
--- a/src/engine/imap/command/imap-store-command.vala
+++ b/src/engine/imap/command/imap-store-command.vala
@@ -15,10 +15,26 @@ public class Geary.Imap.StoreCommand : Command {
     public const string NAME = "store";
     public const string UID_NAME = "uid store";
     
-    public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, bool add_flag, 
-        bool silent) {
+    /**
+     * Options indicating functionality of the { link StoreCommand}.
+     *
+     * Note that { link ADD_FLAGS} and { link REMOVE_FLAGS} are mutally exclusive.  REMOVE_FLAGS
+     * actually does not set a bit, meaning that removing is the default operation and, if both
+     * add and remove are set, an add occurs.
+     */
+    [Flags]
+    public enum Option {
+        REMOVE_FLAGS = 0,
+        ADD_FLAGS,
+        SILENT
+    }
+    
+    public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, Option options) {
         base (message_set.is_uid ? UID_NAME : NAME);
         
+        bool add_flag = (options & Option.ADD_FLAGS) != 0;
+        bool silent = (options & Option.SILENT) != 0;
+        
         add(message_set.to_parameter());
         add(new AtomParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : "")));
         
diff --git a/src/engine/nonblocking/nonblocking-abstract-semaphore.vala 
b/src/engine/nonblocking/nonblocking-abstract-semaphore.vala
index f689003..2c3672a 100644
--- a/src/engine/nonblocking/nonblocking-abstract-semaphore.vala
+++ b/src/engine/nonblocking/nonblocking-abstract-semaphore.vala
@@ -46,9 +46,6 @@ public abstract class Geary.Nonblocking.AbstractSemaphore : BaseObject {
     private bool passed = false;
     private Gee.List<Pending> pending_queue = new Gee.LinkedList<Pending>();
     
-    public virtual signal void at_reset() {
-    }
-    
     protected AbstractSemaphore(bool broadcast, bool autoreset, Cancellable? cancellable = null) {
         this.broadcast = broadcast;
         this.autoreset = autoreset;
@@ -70,10 +67,6 @@ public abstract class Geary.Nonblocking.AbstractSemaphore : BaseObject {
             cancellable.cancelled.disconnect(on_cancelled);
     }
     
-    protected virtual void notify_at_reset() {
-        at_reset();
-    }
-    
     private void trigger(bool all) {
         if (pending_queue.size == 0)
             return;
@@ -138,12 +131,7 @@ public abstract class Geary.Nonblocking.AbstractSemaphore : BaseObject {
     }
     
     public virtual void reset() {
-        if (!passed)
-            return;
-        
         passed = false;
-        
-        notify_at_reset();
     }
     
     public bool is_passed() {
diff --git a/src/engine/nonblocking/nonblocking-mailbox.vala b/src/engine/nonblocking/nonblocking-mailbox.vala
index 3337b44..93854da 100644
--- a/src/engine/nonblocking/nonblocking-mailbox.vala
+++ b/src/engine/nonblocking/nonblocking-mailbox.vala
@@ -28,8 +28,7 @@ public class Geary.Nonblocking.Mailbox<G> : BaseObject {
     private Nonblocking.Spinlock spinlock = new Nonblocking.Spinlock();
     
     public Mailbox(owned CompareDataFunc<G>? comparator = null) {
-        // can't use ternary here, Vala bug
-        if (comparator == null)
+        if (comparator == null && !typeof(G).is_a(typeof(Gee.Comparable)))
             queue = new Gee.LinkedList<G>();
         else
             queue = new Gee.PriorityQueue<G>((owned) comparator);
diff --git a/src/engine/nonblocking/nonblocking-reporting-semaphore.vala 
b/src/engine/nonblocking/nonblocking-reporting-semaphore.vala
index fd8cda5..0ffcb7f 100644
--- a/src/engine/nonblocking/nonblocking-reporting-semaphore.vala
+++ b/src/engine/nonblocking/nonblocking-reporting-semaphore.vala
@@ -17,11 +17,11 @@ public class Geary.Nonblocking.ReportingSemaphore<G> : Geary.Nonblocking.Semapho
         result = default_result;
     }
     
-    protected override void notify_at_reset() {
+    public override void reset() {
         result = default_result;
         err = null;
         
-        base.notify_at_reset();
+        base.reset();
     }
     
     public void notify_result(G result, Error? err) throws Error {


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