[geary/wip/remote-retry] Will properly reconnect and retry mark_email_async() calls



commit 56279ed3471c3b705dea382877f32150b58136e1
Author: Jim Nelson <jim yorba org>
Date:   Thu Jan 15 16:49:45 2015 -0800

    Will properly reconnect and retry mark_email_async() calls

 src/CMakeLists.txt                                 |    1 +
 src/engine/api/geary-folder-supports-mark.vala     |   12 ++-
 src/engine/api/geary-future.vala                   |  107 ++++++++++++++++++++
 .../imap-engine/imap-engine-minimal-folder.vala    |   88 +++++++---------
 .../imap-engine/imap-engine-replay-operation.vala  |   44 +++++++-
 .../imap-engine/imap-engine-replay-queue.vala      |   36 ++++++-
 .../imap-engine-send-replay-operation.vala         |    8 +-
 src/engine/imap-engine/imap-engine.vala            |   15 +++
 .../replay-ops/imap-engine-mark-email.vala         |    8 +-
 src/engine/imap/api/imap-folder.vala               |   17 ++--
 src/engine/imap/command/imap-store-command.vala    |   20 ++++-
 11 files changed, 274 insertions(+), 82 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0d04959..30d00cc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -33,6 +33,7 @@ engine/api/geary-folder-supports-empty.vala
 engine/api/geary-folder-supports-mark.vala
 engine/api/geary-folder-supports-move.vala
 engine/api/geary-folder-supports-remove.vala
+engine/api/geary-future.vala
 engine/api/geary-logging.vala
 engine/api/geary-named-flag.vala
 engine/api/geary-named-flags.vala
diff --git a/src/engine/api/geary-folder-supports-mark.vala b/src/engine/api/geary-folder-supports-mark.vala
index 928aa10..4556fa7 100644
--- a/src/engine/api/geary-folder-supports-mark.vala
+++ b/src/engine/api/geary-folder-supports-mark.vala
@@ -8,14 +8,18 @@
  * 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.
      *
+     * If a { link Future} is returned, the operation has been scheduled for completion in the
+     * background.  The Future can be monitored or waited upon for final completion.
+     *
      * 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, 
+    public abstract async Geary.Future? mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
+        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
         Cancellable? cancellable = null) throws Error;
     
     /**
@@ -23,13 +27,13 @@ public interface Geary.FolderSupport.Mark : Geary.Folder {
      *
      * The { link Geary.Folder} must be opened prior to attempting this operation.
      */
-    public virtual async void mark_single_email_async(Geary.EmailIdentifier to_mark,
+    public virtual async Geary.Future? 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);
+        return yield mark_email_async(list, flags_to_add, flags_to_remove, cancellable);
     }
 }
 
diff --git a/src/engine/api/geary-future.vala b/src/engine/api/geary-future.vala
new file mode 100644
index 0000000..9d76cbf
--- /dev/null
+++ b/src/engine/api/geary-future.vala
@@ -0,0 +1,107 @@
+/* Copyright 2015 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.
+ */
+
+/**
+ * A generic mechanism for a method to return a result value after completing.
+ *
+ * Although designed with Geary's asynchronous interface in mind, this can be expanded for
+ * synchronous (nonblocking) methods as well.
+ */
+
+public class Geary.Future : BaseObject {
+    /**
+     * The name of the { link Future}.
+     *
+     * This is non-unique and should not be displayed to the user.  It's merely used for logging
+     * purposes.
+     */
+    public string name { get; private set; }
+    
+    /**
+     * Returns true if the { link Future} has been { link fulfilled}.
+     *
+     * @see fulfill
+     */
+    public bool is_fulfilled { get { return semaphore.is_passed(); } }
+    
+    private Nonblocking.Semaphore semaphore = new Nonblocking.Semaphore();
+    private Value? result = null;
+    private Error? err = null;
+    
+    /**
+     * Fired when the { link Future} is fulfilled, that is, the awaited result (or Error) has been
+     * determined.
+     *
+     * The promise (the method that returns this Future) should state in its contract the data type
+     * held by the Value, if any, as the result.
+     *
+     * If err is non-null, then result ''must'' be null.
+     *
+     * @see fulfill
+     */
+    public signal void fulfilled(Value? result, Error? err);
+    
+    /**
+     * Create a { link Future}.
+     */
+    public Future(string name) {
+        this.name = name;
+    }
+    
+    /**
+     * Fulfill the { link Future} with the results of the operation.
+     *
+     * This can only be called once.  Additional calls will be ignored.
+     *
+     * If err is non-null, then result ''must'' be null.  However, fulfill() does not enforce this,
+     * but will log a message.
+     *
+     * 
+     */
+    public void fulfill(Value? result, Error? err) {
+        if (is_fulfilled)
+            return;
+        
+        if (err != null && result != null)
+            message("%s fulfilled with non-null Error and Value result", to_string());
+        
+        this.result = result;
+        this.err = err;
+        
+        try {
+            semaphore.notify();
+        } catch (Error err) {
+            message("Unable to fulfill %s: %s", to_string(), err.message);
+        }
+        
+        fulfilled(result, err);
+    }
+    
+    /**
+     * Wait for the { link Future} to be { link fulfilled}.
+     *
+     * If the Future was fulfilled with an Error, it is thrown.  Otherwise, the result Value is
+     * returned.
+     *
+     * This method may be called any number of times.  If already fulfilled, it will complete
+     * instantly.
+     *
+     * @see fulfill
+     */
+    public async Value? wait_for_fulfillment_async(Cancellable? cancellable = null) throws Error {
+        yield semaphore.wait_async(cancellable);
+        
+        if (err != null)
+            throw err;
+        
+        return result;
+    }
+    
+    public string to_string() {
+        return "Future:%s".printf(name);
+    }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 11c262e..a86a327 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -638,10 +638,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;
@@ -696,44 +693,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 {
@@ -770,21 +746,25 @@ 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 {
+                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());
+                }
+            } 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);
         }
         
-        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();
@@ -849,11 +829,16 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         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;
@@ -1282,14 +1267,15 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
             check_id(method, id);
     }
     
-    public virtual async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
-        Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, 
+    public virtual async Geary.Future? mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
+        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);
+        
+        return yield mark.wait_for_ready_async(cancellable);
     }
     
     public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
diff --git a/src/engine/imap-engine/imap-engine-replay-operation.vala 
b/src/engine/imap-engine/imap-engine-replay-operation.vala
index 5662521..e5cf26b 100644
--- a/src/engine/imap-engine/imap-engine-replay-operation.vala
+++ b/src/engine/imap-engine/imap-engine-replay-operation.vala
@@ -29,20 +29,30 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
         CONTINUE
     }
     
+    [Flags]
+    public enum Retry {
+        NONE = 0,
+        REMOTE
+    }
+    
     private static int next_opnum = 0;
     
     public string name { get; set; }
     public int opnum { get; private set; }
     public Scope scope { get; private set; }
+    public Retry retry { get; protected set; }
+    public int 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();
+    private Geary.Future? future = null;
     
-    public ReplayOperation(string name, Scope scope) {
+    public ReplayOperation(string name, Scope scope, Retry retry = Retry.NONE) {
         this.name = name;
         opnum = next_opnum++;
         this.scope = scope;
+        this.retry = retry;
     }
     
     /**
@@ -124,18 +134,29 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
     /**
      * Completes when the operation has completed execution.  If the operation threw an error
      * during execution, it will be thrown here.
+     *
+     * Returns a Semaphore if the operation is retrying.  The Semaphore will be triggered when the
+     * retry completes or errors out.
      */
-    public async void wait_for_ready_async(Cancellable? cancellable = null) throws Error {
+    public async Geary.Future? wait_for_ready_async(Cancellable? cancellable = null) throws Error {
         yield semaphore.wait_async(cancellable);
         
         if (err != null)
             throw err;
+        
+        return future;
     }
     
+    // Can only be called once, although must be called after notify_retrying() is called.
     internal void notify_ready(Error? err) {
-        assert(!notified);
+        if (semaphore.is_passed() && future != null) {
+            future.fulfill(null, err);
+            
+            return;
+        }
         
-        notified = true;
+        // otherwise, if notified but not due to retry, trouble
+        assert(!semaphore.is_passed());
         
         this.err = err;
         
@@ -146,6 +167,19 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
         }
     }
     
+    // Can only be called once, although notify_ready() may be called after calling this.
+    internal void notify_retrying() {
+        assert(!semaphore.is_passed());
+        
+        future = new Future(name);
+        
+        try {
+            semaphore.notify();
+        } catch (Error notify_err) {
+            debug("Unable to notify replay operation as retrying: [%s] %s", name, notify_err.message);
+        }
+    }
+    
     public abstract string describe_state();
     
     public string to_string() {
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala 
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index e376774..f3c4ff4 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, ReplayOperation.Retry.NONE);
         }
         
         public override void notify_remote_removed_position(Imap.SequenceNumber removed) {
@@ -50,8 +50,10 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
     } }
     
     private weak MinimalFolder owner;
-    private Nonblocking.Mailbox<ReplayOperation> local_queue = new Nonblocking.Mailbox<ReplayOperation>();
-    private Nonblocking.Mailbox<ReplayOperation> remote_queue = new Nonblocking.Mailbox<ReplayOperation>();
+    private Nonblocking.Mailbox<ReplayOperation> local_queue = new Nonblocking.Mailbox<ReplayOperation>(
+        replay_operation_comparator);
+    private Nonblocking.Mailbox<ReplayOperation> remote_queue = new Nonblocking.Mailbox<ReplayOperation>(
+        replay_operation_comparator);
     private ReplayOperation? local_op_active = null;
     private ReplayOperation? remote_op_active = null;
     private Gee.ArrayList<ReplayOperation> notification_queue = new Gee.ArrayList<ReplayOperation>();
@@ -344,6 +346,10 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
         }
     }
     
+    private static int replay_operation_comparator(ReplayOperation a, ReplayOperation b) {
+        return a.retry_count - b.retry_count;
+    }
+    
     private async void do_replay_local_async() {
         bool queue_running = true;
         while (queue_running) {
@@ -415,6 +421,9 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
                 }
             }
             
+            // completed one way or another, clear retry count for next round
+            op.retry_count = 0;
+            
             if (remote_enqueue) {
                 if (!remote_queue.send(op)) {
                     debug("Unable to enqueue operation %s for %s remote operation", op.to_string(),
@@ -483,12 +492,33 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
             
             Error? remote_err = null;
             if (folder_opened || is_close_op) {
+                if (op.retry_count > 0) {
+                    debug("RETRYING OP %s on %s: retry_count=%d", op.to_string(), to_string(),
+                        op.retry_count);
+                }
+                
                 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);
                     
+                    // If a hard failure and operation allows remote replay, schedule now
+                    if (is_hard_failure(replay_err) && (op.retry & ReplayOperation.Retry.REMOTE) != 0) {
+                        debug("SCHEDULING RETRY OP %s on %s: retry_count=%d", op.to_string(),
+                            to_string(), op.retry_count);
+                        
+                        // if this is the first retry, complete the waiting async caller but allow
+                        // them to wait later or be signaled when completed
+                        if (op.retry_count == 0)
+                            op.notify_retrying();
+                        
+                        op.retry_count++;
+                        remote_queue.send(op);
+                        
+                        continue;
+                    }
+                    
                     remote_err = replay_err;
                 }
             } else if (!is_close_op) {
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..dcfc041 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.Retry retry = ReplayOperation.Retry.NONE) {
+        base (name, ReplayOperation.Scope.LOCAL_AND_REMOTE, retry);
     }
     
-    public SendReplayOperation.only_remote(string name) {
-        base (name, ReplayOperation.Scope.REMOTE_ONLY);
+    public SendReplayOperation.only_remote(string name, ReplayOperation.Retry retry = 
ReplayOperation.Retry.NONE) {
+        base (name, ReplayOperation.Scope.REMOTE_ONLY, retry);
     }
     
     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..9380dab 100644
--- a/src/engine/imap-engine/imap-engine.vala
+++ b/src/engine/imap-engine/imap-engine.vala
@@ -58,5 +58,20 @@ 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) {
+    // 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-mark-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
index 35b51c2..f7c6069 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", Retry.REMOTE);
         
         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/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" : "")));
         


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