[geary/wip/721790-gmail-delete] First pass through



commit ac49f46ab101dc99211647c0e53492d2c7630079
Author: Jim Nelson <jim yorba org>
Date:   Fri Jan 9 13:09:02 2015 -0800

    First pass through
    
    This collects the COPYUID UIDs and opens a special one-shot connection
    to the Trash folder to delete them.  This is faster than doing a full
    synchronized open of the Folder.

 .../gmail/imap-engine-gmail-folder.vala            |   42 +++++++-
 .../imap-engine/imap-engine-generic-account.vala   |   25 ++++
 .../imap-engine/imap-engine-minimal-folder.vala    |   19 +++-
 .../replay-ops/imap-engine-copy-email.vala         |   10 ++-
 .../replay-ops/imap-engine-create-email.vala       |    2 +-
 .../replay-ops/imap-engine-move-email.vala         |    2 +-
 .../replay-ops/imap-engine-remove-email.vala       |    3 +-
 src/engine/imap/api/imap-account.vala              |   31 +++++-
 src/engine/imap/api/imap-folder.vala               |   64 +++++++++--
 src/engine/imap/command/imap-message-set.vala      |  124 ++++++++++++++++++++
 src/engine/imap/parameter/imap-list-parameter.vala |   38 ++++++
 .../imap/parameter/imap-number-parameter.vala      |    8 +-
 .../imap/parameter/imap-string-parameter.vala      |   18 +++
 .../imap/response/imap-response-code-type.vala     |    1 +
 src/engine/imap/response/imap-response-code.vala   |   20 +++
 src/engine/imap/transport/imap-deserializer.vala   |    2 +
 src/engine/util/util-collection.vala               |    4 +
 17 files changed, 391 insertions(+), 22 deletions(-)
---
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala 
b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
index e8839c0..6f73415 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -5,7 +5,7 @@
  */
 
 private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archive,
-    FolderSupport.Create {
+    FolderSupport.Create, FolderSupport.Remove {
     public GmailFolder(GmailAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
         base (account, remote, local, local_folder, special_folder_type);
@@ -21,5 +21,45 @@ private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archiv
         Cancellable? cancellable = null) throws Error {
         yield expunge_email_async(email_ids, cancellable);
     }
+    
+    public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+        Cancellable? cancellable = null) throws Error {
+        // Gmail offers no direct path via IMAP to delete a message from the server ... must move
+        // the message to the Trash folder, then delete it there
+        
+        // Get path to Trash folder
+        Geary.Folder? trash = account.get_special_folder(SpecialFolderType.TRASH);
+        if (trash == null)
+            throw new EngineError.NOT_FOUND("%s: Trash folder not found for removal", to_string());
+        
+        // Copy to Trash, collect UIDs (note that copying to Trash is like a move; the copied
+        // messages are removed from all labels)
+        Gee.Set<Imap.UID>? uids = yield copy_email_uids_async(email_ids, trash.path, cancellable);
+        if (uids == null || uids.size == 0)
+            return;
+        
+        debug("COPIED, %d UIDS", uids.size);
+        
+        // For speed reasons, use a detched Imap.Folder object to delete moved emails; this is a
+        // separate connection and is not synchronized with the database, but also avoids a full
+        // folder normalization, which can be a heavyweight operation
+        Imap.Folder imap_trash = yield ((GenericAccount) account).fetch_detached_folder_async(trash.path,
+            cancellable);
+        
+        debug("FETCHED DETACHED FOLDER");
+        
+        yield imap_trash.open_async(cancellable);
+        try {
+            debug("REMOVING FROM TRASH");
+            yield imap_trash.remove_email_async(Imap.MessageSet.uid_sparse(uids), cancellable);
+            debug("REMOVED FROM TRASH");
+        } finally {
+            try {
+                yield imap_trash.close_async(null);
+            } catch (Error err) {
+                // ignored
+            }
+        }
+    }
 }
 
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 50f6001..f60dbe7 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -526,6 +526,31 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
         return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
     }
     
+    /**
+     * Returns an Imap.Folder that is not connected (is detached) to a MinimalFolder or any other
+     * ImapEngine container.
+     *
+     * This is useful for one-shot operations that need to bypass the heavyweight synchronziation
+     * routines inside MinimalFolder.  This also means that operations performed on this Folder will
+     * not be reflected in the local database unless there's a separate connection to the server
+     * that is notified or detects these changes.
+     *
+     * It is not recommended this object be held open long-term, or that its status or notifications
+     * be directly written to the database unless you know exactly what you're doing.  ''Caveat
+     * implementor.''
+     */
+    internal async Imap.Folder fetch_detached_folder_async(Geary.FolderPath path, Cancellable? cancellable)
+        throws Error {
+        check_open();
+        
+        if (local_only.has_key(path)) {
+            throw new EngineError.NOT_FOUND("%s: path %s points to local-only folder, not IMAP",
+                to_string(), path.to_string());
+        }
+        
+        return yield remote.fetch_unrecycled_folder_async(path, cancellable);
+    }
+    
     private Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>> get_mailbox_search_names() {
         Gee.HashMap<Geary.SpecialFolderType, string> mailbox_search_names
             = new Gee.HashMap<Geary.SpecialFolderType, string>();
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 45e49f0..bc6a0ea 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -1236,19 +1236,30 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
         replay_queue.schedule(mark);
         yield mark.wait_for_ready_async(cancellable);
     }
-
+    
     public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
         Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
-        check_open("copy_email_async");
-        check_ids("copy_email_async", to_copy);
+        yield copy_email_uids_async(to_copy, destination, cancellable);
+    }
+    
+    /**
+     * Returns the UIDs for the copies messages in the destination folder.
+     */
+    internal async Gee.Set<Imap.UID>? copy_email_uids_async(Gee.List<Geary.EmailIdentifier> to_copy,
+        Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+        check_open("copy_email_uids_async");
+        check_ids("copy_email_uids_async", to_copy);
         
         // watch for copying to this folder, which is treated as a no-op
         if (destination.equal_to(path))
-            return;
+            return null;
         
         CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
         replay_queue.schedule(copy);
+        
         yield copy.wait_for_ready_async(cancellable);
+        
+        return copy.destination_uids;
     }
 
     public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
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 0d68888..f054780 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
@@ -5,6 +5,8 @@
  */
 
 private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation {
+    public Gee.Set<Imap.UID> destination_uids = new Gee.HashSet<Imap.UID>();
+    
     private MinimalFolder engine;
     private Gee.HashSet<ImapDB.EmailIdentifier> to_copy = new Gee.HashSet<ImapDB.EmailIdentifier>();
     private Geary.FolderPath destination;
@@ -46,8 +48,12 @@ private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation
         
         if (uids != null && uids.size > 0) {
             Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(uids);
-            foreach (Imap.MessageSet msg_set in msg_sets)
-                yield engine.remote_folder.copy_email_async(msg_set, destination, cancellable);
+            foreach (Imap.MessageSet msg_set in msg_sets) {
+                Gee.Map<Imap.UID, Imap.UID>? src_dst_uids = yield engine.remote_folder.copy_email_async(
+                    msg_set, destination, cancellable);
+                if (src_dst_uids != null)
+                    destination_uids.add_all(src_dst_uids.values);
+            }
         }
         
         return ReplayOperation.Status.COMPLETED;
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 3104c4c..3750ff0 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
@@ -54,7 +54,7 @@ private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperatio
         // operation atomic.
         if (cancellable.is_cancelled()) {
             yield engine.remote_folder.remove_email_async(
-                new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid), null);
+                new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid).to_list(), null);
             
             throw new IOError.CANCELLED("CreateEmail op cancelled after create");
         }
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 236932b..4fc13b2 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
@@ -71,7 +71,7 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
             ImapDB.EmailIdentifier.to_uids(moved_ids));
         foreach (Imap.MessageSet msg_set in msg_sets) {
             yield engine.remote_folder.copy_email_async(msg_set, destination, null);
-            yield engine.remote_folder.remove_email_async(msg_set, null);
+            yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
         }
         
         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 234ab36..f1ee2d3 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
@@ -62,8 +62,7 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
         // that the signal has already been fired.
         Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(
             ImapDB.EmailIdentifier.to_uids(removed_ids));
-        foreach (Imap.MessageSet msg_set in msg_sets)
-            yield engine.remote_folder.remove_email_async(msg_set, cancellable);
+        yield engine.remote_folder.remove_email_async(msg_sets, cancellable);
         
         return ReplayOperation.Status.COMPLETED;
     }
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 9da0272..b7de768 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -245,7 +245,7 @@ private class Geary.Imap.Account : BaseObject {
         
         Imap.Folder folder;
         if (!mailbox_info.attrs.is_no_select) {
-            StatusData status = yield fetch_status_async(path, StatusDataType.all(), cancellable);
+            StatusData status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
             
             folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info);
         } else {
@@ -257,6 +257,35 @@ private class Geary.Imap.Account : BaseObject {
         return folder;
     }
     
+    /**
+     * Returns an Imap.Folder that is not stored long-term in the Imap.Account object.
+     *
+     * This means the Imap.Folder is not re-used or used by multiple users or containers.  This is
+     * useful for one-shot operations on the server.
+     */
+    public async Imap.Folder fetch_unrecycled_folder_async(FolderPath path, Cancellable? cancellable)
+        throws Error {
+        check_open();
+        
+        MailboxInformation? mailbox_info = path_to_mailbox.get(path);
+        if (mailbox_info == null)
+            throw_not_found(path);
+        
+        // construct canonical folder path
+        FolderPath folder_path = mailbox_info.get_path(inbox_specifier);
+        
+        Imap.Folder folder;
+        if (!mailbox_info.attrs.is_no_select) {
+            StatusData status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
+            
+            folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info);
+        } else {
+            folder = new Imap.Folder.unselectable(folder_path, session_mgr, mailbox_info);
+        }
+        
+        return folder;
+    }
+    
     internal void folders_removed(Gee.Collection<FolderPath> paths) {
         foreach (FolderPath path in paths) {
             if (path_to_mailbox.has_key(path))
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index 82f316a..b4b6ab1 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -626,7 +626,8 @@ private class Geary.Imap.Folder : BaseObject {
         return map;
     }
     
-    public async void remove_email_async(MessageSet msg_set, Cancellable? cancellable) throws Error {
+    public async void remove_email_async(Gee.List<MessageSet> msg_sets, Cancellable? cancellable)
+        throws Error {
         check_open();
         
         Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
@@ -634,8 +635,14 @@ private class Geary.Imap.Folder : BaseObject {
         
         Gee.List<Command> cmds = new Gee.ArrayList<Command>();
         
-        StoreCommand store_cmd = new StoreCommand(msg_set, flags, true, false);
-        cmds.add(store_cmd);
+        // Build STORE command for all MessageSets, see if all are UIDs so we can use UID EXPUNGE
+        bool all_uid = true;
+        foreach (MessageSet msg_set in msg_sets) {
+            if (!msg_set.is_uid)
+                all_uid = false;
+            
+            cmds.add(new StoreCommand(msg_set, flags, true, false));
+        }
         
         // TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work
         // for us).  See:
@@ -644,10 +651,12 @@ private class Geary.Imap.Folder : BaseObject {
         // However, current client implementation doesn't properly close INBOX when application
         // shuts down, which means deleted messages return at application start.  See:
         // http://redmine.yorba.org/issues/6865
-        if (msg_set.is_uid && session.capabilities.supports_uidplus())
-            cmds.add(new ExpungeCommand.uid(msg_set));
-        else
+        if (all_uid && session.capabilities.supports_uidplus()) {
+            foreach (MessageSet msg_set in msg_sets)
+                cmds.add(new ExpungeCommand.uid(msg_set));
+        } else {
             cmds.add(new ExpungeCommand());
+        }
         
         yield exec_commands_async(cmds, null, null, cancellable);
     }
@@ -675,15 +684,52 @@ private class Geary.Imap.Folder : BaseObject {
         yield exec_commands_async(cmds, null, null, cancellable);
     }
     
-    public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
+    // Returns a mapping of the source UID to the destination UID.  If the MessageSet is not for
+    // UIDs, then null is returned.  If the server doesn't support COPYUID, null is returned.
+    public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set, FolderPath destination,
         Cancellable? cancellable) throws Error {
         check_open();
         
         CopyCommand cmd = new CopyCommand(msg_set,
             new MailboxSpecifier.from_folder_path(destination, null));
         
-        yield exec_commands_async(Geary.iterate<Command>(cmd).to_array_list(), null,
-            null, cancellable);
+        Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async(
+            Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable);
+        
+        if (!responses.has_key(cmd))
+            return null;
+        
+        StatusResponse response = responses.get(cmd);
+        if (response.response_code != null && msg_set.is_uid) {
+            Gee.List<UID>? src_uids = null;
+            Gee.List<UID>? dst_uids = null;
+            try {
+                response.response_code.get_copyuid(null, out src_uids, out dst_uids);
+            } catch (ImapError ierr) {
+                debug("Unable to retrieve COPYUID UIDs: %s", ierr.message);
+            }
+            
+            if (!Collection.is_empty(src_uids) && !Collection.is_empty(dst_uids)) {
+                Gee.Map<UID, UID> copyuids = new Gee.HashMap<UID, UID>();
+                int ctr = 0;
+                for (;;) {
+                    UID? src_uid = (ctr < src_uids.size) ? src_uids[ctr] : null;
+                    UID? dst_uid = (ctr < dst_uids.size) ? dst_uids[ctr] : null;
+                    
+                    if (src_uid != null && dst_uid != null)
+                        copyuids.set(src_uid, dst_uid);
+                    else
+                        break;
+                    
+                    ctr++;
+                }
+                
+                if (copyuids.size > 0)
+                    return copyuids;
+            }
+        }
+        
+        return null;
     }
     
     public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria, Cancellable? cancellable)
diff --git a/src/engine/imap/command/imap-message-set.vala b/src/engine/imap/command/imap-message-set.vala
index 85af92a..e8399d4 100644
--- a/src/engine/imap/command/imap-message-set.vala
+++ b/src/engine/imap/command/imap-message-set.vala
@@ -20,6 +20,8 @@ public class Geary.Imap.MessageSet : BaseObject {
     // etc.)
     private const int MAX_SPARSE_VALUES_PER_SET = 50;
     
+    private delegate void ParserCallback(int64 value) throws ImapError;
+    
     /**
      * True if the { link MessageSet} was created with a UID or a UID range.
      *
@@ -109,6 +111,121 @@ public class Geary.Imap.MessageSet : BaseObject {
     }
     
     /**
+     * Parses a string representing a { link MessageSet} into a List of { link SequenceNumber}s.
+     *
+     * See the note at { link parse_uid} about limitations of this method.
+     *
+     * Returns null if the string or parsed set is empty.
+     *
+     * @see uid_parse
+     */
+    public static Gee.List<SequenceNumber>? parse(string str) throws ImapError {
+        Gee.List<SequenceNumber> seq_nums = new Gee.ArrayList<SequenceNumber>();
+        parse_string(str, (value) => { seq_nums.add(new SequenceNumber.checked(value)); });
+        
+        return seq_nums.size > 0 ? seq_nums : null;
+    }
+    
+    /**
+     * Parses a string representing a { link MessageSet} into a List of { link UID}s.
+     *
+     * Note that this is currently designed for parsing message set responses from the server,
+     * specifically for COPYUID, which has some limitations in what may be returned.  Notably, the
+     * asterisk ("*") symbol may not be returned.  Thus, this method does not properly parse
+     * the full range of message set notation and can't even be trusted to reverse-parse the output
+     * of this class.  A full implementation might be considered later.
+     *
+     * Because COPYUID returns values in the order copied, this method returns a List, not a Set,
+     * of values.  They are in the order received (and properly deal with ranges in backwards
+     * order, i.e. "12:10").  This means duplicates may be encountered multiple times if the server
+     * returns those values.
+     *
+     * Returns null if the string or parsed set is empty.
+     */
+    public static Gee.List<UID>? uid_parse(string str) throws ImapError {
+        Gee.List<UID> uids = new Gee.ArrayList<UID>();
+        parse_string(str, (value) => { uids.add(new UID.checked(value)); });
+        
+        return uids.size > 0 ? uids : null;
+    }
+    
+    private static void parse_string(string str, ParserCallback cb) throws ImapError {
+        StringBuilder acc = new StringBuilder();
+        int64 start_range = -1;
+        bool in_range = false;
+        
+        unichar ch;
+        int index = 0;
+        while (str.get_next_char(ref index, out ch)) {
+            // if number, add to accumulator
+            if (ch.isdigit()) {
+                acc.append_unichar(ch);
+                
+                continue;
+            }
+            
+            // look for special characters and deal with them
+            switch (ch) {
+                case ':':
+                    // range separator
+                    if (in_range)
+                        throw new ImapError.INVALID("Bad range specifier in message set \"%s\"", str);
+                    
+                    in_range = true;
+                    
+                    // store current accumulated value as start of range
+                    start_range = int64.parse(acc.str);
+                    acc = new StringBuilder();
+                break;
+                
+                case ',':
+                    // number separator
+                    
+                    // if in range, treat as end-of-range
+                    if (in_range) {
+                        // don't be forgiving here
+                        if (String.is_empty(acc.str))
+                            throw new ImapError.INVALID("Bad range specifier in message set \"%s\"", str);
+                        
+                        process_range(start_range, int64.parse(acc.str), cb);
+                        in_range = false;
+                    } else {
+                        // Be forgiving here
+                        if (String.is_empty(acc.str))
+                            continue;
+                        
+                        cb(int64.parse(acc.str));
+                    }
+                    
+                    // reset accumulator
+                    acc = new StringBuilder();
+                break;
+                
+                default:
+                    // unknown character, treat with great violence
+                    throw new ImapError.INVALID("Bad character '%s' in message set \"%s\"",
+                        ch.to_string(), str);
+            }
+        }
+        
+        // report last bit remaining in accumulator
+        if (!String.is_empty(acc.str)) {
+            if (in_range)
+                process_range(start_range, int64.parse(acc.str), cb);
+            else
+                cb(int64.parse(acc.str));
+        } else if (in_range) {
+            throw new ImapError.INVALID("Incomplete range specifier in message set \"%s\"", str);
+        }
+    }
+    
+    private static void process_range(int64 start, int64 end, ParserCallback cb) throws ImapError {
+        int64 count_by = (start <= end) ? 1 : -1;
+        for (int64 ctr = start; ctr != end + count_by; ctr += count_by)
+            cb(ctr);
+    }
+    
+    /**
      * Convert a collection of { link SequenceNumber}s into a list of { link MessageSet}s.
      *
      * Although this could return a single MessageSet, large collections could create an IMAP
@@ -247,6 +364,13 @@ public class Geary.Imap.MessageSet : BaseObject {
         return new UnquotedStringParameter(value);
     }
     
+    /**
+     * Returns the { link MessageSet} in a Gee.List.
+     */
+    public Gee.List<MessageSet> to_list() {
+        return iterate<MessageSet>(this).to_array_list();
+    }
+    
     public string to_string() {
         return "%s::%s".printf(is_uid ? "UID" : "pos", value);
     }
diff --git a/src/engine/imap/parameter/imap-list-parameter.vala 
b/src/engine/imap/parameter/imap-list-parameter.vala
index e561102..4e57d95 100644
--- a/src/engine/imap/parameter/imap-list-parameter.vala
+++ b/src/engine/imap/parameter/imap-list-parameter.vala
@@ -325,6 +325,44 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
     }
     
     //
+    // Number retrieval
+    //
+    
+    /**
+     * Returns a { link NumberParameter} at index, null if not of that type.
+     *
+     * @see get_if
+     */
+    public NumberParameter? get_if_number(int index) {
+        return (NumberParameter?) get_if(index, typeof(NumberParameter));
+    }
+    
+    /**
+     * Returns a { link NumberParameter} at index.
+     *
+     * Like { link get_as_string}, this method will attempt some coercion.  In this case,
+     * { link QuotedStringParameter} and { link UnquotedStringParameter}s will be converted to
+     * NumberParameter, if appropriate.
+     */
+    public NumberParameter get_as_number(int index) throws ImapError {
+        Parameter param = get_required(index);
+        
+        NumberParameter? numberp = param as NumberParameter;
+        if (numberp != null)
+            return numberp;
+        
+        StringParameter? stringp = param as StringParameter;
+        if (stringp != null) {
+            numberp = stringp.coerce_to_number_parameter();
+            if (numberp != null)
+                return numberp;
+        }
+        
+        throw new ImapError.TYPE_ERROR("Parameter %d not of type number or string (is %s)", index,
+            param.get_type().name());
+    }
+    
+    //
     // List retrieval
     //
     
diff --git a/src/engine/imap/parameter/imap-number-parameter.vala 
b/src/engine/imap/parameter/imap-number-parameter.vala
index e32c3cc..260db0d 100644
--- a/src/engine/imap/parameter/imap-number-parameter.vala
+++ b/src/engine/imap/parameter/imap-number-parameter.vala
@@ -5,7 +5,8 @@
  */
 
 /**
- * A representation of a numerical { link Parameter} in an IMAP { link Command}.
+ * A representation of a numerical { link Parameter} in an IMAP { link Command} or
+ * { link ServerResponse}.
  *
  * See [[http://tools.ietf.org/html/rfc3501#section-4.2]]
  */
@@ -86,6 +87,11 @@ public class Geary.Imap.NumberParameter : UnquotedStringParameter {
                 has_nonzero = true;
         }
         
+        // watch for negative but no numeric portion
+        if (is_negative && str.length == 1)
+            return false;
+        
+        // no such thing as negative zero
         if (is_negative && !has_nonzero)
             is_negative = false;
         
diff --git a/src/engine/imap/parameter/imap-string-parameter.vala 
b/src/engine/imap/parameter/imap-string-parameter.vala
index 01b6f0b..a1978cf 100644
--- a/src/engine/imap/parameter/imap-string-parameter.vala
+++ b/src/engine/imap/parameter/imap-string-parameter.vala
@@ -188,5 +188,23 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
         
         return int64.parse(ascii).clamp(clamp_min, clamp_max);
     }
+    
+    /**
+     * Attempts to coerce a { link StringParameter} into a { link NumberParameter}.
+     *
+     * Returns null if unsuitable for a NumberParameter.
+     *
+     * @see NumberParameter.is_ascii_number
+     */
+    public NumberParameter? coerce_to_number_parameter() {
+        NumberParameter? numberp = this as NumberParameter;
+        if (numberp != null)
+            return numberp;
+        
+        if (NumberParameter.is_ascii_numeric(ascii, null))
+            return new NumberParameter.from_ascii(ascii);
+        
+        return null;
+    }
 }
 
diff --git a/src/engine/imap/response/imap-response-code-type.vala 
b/src/engine/imap/response/imap-response-code-type.vala
index 3bb0c84..c5c52be 100644
--- a/src/engine/imap/response/imap-response-code-type.vala
+++ b/src/engine/imap/response/imap-response-code-type.vala
@@ -23,6 +23,7 @@ public class Geary.Imap.ResponseCodeType : BaseObject, Gee.Hashable<ResponseCode
     public const string BADCHARSET = "badcharset";
     public const string CAPABILITY = "capability";
     public const string CLIENTBUG = "clientbug";
+    public const string COPYUID = "copyuid";
     public const string MYRIGHTS = "myrights";
     public const string NEWNAME = "newname";
     public const string NONEXISTANT = "nonexistant";
diff --git a/src/engine/imap/response/imap-response-code.vala 
b/src/engine/imap/response/imap-response-code.vala
index fbd8ce8..e8887e2 100644
--- a/src/engine/imap/response/imap-response-code.vala
+++ b/src/engine/imap/response/imap-response-code.vala
@@ -89,6 +89,26 @@ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter {
         return capabilities;
     }
     
+    /**
+     * Parses the { link ResponseCode} into UIDPLUS' COPYUID response, if possible.
+     *
+     * Note that the { link UID}s are returned from the server in the order the messages
+     * were copied.
+     *
+     * See [[http://tools.ietf.org/html/rfc4315#section-3]]
+     *
+     * @throws ImapError.INVALID if not COPYUID.
+     */
+    public void get_copyuid(out UIDValidity uidvalidity, out Gee.List<UID>? source_uids,
+        out Gee.List<UID>? destination_uids) throws ImapError {
+        if (!get_response_code_type().is_value(ResponseCodeType.COPYUID))
+            throw new ImapError.INVALID("Not COPYUID response code: %s", to_string());
+        
+        uidvalidity = new UIDValidity.checked(get_as_number(1).as_int64());
+        source_uids = MessageSet.uid_parse(get_as_string(2).ascii);
+        destination_uids = MessageSet.uid_parse(get_as_string(3).ascii);
+    }
+    
     public override string to_string() {
         return "[%s]".printf(stringize_list());
     }
diff --git a/src/engine/imap/transport/imap-deserializer.vala 
b/src/engine/imap/transport/imap-deserializer.vala
index 63cfd2f..3f6be64 100644
--- a/src/engine/imap/transport/imap-deserializer.vala
+++ b/src/engine/imap/transport/imap-deserializer.vala
@@ -449,6 +449,8 @@ public class Geary.Imap.Deserializer : BaseObject {
         
         if (quoted)
             save_parameter(new QuotedStringParameter(str));
+        else if (NumberParameter.is_ascii_numeric(str, null))
+            save_parameter(new NumberParameter.from_ascii(str));
         else
             save_parameter(new UnquotedStringParameter(str));
         
diff --git a/src/engine/util/util-collection.vala b/src/engine/util/util-collection.vala
index 294de11..dc9f43d 100644
--- a/src/engine/util/util-collection.vala
+++ b/src/engine/util/util-collection.vala
@@ -8,6 +8,10 @@ namespace Geary.Collection {
 
 public delegate uint8 ByteTransformer(uint8 b);
 
+public inline bool is_empty(Gee.Collection? c) {
+    return c == null || c.size == 0;
+}
+
 // A substitute for ArrayList<G>.wrap() for compatibility with older versions of Gee.
 public Gee.ArrayList<G> array_list_wrap<G>(G[] a, owned Gee.EqualDataFunc<G>? equal_func = null) {
     Gee.ArrayList<G> list = new Gee.ArrayList<G>(equal_func);


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