[geary] Better special folder detection/creation



commit 4552757994309ab975945b4d50c59963582022d4
Author: Charles Lindsay <chaz yorba org>
Date:   Tue Feb 11 15:24:01 2014 -0800

    Better special folder detection/creation
    
    This looks for some translatable common names for special folders like
    Sent Mail, Drafts, Spam and Trash, instead of only relying on the
    server's special-use or xlist extensions.  If the server doesn't report
    special-use/xlist, we look for common folder names, creating them on the
    server if necessary, so we always have folders necessary for tasks like
    saving drafts or sent mail.
    
    Closes: bgo #713492

 po/POTFILES.in                                     |    7 +-
 src/CMakeLists.txt                                 |    7 +-
 src/client/application/geary-controller.vala       |   64 +-
 src/client/composer/composer-window.vala           |   25 +-
 src/engine/abstract/geary-abstract-account.vala    |    3 +
 src/engine/api/geary-account-information.vala      |  108 ++
 src/engine/api/geary-account.vala                  |    9 +
 src/engine/api/geary-folder-path.vala              |   56 +-
 src/engine/imap-db/outbox/smtp-outbox-folder.vala  |    6 +-
 .../gmail/imap-engine-gmail-account.vala           |   16 +-
 .../gmail/imap-engine-gmail-folder.vala            |    2 +-
 .../gmail/imap-engine-gmail-search-folder.vala     |    2 +-
 .../imap-engine/imap-engine-email-prefetcher.vala  |    4 +-
 .../imap-engine/imap-engine-generic-account.vala   |  172 +++-
 .../imap-engine-generic-all-mail-folder.vala       |   18 -
 .../imap-engine-generic-drafts-folder.vala         |   28 -
 .../imap-engine/imap-engine-generic-folder.vala    | 1287 +-------------------
 .../imap-engine-generic-sent-mail-folder.vala      |   22 -
 .../imap-engine-generic-trash-folder.vala          |   23 -
 .../imap-engine/imap-engine-minimal-folder.vala    | 1295 ++++++++++++++++++++
 .../imap-engine/imap-engine-replay-queue.vala      |    4 +-
 .../other/imap-engine-other-account.vala           |   19 +-
 .../other/imap-engine-other-folder.vala            |    7 +-
 .../outlook/imap-engine-outlook-account.vala       |   23 +-
 .../outlook/imap-engine-outlook-drafts-folder.vala |   19 +
 .../outlook/imap-engine-outlook-folder.vala        |    7 +-
 .../imap-engine-abstract-list-email.vala           |    8 +-
 .../replay-ops/imap-engine-copy-email.vala         |    4 +-
 .../replay-ops/imap-engine-create-email.vala       |    4 +-
 .../replay-ops/imap-engine-fetch-email.vala        |    4 +-
 .../replay-ops/imap-engine-list-email-by-id.vala   |    2 +-
 .../imap-engine-list-email-by-sparse-id.vala       |    2 +-
 .../replay-ops/imap-engine-mark-email.vala         |    4 +-
 .../replay-ops/imap-engine-move-email.vala         |    4 +-
 .../replay-ops/imap-engine-remove-email.vala       |    4 +-
 .../replay-ops/imap-engine-replay-append.vala      |    4 +-
 .../replay-ops/imap-engine-replay-disconnect.vala  |    4 +-
 .../replay-ops/imap-engine-replay-removal.vala     |    4 +-
 .../imap-engine-server-search-email.vala           |    2 +-
 .../yahoo/imap-engine-yahoo-account.vala           |   21 +-
 .../yahoo/imap-engine-yahoo-folder.vala            |    7 +-
 src/engine/imap/api/imap-account.vala              |   12 +
 src/engine/imap/command/imap-create-command.vala   |   23 +
 src/engine/util/util-iterable.vala                 |    7 +
 44 files changed, 1765 insertions(+), 1588 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a4ce70c..361fce6 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -154,6 +154,7 @@ src/engine/imap/command/imap-close-command.vala
 src/engine/imap/command/imap-command.vala
 src/engine/imap/command/imap-compress-command.vala
 src/engine/imap/command/imap-copy-command.vala
+src/engine/imap/command/imap-create-command.vala
 src/engine/imap/command/imap-examine-command.vala
 src/engine/imap/command/imap-expunge-command.vala
 src/engine/imap/command/imap-fetch-command.vala
@@ -195,11 +196,8 @@ src/engine/imap-engine/imap-engine-contact-store.vala
 src/engine/imap-engine/imap-engine-email-flag-watcher.vala
 src/engine/imap-engine/imap-engine-email-prefetcher.vala
 src/engine/imap-engine/imap-engine-generic-account.vala
-src/engine/imap-engine/imap-engine-generic-all-mail-folder.vala
-src/engine/imap-engine/imap-engine-generic-drafts-folder.vala
 src/engine/imap-engine/imap-engine-generic-folder.vala
-src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
-src/engine/imap-engine/imap-engine-generic-trash-folder.vala
+src/engine/imap-engine/imap-engine-minimal-folder.vala
 src/engine/imap-engine/imap-engine-replay-operation.vala
 src/engine/imap-engine/imap-engine-replay-queue.vala
 src/engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -208,6 +206,7 @@ src/engine/imap-engine/other/imap-engine-other-account.vala
 src/engine/imap-engine/other/imap-engine-other-folder.vala
 src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
 src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
 src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
 src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
 src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3eed7a6..b992514 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -94,6 +94,7 @@ engine/imap/command/imap-close-command.vala
 engine/imap/command/imap-command.vala
 engine/imap/command/imap-compress-command.vala
 engine/imap/command/imap-copy-command.vala
+engine/imap/command/imap-create-command.vala
 engine/imap/command/imap-examine-command.vala
 engine/imap/command/imap-expunge-command.vala
 engine/imap/command/imap-fetch-command.vala
@@ -182,11 +183,8 @@ engine/imap-engine/imap-engine-contact-store.vala
 engine/imap-engine/imap-engine-email-flag-watcher.vala
 engine/imap-engine/imap-engine-email-prefetcher.vala
 engine/imap-engine/imap-engine-generic-account.vala
-engine/imap-engine/imap-engine-generic-all-mail-folder.vala
-engine/imap-engine/imap-engine-generic-drafts-folder.vala
 engine/imap-engine/imap-engine-generic-folder.vala
-engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
-engine/imap-engine/imap-engine-generic-trash-folder.vala
+engine/imap-engine/imap-engine-minimal-folder.vala
 engine/imap-engine/imap-engine-replay-operation.vala
 engine/imap-engine/imap-engine-replay-queue.vala
 engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -197,6 +195,7 @@ engine/imap-engine/other/imap-engine-other-account.vala
 engine/imap-engine/other/imap-engine-other-folder.vala
 engine/imap-engine/outlook/imap-engine-outlook-account.vala
 engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
 engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
 engine/imap-engine/replay-ops/imap-engine-copy-email.vala
 engine/imap-engine/replay-ops/imap-engine-create-email.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index fb09d59..a198bcf 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1426,15 +1426,7 @@ public class GearyController : Geary.BaseObject {
         actions.get_action(ACTION_MARK_AS_STARRED).set_visible(unstarred_selected);
         actions.get_action(ACTION_MARK_AS_UNSTARRED).set_visible(starred_selected);
         
-        Geary.Folder? spam_folder = null;
-        try {
-            spam_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM);
-        } catch (Error e) {
-            debug("Could not locate special spam folder: %s", e.message);
-        }
-        
-        if (spam_folder != null &&
-            current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
+        if (current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
             current_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX) {
             if (current_folder.special_folder_type == Geary.SpecialFolderType.SPAM) {
                 // We're in the spam folder.
@@ -1446,7 +1438,7 @@ public class GearyController : Geary.BaseObject {
                 actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL;
             }
         } else {
-            // No Spam folder, or we're in Drafts/Outbox, so gray-out the option.
+            // We're in Drafts/Outbox, so gray-out the option.
             actions.get_action(ACTION_MARK_AS_SPAM).sensitive = false;
             actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL;
         }
@@ -1531,12 +1523,13 @@ public class GearyController : Geary.BaseObject {
         mark_email(get_selected_email_ids(false), null, flags);
     }
     
-    private void on_mark_as_spam() {
+    private async void mark_as_spam_async(Cancellable? cancellable) {
         Geary.Folder? destination_folder = null;
         if (current_folder.special_folder_type != Geary.SpecialFolderType.SPAM) {
             // Move to spam folder.
             try {
-                destination_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM);
+                destination_folder = yield current_account.get_required_special_folder_async(
+                    Geary.SpecialFolderType.SPAM, cancellable);
             } catch (Error e) {
                 debug("Error getting spam folder: %s", e.message);
             }
@@ -1553,6 +1546,10 @@ public class GearyController : Geary.BaseObject {
             on_move_conversation(destination_folder);
     }
     
+    private void on_mark_as_spam() {
+        mark_as_spam_async.begin(null);
+    }
+    
     private void copy_email(Gee.Collection<Geary.EmailIdentifier> ids,
         Geary.FolderPath destination) {
         if (ids.size > 0) {
@@ -1902,26 +1899,10 @@ public class GearyController : Geary.BaseObject {
             on_archive_or_delete_selection_finished);
     }
     
-    private bool current_folder_supports_trash(out Geary.FolderSupport.Move? move = null,
-        out Geary.FolderPath? trash_path = null) {
-        try {
-            if (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
-                && !current_folder.properties.is_local_only && current_account != null) {
-                Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
-                Geary.Folder? trash_folder = 
current_account.get_special_folder(Geary.SpecialFolderType.TRASH);
-                if (supports_move != null && trash_folder != null) {
-                    move = supports_move;
-                    trash_path = trash_folder.path;
-                    return true;
-                }
-            }
-        } catch (Error e) {
-            debug("Error finding trash folder: %s", e.message);
-        }
-        
-        move = null;
-        trash_path = null;
-        return false;
+    private bool current_folder_supports_trash() {
+        return (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
+            && !current_folder.properties.is_local_only && current_account != null
+            && (current_folder as Geary.FolderSupport.Move) != null);
     }
     
     public bool confirm_delete(int num_messages) {
@@ -1963,13 +1944,18 @@ public class GearyController : Geary.BaseObject {
         if (trash) {
             debug("Trashing selected messages");
             
-            Geary.FolderPath? trash_path;
-            Geary.FolderSupport.Move? supports_move;
-            if (!current_folder_supports_trash(out supports_move, out trash_path))
-                debug("Folder %s doesn't support move or account %s doesn't have a trash folder",
-                    current_folder.to_string(), current_account.to_string());
-            else
-                yield supports_move.move_email_async(ids, trash_path, cancellable);
+            if (current_folder_supports_trash()) {
+                Geary.FolderPath trash_path = (yield current_account.get_required_special_folder_async(
+                    Geary.SpecialFolderType.TRASH, cancellable)).path;
+                Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
+                if (supports_move != null) {
+                    yield supports_move.move_email_async(ids, trash_path, cancellable);
+                    return;
+                }
+            }
+            
+            debug("Folder %s doesn't support move or account %s doesn't have a trash folder",
+                current_folder.to_string(), current_account.to_string());
             return;
         }
         
diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala
index 85937f2..880da63 100644
--- a/src/client/composer/composer-window.vala
+++ b/src/client/composer/composer-window.vala
@@ -343,13 +343,8 @@ public class ComposerWindow : Gtk.Window {
                         debug("Error getting message body: %s", error.message);
                     }
                     
-                    try {
-                        Geary.Folder? draft_folder = 
account.get_special_folder(Geary.SpecialFolderType.DRAFTS);
-                        if (draft_folder != null && is_referred_draft)
-                            draft_id = referred.id;
-                    } catch (Error e) {
-                        debug("Error looking up special folder: %s", e.message);
-                    }
+                    if (is_referred_draft)
+                        draft_id = referred.id;
                     
                     add_attachments(referred.attachments);
                 break;
@@ -461,7 +456,7 @@ public class ComposerWindow : Gtk.Window {
         // If there's only one account, open the drafts folder.  If there's more than one account,
         // the drafts folder will be opened by on_from_changed().
         if (!from_multiple.visible)
-            open_drafts_folder.begin(cancellable_drafts);
+            open_drafts_folder_async.begin(cancellable_drafts);
     }
     
     public ComposerWindow.from_mailto(Geary.Account account, string mailto) {
@@ -801,11 +796,11 @@ public class ComposerWindow : Gtk.Window {
     }
     
     // Returns the drafts folder for the current From account.
-    private async void open_drafts_folder(Cancellable cancellable) throws Error {
-        yield close_drafts_folder(cancellable);
+    private async void open_drafts_folder_async(Cancellable cancellable) throws Error {
+        yield close_drafts_folder_async(cancellable);
         
-        Geary.FolderSupport.Create? folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS) 
-            as Geary.FolderSupport.Create;
+        Geary.FolderSupport.Create? folder = (yield account.get_required_special_folder_async(
+            Geary.SpecialFolderType.DRAFTS, cancellable)) as Geary.FolderSupport.Create;
         
         if (folder == null)
             return; // No drafts folder.
@@ -815,7 +810,7 @@ public class ComposerWindow : Gtk.Window {
         drafts_folder = folder;
     }
     
-    private async void close_drafts_folder(Cancellable? cancellable = null) throws Error {
+    private async void close_drafts_folder_async(Cancellable? cancellable = null) throws Error {
         if (drafts_folder == null)
             return;
         
@@ -1683,7 +1678,7 @@ public class ComposerWindow : Gtk.Window {
                     from = new_account_info.get_from().to_rfc822_string();
                     set_entry_completions();
                     
-                    open_drafts_folder.begin(cancellable_drafts);
+                    open_drafts_folder_async.begin(cancellable_drafts);
                 }
             } catch (Error e) {
                 debug("Error updating account in Composer: %s", e.message);
@@ -1705,7 +1700,7 @@ public class ComposerWindow : Gtk.Window {
     }
     
     public override void destroy() {
-        close_drafts_folder.begin();
+        close_drafts_folder_async.begin();
     }
 }
 
diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala
index 29db690..7c194f8 100644
--- a/src/engine/abstract/geary-abstract-account.vala
+++ b/src/engine/abstract/geary-abstract-account.vala
@@ -104,6 +104,9 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
             .first_matching(f => f.special_folder_type == special);
     }
     
+    public abstract async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+        Cancellable? cancellable) throws Error;
+    
     public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = 
null)
         throws Error;
     
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 297799f..6955ed1 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -27,6 +27,10 @@ public class Geary.AccountInformation : BaseObject {
     private const string SMTP_STARTTLS = "smtp_starttls";
     private const string SMTP_NOAUTH = "smtp_noauth";
     private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
+    private const string DRAFTS_FOLDER_KEY = "drafts_folder";
+    private const string SENT_MAIL_FOLDER_KEY = "sent_mail_folder";
+    private const string SPAM_FOLDER_KEY = "spam_folder";
+    private const string TRASH_FOLDER_KEY = "trash_folder";
     
     //
     // "Retired" keys
@@ -80,6 +84,11 @@ public class Geary.AccountInformation : BaseObject {
     public bool default_smtp_server_ssl  { get; set; }
     public bool default_smtp_server_starttls { get; set; }
     public bool default_smtp_server_noauth { get; set; }
+    
+    public Geary.FolderPath? drafts_folder_path { get; set; default = null; }
+    public Geary.FolderPath? sent_mail_folder_path { get; set; default = null; }
+    public Geary.FolderPath? spam_folder_path { get; set; default = null; }
+    public Geary.FolderPath? trash_folder_path { get; set; default = null; }
 
     public Geary.Credentials imap_credentials { get; set; default = new Geary.Credentials(null, null); }
     public bool imap_remember_password { get; set; default = true; }
@@ -142,6 +151,15 @@ public class Geary.AccountInformation : BaseObject {
                     smtp_credentials = null;
                 }
             }
+            
+            drafts_folder_path = build_folder_path(get_string_list_value(
+                key_file, GROUP, DRAFTS_FOLDER_KEY));
+            sent_mail_folder_path = build_folder_path(get_string_list_value(
+                key_file, GROUP, SENT_MAIL_FOLDER_KEY));
+            spam_folder_path = build_folder_path(get_string_list_value(
+                key_file, GROUP, SPAM_FOLDER_KEY));
+            trash_folder_path = build_folder_path(get_string_list_value(
+                key_file, GROUP, TRASH_FOLDER_KEY));
         }
     }
     
@@ -167,6 +185,10 @@ public class Geary.AccountInformation : BaseObject {
         imap_remember_password = from.imap_remember_password;
         smtp_credentials = from.smtp_credentials;
         smtp_remember_password = from.smtp_remember_password;
+        drafts_folder_path = from.drafts_folder_path;
+        sent_mail_folder_path = from.sent_mail_folder_path;
+        spam_folder_path = from.spam_folder_path;
+        trash_folder_path = from.trash_folder_path;
     }
     
     /**
@@ -181,6 +203,61 @@ public class Geary.AccountInformation : BaseObject {
     }
     
     /**
+     * Gets the path used when Geary has found or created a special folder for
+     * this account.  This will be null if Geary has always been told about the
+     * special folders by the server, and hasn't had to go looking for them.
+     * Only the DRAFTS, SENT, SPAM, and TRASH special folder types are valid to
+     * pass to this function.
+     */
+    public Geary.FolderPath? get_special_folder_path(Geary.SpecialFolderType special) {
+        switch (special) {
+            case Geary.SpecialFolderType.DRAFTS:
+                return drafts_folder_path;
+            
+            case Geary.SpecialFolderType.SENT:
+                return sent_mail_folder_path;
+            
+            case Geary.SpecialFolderType.SPAM:
+                return spam_folder_path;
+            
+            case Geary.SpecialFolderType.TRASH:
+                return trash_folder_path;
+            
+            default:
+                assert_not_reached();
+        }
+    }
+    
+    /**
+     * Sets the path Geary will look for or create a special folder.  This is
+     * only obeyed if the server doesn't tell Geary which folders are special.
+     * Only the DRAFTS, SENT, SPAM, and TRASH special folder types are valid to
+     * pass to this function.
+     */
+    public void set_special_folder_path(Geary.SpecialFolderType special, Geary.FolderPath? path) {
+        switch (special) {
+            case Geary.SpecialFolderType.DRAFTS:
+                drafts_folder_path = path;
+            break;
+            
+            case Geary.SpecialFolderType.SENT:
+                sent_mail_folder_path = path;
+            break;
+            
+            case Geary.SpecialFolderType.SPAM:
+                spam_folder_path = path;
+            break;
+            
+            case Geary.SpecialFolderType.TRASH:
+                trash_folder_path = path;
+            break;
+            
+            default:
+                assert_not_reached();
+        }
+    }
+    
+    /**
      * Fetch the passwords for the given services.  For each service, if the
      * password is unset, use get_passwords_async() first; if the password is
      * set or it's not in the key store, use prompt_passwords_async().  Return
@@ -406,6 +483,16 @@ public class Geary.AccountInformation : BaseObject {
         }
     }
     
+    private Geary.FolderPath? build_folder_path(Gee.List<string>? parts) {
+        if (parts == null || parts.size == 0)
+            return null;
+        
+        Geary.FolderPath path = new Imap.FolderRoot(parts[0], null);
+        for (int i = 1; i < parts.size; i++)
+            path = path.get_child(parts.get(i));
+        return path;
+    }
+    
     private string get_string_value(KeyFile key_file, string group, string key, string def = "") {
         try {
             return key_file.get_value(group, key);
@@ -416,6 +503,18 @@ public class Geary.AccountInformation : BaseObject {
         return def;
     }
     
+    private Gee.List<string> get_string_list_value(KeyFile key_file, string group, string key) {
+        try {
+            string[] list = key_file.get_string_list(group, key);
+            if (list.length > 0)
+                return new Gee.ArrayList<string>.wrap(list);
+        } catch(KeyFileError err) {
+            // Ignore.
+        }
+        
+        return new Gee.ArrayList<string>();
+    }
+    
     private bool get_bool_value(KeyFile key_file, string group, string key, bool def = false) {
         try {
             return key_file.get_boolean(group, key);
@@ -490,6 +589,15 @@ public class Geary.AccountInformation : BaseObject {
             key_file.set_boolean(GROUP, SMTP_NOAUTH, default_smtp_server_noauth);
         }
         
+        key_file.set_string_list(GROUP, DRAFTS_FOLDER_KEY, (drafts_folder_path != null
+            ? drafts_folder_path.as_list().to_array() : new string[] {}));
+        key_file.set_string_list(GROUP, SENT_MAIL_FOLDER_KEY, (sent_mail_folder_path != null
+            ? sent_mail_folder_path.as_list().to_array() : new string[] {}));
+        key_file.set_string_list(GROUP, SPAM_FOLDER_KEY, (spam_folder_path != null
+            ? spam_folder_path.as_list().to_array() : new string[] {}));
+        key_file.set_string_list(GROUP, TRASH_FOLDER_KEY, (trash_folder_path != null
+            ? trash_folder_path.as_list().to_array() : new string[] {}));
+        
         string data = key_file.to_data();
         string new_etag;
         
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 989039b..a144566 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -280,6 +280,15 @@ public interface Geary.Account : BaseObject {
     public abstract Geary.Folder? get_special_folder(Geary.SpecialFolderType special) throws Error;
     
     /**
+     * Returns the Folder object with the given special folder type.  The folder will be
+     * created on the server if it doesn't already exist.  An error will be thrown if the
+     * folder doesn't exist and can't be created.  The only valid special folder types that
+     * can be required are: DRAFTS, SENT, SPAM, and TRASH.
+     */
+    public abstract async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+        Cancellable? cancellable = null) throws Error;
+    
+    /**
      * Submits a ComposedEmail for delivery.  Messages may be scheduled for later delivery or immediately
      * sent.  Subscribe to the "email-sent" signal to be notified of delivery.  Note that that signal
      * does not return the ComposedEmail object but an RFC822-formatted object.  Allowing for the
diff --git a/src/engine/api/geary-folder-path.vala b/src/engine/api/geary-folder-path.vala
index 41f02a2..886f589 100644
--- a/src/engine/api/geary-folder-path.vala
+++ b/src/engine/api/geary-folder-path.vala
@@ -192,8 +192,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
     public string? get_fullpath(string? use_separator) {
         string? separator = use_separator ?? get_root().default_separator;
         
-        // no separator, no fullpath
-        if (separator == null)
+        if (separator == null && !is_root())
             return null;
         
         // use cached copy if the stars align
@@ -221,22 +220,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
         return case_sensitive ? str_hash(basename) : str_hash(basename.down());
     }
     
-    /**
-     * { inheritDoc}
-     *
-     * Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
-     * and (b) each element is compared to the corresponding path element of the other FolderPath
-     * following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
-     * are less-than longer paths, assuming the path elements are equal up to the shorter path's
-     * length.
-     *
-     * Note that the { link FolderRoot.default_separator} has no bearing on comparisons, although
-     * { link FolderPath.case_sensitive} does.
-     *
-     * Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
-     * are equal.
-     */
-    public int compare_to(Geary.FolderPath other) {
+    private int compare_internal(Geary.FolderPath other, bool allow_case_sensitive, bool normalize) {
         if (this == other)
             return 0;
         
@@ -251,8 +235,13 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
             string this_element = this_list[ctr];
             string other_element = other_list[ctr];
             
-            // if either case-sensitive, then comparison is CS
-            if (!get_folder_at(ctr).case_sensitive && !other.get_folder_at(ctr).case_sensitive) {
+            if (normalize) {
+                this_element = this_element.normalize();
+                other_element = other_element.normalize();
+            }
+            if (!allow_case_sensitive
+                // if either case-sensitive, then comparison is CS
+                || (!get_folder_at(ctr).case_sensitive && !other.get_folder_at(ctr).case_sensitive)) {
                 this_element = this_element.casefold();
                 other_element = other_element.casefold();
             }
@@ -268,6 +257,33 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
     }
     
     /**
+     * Does a Unicode-normalized, case insensitive match.  Useful for getting a rough idea if
+     * a folder matches a name, but shouldn't be used to determine strict equality.
+     */
+    public int compare_normalized_ci(Geary.FolderPath other) {
+        return compare_internal(other, false, true);
+    }
+    
+    /**
+     * { inheritDoc}
+     *
+     * Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
+     * and (b) each element is compared to the corresponding path element of the other FolderPath
+     * following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
+     * are less-than longer paths, assuming the path elements are equal up to the shorter path's
+     * length.
+     *
+     * Note that the { link FolderRoot.default_separator} has no bearing on comparisons, although
+     * { link FolderPath.case_sensitive} does.
+     *
+     * Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
+     * are equal.
+     */
+    public int compare_to(Geary.FolderPath other) {
+        return compare_internal(other, true, false);
+    }
+    
+    /**
      * { inheritDoc}
      *
      * As with { link compare_to}, the { link FolderRoot.default_separator} has no bearing on the
diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala 
b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
index 2dc8ea0..f791242 100644
--- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala
+++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
@@ -636,10 +636,10 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
     
     private async void save_sent_mail_async(Geary.RFC822.Message rfc822, Cancellable? cancellable)
         throws Error {
-        Geary.Folder? sent_mail = _account.get_special_folder(Geary.SpecialFolderType.SENT);
-        Geary.FolderSupport.Create? create = sent_mail as Geary.FolderSupport.Create;
+        Geary.FolderSupport.Create? create = (yield _account.get_required_special_folder_async(
+            Geary.SpecialFolderType.SENT, cancellable)) as Geary.FolderSupport.Create;
         if (create == null)
-            throw new EngineError.NOT_FOUND("Save sent mail enabled, but no sent mail folder");
+            throw new EngineError.NOT_FOUND("Save sent mail enabled, but no writable sent mail folder");
         
         bool open = false;
         try {
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala 
b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
index 66f2569..cf0c3d0 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
@@ -71,7 +71,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
         }
     }
     
-    protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+    protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
         ImapDB.Account local_account, ImapDB.Folder local_folder) {
         // although Gmail supports XLIST, this will be called on startup if the XLIST properties
         // for the folders hasn't been retrieved yet.  Once they've been retrieved and stored in
@@ -81,19 +81,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
         
         switch (special_folder_type) {
             case SpecialFolderType.ALL_MAIL:
-                return new GenericAllMailFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.SENT:
-                return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.TRASH:
-                return new GenericTrashFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.DRAFTS:
-                return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
+                return new MinimalFolder(this, remote_account, local_account, local_folder,
                     special_folder_type);
             
             default:
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 f6525bd..3151c3b 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -4,7 +4,7 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.GmailFolder : GenericFolder, FolderSupport.Archive {
+private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archive {
     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);
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala 
b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
index f5c0328..18aecee 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
@@ -21,7 +21,7 @@ public class Geary.ImapEngine.GmailSearchFolder : Geary.SearchFolder {
         Cancellable? cancellable = null) throws Error {
         Geary.Folder? trash_folder = null;
         try {
-            trash_folder = account.get_special_folder(Geary.SpecialFolderType.TRASH);
+            trash_folder = yield account.get_required_special_folder_async(Geary.SpecialFolderType.TRASH, 
cancellable);
         } catch (Error e) {
             debug("Error looking up trash folder in %s: %s", account.to_string(), e.message);
         }
diff --git a/src/engine/imap-engine/imap-engine-email-prefetcher.vala 
b/src/engine/imap-engine/imap-engine-email-prefetcher.vala
index 9f574f4..619e102 100644
--- a/src/engine/imap-engine/imap-engine-email-prefetcher.vala
+++ b/src/engine/imap-engine/imap-engine-email-prefetcher.vala
@@ -23,7 +23,7 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
     public Nonblocking.CountingSemaphore active_sem { get; private set;
         default = new Nonblocking.CountingSemaphore(null); }
     
-    private unowned ImapEngine.GenericFolder folder;
+    private unowned ImapEngine.MinimalFolder folder;
     private int start_delay_sec;
     private Nonblocking.Mutex mutex = new Nonblocking.Mutex();
     private Gee.TreeSet<Geary.Email> prefetch_emails = new Gee.TreeSet<Geary.Email>(
@@ -31,7 +31,7 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
     private uint schedule_id = 0;
     private Cancellable cancellable = new Cancellable();
     
-    public EmailPrefetcher(ImapEngine.GenericFolder folder, int start_delay_sec = PREFETCH_DELAY_SEC) {
+    public EmailPrefetcher(ImapEngine.MinimalFolder folder, int start_delay_sec = PREFETCH_DELAY_SEC) {
         assert(start_delay_sec > 0);
         
         this.folder = folder;
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 668dd04..87741b6 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -13,8 +13,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
     private Imap.Account remote;
     private ImapDB.Account local;
     private bool open = false;
-    private Gee.HashMap<FolderPath, GenericFolder> folder_map = new Gee.HashMap<
-        FolderPath, GenericFolder>();
+    private Gee.HashMap<FolderPath, MinimalFolder> folder_map = new Gee.HashMap<
+        FolderPath, MinimalFolder>();
     private Gee.HashMap<FolderPath, Folder> local_only = new Gee.HashMap<FolderPath, Folder>();
     private uint refresh_folder_timeout_id = 0;
     private bool in_refresh_enumerate = false;
@@ -194,12 +194,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
         message("%s: Rebuild complete", to_string());
     }
     
-    // Subclasses should implement this to return their flavor of a GenericFolder with the
+    // Subclasses should implement this to return their flavor of a MinimalFolder with the
     // appropriate interfaces attached.  The returned folder should have its SpecialFolderType
     // set using either the properties from the local folder or its path.
     //
     // This won't be called to build the Outbox or search folder, but for all others (including Inbox) it 
will.
-    protected abstract GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+    protected abstract MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
         ImapDB.Account local_account, ImapDB.Folder local_folder);
     
     // Subclasses with specific SearchFolder implementations should override
@@ -208,15 +208,15 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
         return new SearchFolder(this);
     }
     
-    private GenericFolder build_folder(ImapDB.Folder local_folder) {
+    private MinimalFolder build_folder(ImapDB.Folder local_folder) {
         return Geary.Collection.get_first(build_folders(
             Geary.iterate<ImapDB.Folder>(local_folder).to_array_list()));
     }
 
-    private Gee.Collection<GenericFolder> build_folders(Gee.Collection<ImapDB.Folder> local_folders) {
+    private Gee.Collection<MinimalFolder> build_folders(Gee.Collection<ImapDB.Folder> local_folders) {
         Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
-        Gee.ArrayList<GenericFolder> built_folders = new Gee.ArrayList<GenericFolder>();
-        Gee.ArrayList<GenericFolder> return_folders = new Gee.ArrayList<GenericFolder>();
+        Gee.ArrayList<MinimalFolder> built_folders = new Gee.ArrayList<MinimalFolder>();
+        Gee.ArrayList<MinimalFolder> return_folders = new Gee.ArrayList<MinimalFolder>();
         
         foreach(ImapDB.Folder local_folder in local_folders) {
             if (folder_map.has_key(local_folder.get_path()))
@@ -226,7 +226,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
         }
         
         foreach(ImapDB.Folder folder_to_build in folders_to_build) {
-            GenericFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
+            MinimalFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
             folder_map.set(folder.path, folder);
             built_folders.add(folder);
             return_folders.add(folder);
@@ -425,26 +425,156 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
             yield local.clone_folder_async(remote_folder, cancellable);
         }
         
-        // Fetch the local account's version of the folder for the GenericFolder
+        // Fetch the local account's version of the folder for the MinimalFolder
         return build_folder((ImapDB.Folder) yield local.fetch_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>();
+        mailbox_search_names.set(Geary.SpecialFolderType.DRAFTS,
+            // List of folder names to match for Drafts, separated by |.  Please add localized common
+            // names for the Drafts folder, leaving in the English names as well.  The first in the list
+            // will be the default, so please add the most common localized name to the front.
+            _("Drafts | Draft"));
+        mailbox_search_names.set(Geary.SpecialFolderType.SENT,
+            // List of folder names to match for Sent Mail, separated by |.  Please add localized common
+            // names for the Sent Mail folder, leaving in the English names as well.  The first in the list
+            // will be the default, so please add the most common localized name to the front.
+            _("Sent | Sent Mail | Sent Email | Sent E-Mail"));
+        mailbox_search_names.set(Geary.SpecialFolderType.SPAM,
+            // List of folder names to match for Spam, separated by |.  Please add localized common
+            // names for the Spam folder, leaving in the English names as well.  The first in the list
+            // will be the default, so please add the most common localized name to the front.
+            _("Junk | Spam | Junk Mail | Junk Email | Junk E-Mail | Bulk Mail | Bulk Email | Bulk E-Mail"));
+        mailbox_search_names.set(Geary.SpecialFolderType.TRASH,
+            // List of folder names to match for Trash, separated by |.  Please add localized common
+            // names for the Trash folder, leaving in the English names as well.  The first in the list
+            // will be the default, so please add the most common localized name to the front.
+            _("Trash | Rubbish | Rubbish Bin"));
+        
+        Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>> compiled
+            = new Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>>();
+        
+        foreach (Geary.SpecialFolderType t in mailbox_search_names.keys) {
+            compiled.set(t, Geary.iterate_array<string>(mailbox_search_names.get(t).split("|"))
+                .map<string>(n => n.strip()).to_array_list());
+        }
+        
+        return compiled;
+    }
+    
+    private async Geary.Folder ensure_special_folder_async(Geary.SpecialFolderType special,
+        Cancellable? cancellable) throws Error {
+        Geary.Folder? folder = get_special_folder(special);
+        if (folder != null)
+            return folder;
+        
+        MinimalFolder? minimal_folder = null;
+        Geary.FolderPath? path = information.get_special_folder_path(special);
+        if (path != null) {
+            debug("Previously used %s for special folder %s", path.to_string(), special.to_string());
+        } else {
+            // This is the first time we're turning a non-special folder into a special one.
+            // After we do this, we'll record which one we picked in the account info.
+            
+            Gee.ArrayList<string> search_names = get_mailbox_search_names().get(special);
+            foreach (string search_name in search_names) {
+                Geary.FolderPath search_path = new Imap.FolderRoot(search_name, null);
+                foreach (Geary.FolderPath test_path in folder_map.keys) {
+                    if (test_path.compare_normalized_ci(search_path) == 0) {
+                        path = search_path;
+                        break;
+                    }
+                }
+                if (path != null)
+                    break;
+            }
+            if (path == null) {
+                foreach (string search_name in search_names) {
+                    Geary.FolderPath search_path = new Imap.FolderRoot(
+                        Imap.MailboxSpecifier.CANONICAL_INBOX_NAME, null).get_child(search_name);
+                    foreach (Geary.FolderPath test_path in folder_map.keys) {
+                        if (test_path.compare_normalized_ci(search_path) == 0) {
+                            path = search_path;
+                            break;
+                        }
+                    }
+                    if (path != null)
+                        break;
+                }
+            }
+            
+            if (path == null)
+                path = new Imap.FolderRoot(search_names[0], null);
+            
+            information.set_special_folder_path(special, path);
+            yield information.store_async(cancellable);
+        }
+        
+        if (path in folder_map.keys) {
+            debug("Promoting %s to special folder %s", path.to_string(), special.to_string());
+            
+            minimal_folder = folder_map.get(path);
+        } else {
+            debug("Creating %s to use as special folder %s", path.to_string(), special.to_string());
+            
+            // TODO: ignore error due to already existing.
+            yield remote.create_folder_async(path, cancellable);
+            minimal_folder = (MinimalFolder) yield fetch_folder_async(path, cancellable);
+        }
+        
+        minimal_folder.set_special_folder_type(special);
+        return minimal_folder;
+    }
+    
+    public override async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+        Cancellable? cancellable) throws Error {
+        switch (special) {
+            case Geary.SpecialFolderType.DRAFTS:
+            case Geary.SpecialFolderType.SENT:
+            case Geary.SpecialFolderType.SPAM:
+            case Geary.SpecialFolderType.TRASH:
+            break;
+            
+            default:
+                throw new EngineError.BAD_PARAMETERS(
+                    "Invalid special folder type %s passed to get_required_special_folder_async",
+                    special.to_string());
+        }
+        
+        check_open();
+        
+        return yield ensure_special_folder_async(special, cancellable);
+    }
+    
+    private async void ensure_special_folders_async(Cancellable? cancellable) throws Error {
+        Geary.SpecialFolderType[] required = {
+            Geary.SpecialFolderType.DRAFTS,
+            Geary.SpecialFolderType.SENT,
+            Geary.SpecialFolderType.SPAM,
+            Geary.SpecialFolderType.TRASH,
+        };
+        foreach (Geary.SpecialFolderType special in required)
+            yield ensure_special_folder_async(special, cancellable);
+    }
+    
     private async void update_folders_async(Gee.Map<FolderPath, Geary.Folder> existing_folders,
         Gee.Map<FolderPath, Imap.Folder> remote_folders, Cancellable? cancellable) {
         // update all remote folders properties in the local store and active in the system
         Gee.HashSet<Geary.FolderPath> altered_paths = new Gee.HashSet<Geary.FolderPath>();
         foreach (Imap.Folder remote_folder in remote_folders.values) {
-            GenericFolder? generic_folder = existing_folders.get(remote_folder.path)
-                as GenericFolder;
-            if (generic_folder == null)
+            MinimalFolder? minimal_folder = existing_folders.get(remote_folder.path)
+                as MinimalFolder;
+            if (minimal_folder == null)
                 continue;
             
             // only worry about alterations if the remote is openable
             if (remote_folder.properties.is_openable.is_possible()) {
-                ImapDB.Folder local_folder = generic_folder.local_folder;
+                ImapDB.Folder local_folder = minimal_folder.local_folder;
                 
                 if (remote_folder.properties.have_contents_changed(local_folder.get_properties(),
-                    generic_folder.to_string())) {
+                    minimal_folder.to_string())) {
                     altered_paths.add(remote_folder.path);
                 }
             }
@@ -462,8 +592,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
             // (but only promote, not demote, since getting the special folder type via its
             // properties relies on the optional XLIST extension)
             // use this iteration to add discovered properties to map
-            if (generic_folder.special_folder_type == SpecialFolderType.NONE)
-                
generic_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
+            if (minimal_folder.special_folder_type == SpecialFolderType.NONE)
+                
minimal_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
         }
         
         // If path in remote but not local, need to add it
@@ -500,7 +630,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
                 debug("Unable to fetch local folder after cloning: %s", convert_err.message);
             }
         }
-        Gee.Collection<Geary.Folder> engine_added = new Gee.ArrayList<Geary.Folder>();
+        Gee.Collection<MinimalFolder> engine_added = new Gee.ArrayList<Geary.Folder>();
         engine_added.add_all(build_folders(folders_to_build));
         
         // TODO: Remove local folders no longer available remotely.
@@ -523,6 +653,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
             if (altered.size > 0)
                 notify_folders_contents_altered(altered);
         }
+        
+        try {
+            yield ensure_special_folders_async(cancellable);
+        } catch (Error e) {
+            warning("Unable to ensure special folders: %s", e.message);
+        }
     }
     
     public override async void send_email_async(Geary.ComposedEmail composed,
diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala 
b/src/engine/imap-engine/imap-engine-generic-folder.vala
index ade5748..b09f034 100644
--- a/src/engine/imap-engine/imap-engine-generic-folder.vala
+++ b/src/engine/imap-engine/imap-engine-generic-folder.vala
@@ -4,1291 +4,22 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.FolderSupport.Copy,
-    Geary.FolderSupport.Mark, Geary.FolderSupport.Move {
-    private const int FORCE_OPEN_REMOTE_TIMEOUT_SEC = 10;
-    private const int DEFAULT_REESTABLISH_DELAY_MSEC = 10;
-    private const int MAX_REESTABLISH_DELAY_MSEC = 1000;
-    
-    public override Account account { get { return _account; } }
-    
-    public override FolderProperties properties { get { return _properties; } }
-    
-    public override FolderPath path {
-        get {
-            return local_folder.get_path();
-        }
-    }
-    
-    private SpecialFolderType _special_folder_type;
-    public override SpecialFolderType special_folder_type {
-        get {
-            return _special_folder_type;
-        }
-    }
-    
-    internal ImapDB.Folder local_folder  { get; protected set; }
-    internal Imap.Folder? remote_folder { get; protected set; default = null; }
-    internal EmailPrefetcher email_prefetcher { get; private set; }
-    internal EmailFlagWatcher email_flag_watcher;
-    
-    private weak GenericAccount _account;
-    private Geary.AggregatedFolderProperties _properties = new Geary.AggregatedFolderProperties(
-        false, false);
-    private Imap.Account remote;
-    private ImapDB.Account local;
-    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 int remote_count = -1;
-    private uint open_remote_timer_id = 0;
-    private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
-    
+private class Geary.ImapEngine.GenericFolder : MinimalFolder, Geary.FolderSupport.Remove,
+    Geary.FolderSupport.Create {
     public GenericFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
-        _account = account;
-        this.remote = remote;
-        this.local = local;
-        this.local_folder = local_folder;
-        _special_folder_type = special_folder_type;
-        _properties.add(local_folder.get_properties());
-        
-        email_flag_watcher = new EmailFlagWatcher(this);
-        email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
-        
-        email_prefetcher = new EmailPrefetcher(this);
-        
-        local_folder.email_complete.connect(on_email_complete);
-    }
-    
-    ~EngineFolder() {
-        if (open_count > 0)
-            warning("Folder %s destroyed without closing", to_string());
-        
-        local_folder.email_complete.disconnect(on_email_complete);
-    }
-    
-    public void set_special_folder_type(SpecialFolderType new_type) {
-        SpecialFolderType old_type = _special_folder_type;
-        _special_folder_type = new_type;
-        if(old_type != new_type)
-            notify_special_folder_type_changed(old_type, new_type);
-    }
-    
-    public override Geary.Folder.OpenState get_open_state() {
-        if (open_count == 0)
-            return Geary.Folder.OpenState.CLOSED;
-        
-        return (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL;
-    }
-    
-    // Returns the synchronized remote count (-1 if not opened) and the last seen remote count (stored
-    // locally, -1 if not available)
-    //
-    // Return value is the remote_count, unless the remote is unopened, in which case it's the
-    // last_seen_remote_count (which may be -1).
-    //
-    // remote_count, last_seen_remote_count, and returned value do not reflect any notion of
-    // messages marked for removal
-    internal int get_remote_counts(out int remote_count, out int last_seen_remote_count) {
-        remote_count = this.remote_count;
-        last_seen_remote_count = local_folder.get_properties().select_examine_messages;
-        if (last_seen_remote_count < 0)
-            last_seen_remote_count = local_folder.get_properties().status_messages;
-        
-        return (remote_count >= 0) ? remote_count : last_seen_remote_count;
-    }
-    
-    private async bool normalize_folders(Geary.Imap.Folder remote_folder, Geary.Folder.OpenFlags open_flags,
-        Cancellable? cancellable) throws Error {
-        debug("%s: Begin normalizing remote and local folders", to_string());
-        
-        Geary.Imap.FolderProperties local_properties = local_folder.get_properties();
-        Geary.Imap.FolderProperties remote_properties = remote_folder.properties;
-        
-        // and both must have their next UID's (it's possible they don't if it's a non-selectable
-        // folder)
-        if (local_properties.uid_next == null || local_properties.uid_validity == null) {
-            debug("%s: Unable to verify UIDs: missing local UIDNEXT (%s) and/or UIDVALIDITY (%s)",
-                to_string(), (local_properties.uid_next == null).to_string(),
-                (local_properties.uid_validity == null).to_string());
-            
-            return false;
-        }
-        
-        if (remote_properties.uid_next == null || remote_properties.uid_validity == null) {
-            debug("%s: Unable to verify UIDs: missing remote UIDNEXT (%s) and/or UIDVALIDITY (%s)",
-                to_string(), (remote_properties.uid_next == null).to_string(),
-                (remote_properties.uid_validity == null).to_string());
-            
-            return false;
-        }
-        
-        // If UIDVALIDITY changes, all email in the folder must be removed as the UIDs are now
-        // invalid ... we merely detach the emails (leaving their contents behind) so duplicate
-        // detection can fix them up.  But once all UIDs are removed, it's much like the next
-        // if case where no earliest UID available, so simply exit.
-        //
-        // see http://tools.ietf.org/html/rfc3501#section-2.3.1.1
-        if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
-            debug("%s: UID validity changed, detaching all email: %s -> %s", to_string(),
-                local_properties.uid_validity.value.to_string(),
-                remote_properties.uid_validity.value.to_string());
-            
-            yield local_folder.detach_all_emails_async(cancellable);
-            
-            return true;
-        }
-        
-        // fetch email from earliest email to last to (a) remove any deletions and (b) update
-        // any flags that may have changed
-        ImapDB.EmailIdentifier? local_earliest_id = yield local_folder.get_earliest_id_async(cancellable);
-        ImapDB.EmailIdentifier? local_latest_id = yield local_folder.get_latest_id_async(cancellable);
-        
-        // verify still open; this is required throughout after each yield, as a close_async() can
-        // come in ay any time since this does not run in the context of open_async()
-        check_open("normalize_folders (local earliest/latest UID)");
-        
-        // if no earliest UID, that means no messages in local store, so nothing to update
-        if (local_earliest_id == null || local_latest_id == null) {
-            debug("%s: local store empty, nothing to normalize", to_string());
-            
-            return true;
-        }
-        
-        assert(local_earliest_id.has_uid());
-        assert(local_latest_id.has_uid());
-        
-        // if any messages are still marked for removal from last time, that means the EXPUNGE
-        // never arrived from the server, in which case the folder is "dirty" and needs a full
-        // normalization
-        Gee.Set<ImapDB.EmailIdentifier>? already_marked_ids = yield local_folder.get_marked_ids_async(
-            cancellable);
-        
-        // however, there may be enqueue ReplayOperations waiting to remove messages on the server
-        // that marked some or all of those messages
-        Gee.HashSet<ImapDB.EmailIdentifier> to_be_removed = new Gee.HashSet<ImapDB.EmailIdentifier>();
-        replay_queue.get_ids_to_be_remote_removed(to_be_removed);
-        
-        // don't consider those already marked as "already marked" if they were not leftover from
-        // the last open of this folder
-        if (already_marked_ids != null)
-            already_marked_ids.remove_all(to_be_removed);
-        
-        bool is_dirty = (already_marked_ids != null && already_marked_ids.size > 0);
-        
-        if (is_dirty)
-            debug("%s: %d remove markers found, folder is dirty", to_string(), already_marked_ids.size);
-        
-        // if UIDNEXT has changed, that indicates messages have been appended (and possibly removed)
-        int64 uidnext_diff = remote_properties.uid_next.value - local_properties.uid_next.value;
-        
-        int local_message_count = (local_properties.select_examine_messages >= 0)
-            ? local_properties.select_examine_messages : 0;
-        int remote_message_count = (remote_properties.select_examine_messages >= 0)
-            ? remote_properties.select_examine_messages : 0;
-        
-        // if UIDNEXT is the same as last time AND the total count of email is the same, then
-        // nothing has been added or removed
-        if (!is_dirty && uidnext_diff == 0 && local_message_count == remote_message_count) {
-            debug("%s: No messages added/removed since last opened, normalization completed", to_string());
-            
-            return true;
-        }
-        
-        // a full normalize works from the highest possible UID on the remote and work down to the lowest 
UID on
-        // the local; this covers all messages appended since last seen as well as any removed
-        Imap.UID last_uid = remote_properties.uid_next.previous(true);
-        
-        // if the difference in UIDNEXT values equals the difference in message count, then only
-        // an append could have happened, so only pull in the new messages ... note that this is not 
foolproof,
-        // as UIDs are not guaranteed to increase by 1; however, this is a standard implementation practice,
-        // so it's worth looking for
-        //
-        // (Also, this cannot fail; if this situation exists, then it cannot by definition indicate another
-        // situation, esp. messages being removed.)
-        Imap.UID first_uid;
-        if (!is_dirty && uidnext_diff == (remote_message_count - local_message_count)) {
-            first_uid = local_latest_id.uid.next(true);
-            
-            debug("%s: Messages only appended (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering 
mail UIDs %s:%s",
-                to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
-                local_properties.select_examine_messages, remote_properties.select_examine_messages, 
uidnext_diff.to_string(),
-                first_uid.to_string(), last_uid.to_string());
-        } else {
-            first_uid = local_earliest_id.uid;
-            
-            debug("%s: Messages appended/removed (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering 
mail UIDs %s:%s",
-                to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
-                local_properties.select_examine_messages, remote_properties.select_examine_messages, 
uidnext_diff.to_string(),
-                first_uid.to_string(), last_uid.to_string());
-        }
-        
-        // get all the UIDs in said range from the local store, sorted; convert to non-null
-        // for ease of use later
-        Gee.Set<Imap.UID>? local_uids = yield local_folder.list_uids_by_range_async(
-            first_uid, last_uid, true, cancellable);
-        if (local_uids == null)
-            local_uids = new Gee.HashSet<Imap.UID>();
-        
-        check_open("normalize_folders (list local)");
-        
-        // Do the same on the remote ... make non-null for ease of use later
-        Gee.Set<Imap.UID>? remote_uids = yield remote_folder.list_uids_async(
-            new Imap.MessageSet.uid_range(first_uid, last_uid), cancellable);
-        if (remote_uids == null)
-            remote_uids = new Gee.HashSet<Imap.UID>();
-        
-        check_open("normalize_folders (list remote)");
-        
-        debug("%s: Loaded local (%d) and remote (%d) UIDs, normalizing...", to_string(),
-            local_uids.size, remote_uids.size);
-        
-        Gee.HashSet<Imap.UID> removed_uids = new Gee.HashSet<Imap.UID>();
-        Gee.HashSet<Imap.UID> appended_uids = new Gee.HashSet<Imap.UID>();
-        Gee.HashSet<Imap.UID> inserted_uids = new Gee.HashSet<Imap.UID>();
-        
-        // Because the number of UIDs being processed can be immense in large folders, process
-        // in a background thread
-        yield Nonblocking.Concurrent.global.schedule_async(() => {
-            // walk local UIDs looking for UIDs no longer on remote, removing those that are available
-            // make the next pass that much shorter
-            foreach (Imap.UID local_uid in local_uids) {
-                // if in local but not remote, consider removed from remote
-                if (!remote_uids.remove(local_uid))
-                    removed_uids.add(local_uid);
-            }
-            
-            // everything remaining in remote has been added since folder last seen ... whether they're
-            // discovered (inserted) or appended depends on the highest local UID
-            foreach (Imap.UID remote_uid in remote_uids) {
-                if (remote_uid.compare_to(local_latest_id.uid) > 0)
-                    appended_uids.add(remote_uid);
-                else
-                    inserted_uids.add(remote_uid);
-            }
-            
-            // the UIDs marked for removal are going to be re-inserted into the vector once they're
-            // cleared, so add them here as well
-            if (already_marked_ids != null) {
-                foreach (ImapDB.EmailIdentifier id in already_marked_ids) {
-                    assert(id.has_uid());
-                    
-                    if (!appended_uids.contains(id.uid))
-                        inserted_uids.add(id.uid);
-                }
-            }
-        }, cancellable);
-        
-        debug("%s: changes since last seen: removed=%d appended=%d inserted=%d", to_string(),
-            removed_uids.size, appended_uids.size, inserted_uids.size);
-        
-        // fetch from the server the local store's required flags for all appended/inserted messages
-        // (which is simply equal to all remaining remote UIDs)
-        Gee.List<Geary.Email>? to_create = null;
-        if (remote_uids.size > 0) {
-            // for new messages, get the local store's required fields (which provide duplicate
-            // detection)
-            to_create = yield remote_folder.list_email_async(
-                new Imap.MessageSet.uid_sparse(remote_uids.to_array()), ImapDB.Folder.REQUIRED_FIELDS,
-                cancellable);
-        }
-        
-        check_open("normalize_folders (list remote appended/inserted required fields)");
-        
-        // store new messages and add IDs to the appended/discovered EmailIdentifier buckets
-        Gee.Set<ImapDB.EmailIdentifier> appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
-        Gee.Set<ImapDB.EmailIdentifier> locally_appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
-        Gee.Set<ImapDB.EmailIdentifier> inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
-        Gee.Set<ImapDB.EmailIdentifier> locally_inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
-        if (to_create != null && to_create.size > 0) {
-            Gee.Map<Email, bool>? created_or_merged = yield local_folder.create_or_merge_email_async(
-                to_create, cancellable);
-            assert(created_or_merged != null);
-            
-            // it's possible a large number of messages have come in, so process them in the
-            // background
-            yield Nonblocking.Concurrent.global.schedule_async(() => {
-                foreach (Email email in created_or_merged.keys) {
-                    ImapDB.EmailIdentifier id = (ImapDB.EmailIdentifier) email.id;
-                    bool created = created_or_merged.get(email);
-                    
-                    // report all appended email, but separate out email never seen before (created)
-                    // as locally-appended
-                    if (appended_uids.contains(id.uid)) {
-                        appended_ids.add(id);
-                        
-                        if (created)
-                            locally_appended_ids.add(id);
-                    } else if (inserted_uids.contains(id.uid)) {
-                        inserted_ids.add(id);
-                        
-                        if (created)
-                            locally_inserted_ids.add(id);
-                    }
-                }
-            }, cancellable);
-            
-            debug("%s: Finished creating/merging %d emails", to_string(), created_or_merged.size);
-        }
-        
-        check_open("normalize_folders (created/merged appended/inserted emails)");
-        
-        // Convert removed UIDs into EmailIdentifiers and detach immediately
-        Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
-        if (removed_uids.size > 0) {
-            removed_ids = yield local_folder.get_ids_async(removed_uids,
-                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
-            if (removed_ids != null && removed_ids.size > 0) {
-                yield local_folder.detach_multiple_emails_async(removed_ids, cancellable);
-            }
-        }
-        
-        check_open("normalize_folders (removed emails)");
-        
-        // remove any extant remove markers, as everything is accounted for now, except for those
-        // waiting to be removed in the queue
-        yield local_folder.clear_remove_markers_async(to_be_removed, cancellable);
-        
-        check_open("normalize_folders (clear remove markers)");
-        
-        //
-        // now normalized
-        // notify subscribers of changes
-        //
-        
-        Folder.CountChangeReason count_change_reason = Folder.CountChangeReason.NONE;
-        
-        if (removed_ids != null && removed_ids.size > 0) {
-            // there may be operations pending on the remote queue for these removed emails; notify
-            // operations that the email has shuffled off this mortal coil
-            replay_queue.notify_remote_removed_ids(removed_ids);
-            
-            // notify subscribers about emails that have been removed
-            debug("%s: Notifying of %d removed emails since last opened", to_string(), removed_ids.size);
-            notify_email_removed(removed_ids);
-            
-            count_change_reason |= Folder.CountChangeReason.REMOVED;
-        }
-        
-        // notify inserted (new email located somewhere inside the local vector)
-        if (inserted_ids.size > 0) {
-            debug("%s: Notifying of %d inserted emails since last opened", to_string(), inserted_ids.size);
-            notify_email_inserted(inserted_ids);
-            
-            count_change_reason |= Folder.CountChangeReason.INSERTED;
-        }
-        
-        // notify inserted (new email located somewhere inside the local vector that had to be
-        // created, i.e. no portion was stored locally)
-        if (locally_inserted_ids.size > 0) {
-            debug("%s: Notifying of %d locally inserted emails since last opened", to_string(),
-                locally_inserted_ids.size);
-            notify_email_locally_inserted(locally_inserted_ids);
-            
-            count_change_reason |= Folder.CountChangeReason.INSERTED;
-        }
-        
-        // notify appended (new email added since the folder was last opened)
-        if (appended_ids.size > 0) {
-            debug("%s: Notifying of %d appended emails since last opened", to_string(), appended_ids.size);
-            notify_email_appended(appended_ids);
-            
-            count_change_reason |= Folder.CountChangeReason.APPENDED;
-        }
-        
-        // notify locally appended (new email never seen before added since the folder was last
-        // opened)
-        if (locally_appended_ids.size > 0) {
-            debug("%s: Notifying of %d locally appended emails since last opened", to_string(),
-                locally_appended_ids.size);
-            notify_email_locally_appended(locally_appended_ids);
-            
-            count_change_reason |= Folder.CountChangeReason.APPENDED;
-        }
-        
-        if (count_change_reason != Folder.CountChangeReason.NONE) {
-            debug("%s: Notifying of %Xh count change reason (%d remote messages)", to_string(),
-                count_change_reason, remote_message_count);
-            notify_email_count_changed(remote_message_count, count_change_reason);
-        }
-        
-        debug("%s: Completed normalize_folder", to_string());
-        
-        return true;
-    }
-    
-    public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
-        if (open_count == 0 || remote_semaphore == null)
-            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
-        // an open_async, it's reset at close time
-        if (!remote_opened) {
-            debug("wait_for_open_async %s: opening remote on demand...", to_string());
-            
-            remote_opened = true;
-            open_remote_async.begin(open_flags, null);
-        }
-        
-        if (!yield remote_semaphore.wait_for_result_async(cancellable))
-            throw new EngineError.ALREADY_CLOSED("%s failed to open", to_string());
-    }
-    
-    public override async bool open_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable = null)
-        throws Error {
-        if (open_count++ > 0) {
-            // even if opened or opening, respect the NO_DELAY flag
-            if (open_flags.is_all_set(OpenFlags.NO_DELAY)) {
-                cancel_remote_open_timer();
-                wait_for_open_async.begin();
-            }
-            
-            debug("Not opening %s: already open (open_count=%d)", to_string(), open_count);
-            
-            return false;
-        }
-        
-        this.open_flags = open_flags;
-        
-        open_internal(open_flags, cancellable);
-        
-        return true;
-    }
-    
-    private void open_internal(Folder.OpenFlags open_flags, Cancellable? cancellable) {
-        remote_semaphore = new Geary.Nonblocking.ReportingSemaphore<bool>(false);
-        
-        // start the replay queue
-        replay_queue = new ReplayQueue(this);
-        
-        // 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
-        // fast local-only operations to occur, local-only either because (a) the folder has all
-        // the information required (for a list or fetch operation), or (b) the operation was de
-        // facto local-only.  In particular, EmailStore will open and close lots of folders,
-        // causing a lot of connection setup and teardown
-        //
-        // However, want to eventually open, otherwise if there's no user interaction (i.e. a
-        // second account Inbox they don't manipulate), no remote connection will ever be made,
-        // meaning that folder normalization never happens and unsolicited notifications never
-        // arrive
-        if (open_flags.is_all_set(OpenFlags.NO_DELAY))
-            wait_for_open_async.begin();
-        else
-            start_remote_open_timer();
+        base (account, remote, local, local_folder, special_folder_type);
     }
     
-    private void start_remote_open_timer() {
-        if (open_remote_timer_id != 0)
-            Source.remove(open_remote_timer_id);
-        
-        open_remote_timer_id = Timeout.add_seconds(FORCE_OPEN_REMOTE_TIMEOUT_SEC, on_open_remote_timeout);
-    }
-    
-    private void cancel_remote_open_timer() {
-        if (open_remote_timer_id == 0)
-            return;
-        
-        Source.remove(open_remote_timer_id);
-        open_remote_timer_id = 0;
-    }
-    
-    private bool on_open_remote_timeout() {
-        open_remote_timer_id = 0;
-        
-        // remote was not forced open due to caller, so open now
-        wait_for_open_async.begin();
-        
-        return false;
-    }
-    
-    private async void open_remote_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable) {
-        cancel_remote_open_timer();
-        
-        // watch for folder closing before this call got a chance to execute
-        if (open_count == 0)
-            return;
-        
-        try {
-            debug("Fetching information for remote folder %s", to_string());
-            Imap.Folder folder = yield remote.fetch_folder_async(local_folder.get_path(),
-                cancellable);
-            
-            debug("Opening remote folder %s", folder.to_string());
-            yield folder.open_async(cancellable);
-            
-            // allow subclasses to examine the opened folder and resolve any vital
-            // inconsistencies
-            if (yield normalize_folders(folder, open_flags, cancellable)) {
-                // update flags, properties, etc.
-                yield local.update_folder_select_examine_async(folder, cancellable);
-                
-                // signals
-                folder.appended.connect(on_remote_appended);
-                folder.removed.connect(on_remote_removed);
-                folder.disconnected.connect(on_remote_disconnected);
-            
-                // state
-                remote_count = folder.properties.email_total;
-                
-                // all set; bless the remote folder as opened
-                remote_folder = folder;
-            } else {
-                debug("Unable to prepare remote folder %s: normalize_folders() failed", to_string());
-                notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
-                
-                // schedule immediate close
-                close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
-                    cancellable);
-                
-                return;
-            }
-        } catch (Error open_err) {
-            debug("Unable to open or prepare remote folder %s: %s", to_string(), open_err.message);
-            notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, open_err);
-            
-            // schedule immediate close and force reestablishment
-            close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, true,
-                cancellable);
-            
-            return;
-        }
-        
-        // 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", count_err.message);
-            
-            count = 0;
-        }
-        
-        // 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);
-        } catch (Error notify_err) {
-            debug("Unable to fire semaphore notifying remote folder ready/not ready: %s",
-                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);
-    }
-    
-    public override async void close_async(Cancellable? cancellable = null) throws Error {
-        if (open_count == 0 || --open_count > 0)
-            return;
-        
-        if (remote_folder != null)
-            _properties.remove(remote_folder.properties);
-        
-        yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
-            cancellable);
-    }
-    
-    // NOTE: This bypasses open_count and forces the Folder closed.
-    internal async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason 
remote_reason,
-        bool force_reestablish, Cancellable? cancellable) {
-        cancel_remote_open_timer();
-        
-        // only flushing pending ReplayOperations if this is a "clean" close, not forced due to
-        // error
-        bool flush_pending = !remote_reason.is_error();
-        
-        // If closing due to error, notify all operations waiting for the remote that it's not
-        // coming available ... this wakes up any ReplayOperation blocking on wait_for_open_async(),
-        // necessary in order to finish ReplayQueue.close_async (i.e. to prevent deadlock); this
-        // is necessary because it's possible for this method to be called before the remote_folder
-        // has even had a chance to open.
-        //
-        // Note that we don't want to do this for a clean close, because we want to flush out
-        // pending operations first
-        Imap.Folder? closing_remote_folder = null;
-        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());
-            }
-        } catch (Error replay_queue_err) {
-            debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
-        }
-        
-        replay_queue = null;
-        
-        // if a "clean" close, now go ahead and close the folder
-        if (flush_pending)
-            closing_remote_folder = clear_remote_folder();
-        
-        if (closing_remote_folder != null || force_reestablish) {
-            // to avoid keeping the caller waiting while the remote end closes (i.e. drops the
-            // connection or performs an IMAP CLOSE operation), close it in the background and
-            // reestablish connection there, if necessary
-            //
-            // TODO: Problem with this is that we cannot effectively signal or report a close error,
-            // because by the time this operation completes the folder is considered closed.  That
-            // may not be important to most callers, however.
-            //
-            // It also means the reference to the Folder must be maintained until completely
-            // closed.  Also not a problem, as GenericAccount does that internally.  However, this
-            // might be an issue if GenericAccount removes this folder due to a user command or
-            // detection on the server, so this background op keeps a reference to the Folder
-            close_remote_folder_async.begin(this, closing_remote_folder, remote_reason,
-                force_reestablish);
-        }
-        
-        remote_opened = false;
-        
-        // if remote reason is an error, then close_remote_folder_async() will be performing
-        // reestablishment, so go no further
-        if ((remote_reason.is_error() && closing_remote_folder != null) || force_reestablish)
-            return;
-        
-        // forced closed one way or another, so reset state
-        open_count = 0;
-        reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
-        
-        // use remote_reason even if remote_folder was null; it could be that the error occurred
-        // while opening and remote_folder was yet unassigned ... also, need to call this every
-        // time, even if remote was not fully opened, as some callers rely on order of signals
-        notify_closed(remote_reason);
-        
-        // see above note for why this must be called every time
-        notify_closed(local_reason);
-        
-        notify_closed(CloseReason.FOLDER_CLOSED);
-        
-        debug("Folder %s closed", to_string());
-    }
-    
-    // Returns the remote_folder, if it was set
-    private Imap.Folder? clear_remote_folder() {
-        if (remote_folder != null) {
-            // disconnect signals before ripping out reference
-            remote_folder.appended.disconnect(on_remote_appended);
-            remote_folder.removed.disconnect(on_remote_removed);
-            remote_folder.disconnected.disconnect(on_remote_disconnected);
-        }
-        
-        Imap.Folder? old_remote_folder = remote_folder;
-        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);
-        }
-        
-        return old_remote_folder;
-    }
-    
-    // See note in close_async() for why this method is static and uses an owned ref
-    private static async void close_remote_folder_async(owned GenericFolder folder,
-        owned Imap.Folder? remote_folder, Folder.CloseReason remote_reason, bool force_reestablish) {
-        // force the remote closed; if due to a remote disconnect and plan on reopening, *still*
-        // need to do this
-        try {
-            if (remote_folder != null)
-                yield remote_folder.close_async(null);
-        } catch (Error err) {
-            debug("Unable to close remote %s: %s", remote_folder.to_string(), err.message);
-            
-            // fallthrough
-        }
-        
-        // reestablish connection (which requires renormalizing the remote with the local) if
-        // close was in error
-        if (remote_reason.is_error() || force_reestablish) {
-            debug("Reestablishing broken connection to %s in %dms", folder.to_string(),
-                folder.reestablish_delay_msec);
-            Timeout.add(folder.reestablish_delay_msec, () => {
-                folder.open_internal(OpenFlags.NO_DELAY, null);
-                
-                return false;
-            });
-            
-            folder.reestablish_delay_msec = (folder.reestablish_delay_msec * 2).clamp(
-                DEFAULT_REESTABLISH_DELAY_MSEC, MAX_REESTABLISH_DELAY_MSEC);
-        }
-    }
-    
-    public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
-        out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+    public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
         Cancellable? cancellable = null) throws Error {
-        low = null;
-        high = null;
-        
-        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? map
-            = yield account.get_containing_folders_async(ids, cancellable);
-        
-        if (map != null) {
-            Gee.ArrayList<Geary.EmailIdentifier> in_folder = new Gee.ArrayList<Geary.EmailIdentifier>();
-            foreach (Geary.EmailIdentifier id in map.get_keys()) {
-                if (path in map.get(id))
-                    in_folder.add(id);
-            }
-            
-            if (in_folder.size > 0) {
-                Gee.SortedSet<Geary.EmailIdentifier> sorted = Geary.EmailIdentifier.sort(in_folder);
-                
-                low = sorted.first();
-                high = sorted.last();
-            }
-        }
-    }
-    
-    private void on_email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids) {
-        notify_email_locally_complete(email_ids);
-    }
-    
-    private void on_remote_appended(int reported_remote_count) {
-        debug("%s on_remote_appended: remote_count=%d reported_remote_count=%d", to_string(), remote_count,
-            reported_remote_count);
-        
-        if (reported_remote_count < 0)
-            return;
-        
-        // from the new remote total and the old remote total, glean the SequenceNumbers of the
-        // new email(s)
-        Gee.List<Imap.SequenceNumber> positions = new Gee.ArrayList<Imap.SequenceNumber>();
-        for (int pos = remote_count + 1; pos <= reported_remote_count; pos++)
-            positions.add(new Imap.SequenceNumber(pos));
-        
-        // store the remote count NOW, as further appended messages could arrive before the
-        // ReplayAppend executes
-        remote_count = reported_remote_count;
-        
-        if (positions.size > 0)
-            replay_queue.schedule_server_notification(new ReplayAppend(this, reported_remote_count, 
positions));
-    }
-    
-    // Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
-    // normalized placeholder in the local database of the message, so all positions are
-    // properly relative to the end of the message list; once this is done, notify user of new
-    // messages.  If duplicates, create_email_async() will fall through to an updated merge,
-    // which is exactly what we want.
-    //
-    // This MUST only be called from ReplayAppend.
-    internal async void do_replay_appended_messages(int reported_remote_count,
-        Gee.List<Imap.SequenceNumber> remote_positions) {
-        StringBuilder positions_builder = new StringBuilder("( ");
-        foreach (Imap.SequenceNumber remote_position in remote_positions)
-            positions_builder.append_printf("%s ", remote_position.to_string());
-        positions_builder.append(")");
-        
-        debug("%s do_replay_appended_message: current remote_count=%d reported_remote_count=%d 
remote_positions=%s",
-            to_string(), remote_count, reported_remote_count, positions_builder.str);
-        
-        if (remote_positions.size == 0)
-            return;
-        
-        Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>();
-        Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>();
-        try {
-            Imap.MessageSet msg_set = new Imap.MessageSet.sparse(remote_positions.to_array());
-            Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(msg_set,
-                ImapDB.Folder.REQUIRED_FIELDS, null);
-            if (list != null && list.size > 0) {
-                debug("%s do_replay_appended_message: %d new messages in %s", to_string(),
-                    list.size, msg_set.to_string());
-                
-                // need to report both if it was created (not known before) and appended (which
-                // could mean created or simply a known email associated with this folder)
-                Gee.Map<Geary.Email, bool> created_or_merged =
-                    yield local_folder.create_or_merge_email_async(list, null);
-                foreach (Geary.Email email in created_or_merged.keys) {
-                    // true means created
-                    if (created_or_merged.get(email)) {
-                        debug("%s do_replay_appended_message: appended email ID %s added",
-                            to_string(), email.id.to_string());
-                        
-                        created.add(email.id);
-                    } else {
-                        debug("%s do_replay_appended_message: appended email ID %s associated",
-                            to_string(), email.id.to_string());
-                    }
-                    
-                    appended.add(email.id);
-                }
-            } else {
-                debug("%s do_replay_appended_message: no new messages in %s", to_string(),
-                    msg_set.to_string());
-            }
-        } catch (Error err) {
-            debug("%s do_replay_appended_message: Unable to process: %s",
-                to_string(), err.message);
-        }
-        
-        // store the reported count, *not* the current count (which is updated outside the of
-        // the queue) to ensure that updates happen serially and reflect committed local changes
-        try {
-            yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
-        } catch (Error err) {
-            debug("%s do_replay_appended_message: Unable to save appended remote count %d: %s",
-                to_string(), reported_remote_count, err.message);
-        }
-        
-        if (appended.size > 0)
-            notify_email_appended(appended);
-        
-        if (created.size > 0)
-            notify_email_locally_appended(created);
-        
-        notify_email_count_changed(reported_remote_count, CountChangeReason.APPENDED);
-        
-        debug("%s do_replay_appended_message: completed, current remote_count=%d reported_remote_count=%d",
-            to_string(), remote_count, reported_remote_count);
-    }
-    
-    private void on_remote_removed(Imap.SequenceNumber position, int reported_remote_count) {
-        debug("%s on_remote_removed: remote_count=%d position=%s reported_remote_count=%d", to_string(),
-            remote_count, position.to_string(), reported_remote_count);
-        
-        if (reported_remote_count < 0)
-            return;
-        
-        // notify of removal to all pending replay operations
-        replay_queue.notify_remote_removed_position(position);
-        
-        // update remote count NOW, as further appended and removed messages can arrive before
-        // ReplayRemoval executes
-        //
-        // something to note at this point: the ExpungeEmail operation marks messages as removed,
-        // then signals they're removed and reports an adjusted count in its replay_local_async().
-        // remote_count is *not* updated, which is why it's safe to do that here without worry.
-        // similarly, signals are only fired here if marked, so the same EmailIdentifier isn't
-        // reported twice
-        remote_count = reported_remote_count;
-        
-        replay_queue.schedule_server_notification(new ReplayRemoval(this, reported_remote_count, position));
-    }
-    
-    // This MUST only be called from ReplayRemoval.
-    internal async void do_replay_removed_message(int reported_remote_count, Imap.SequenceNumber 
remote_position) {
-        debug("%s do_replay_removed_message: current remote_count=%d remote_position=%d 
reported_remote_count=%d",
-            to_string(), remote_count, remote_position.value, reported_remote_count);
-        
-        if (!remote_position.is_valid()) {
-            debug("%s do_replay_removed_message: ignoring, invalid remote position or count",
-                to_string());
-            
-            return;
-        }
-        
-        int local_count = -1;
-        int local_position = -1;
-        
-        ImapDB.EmailIdentifier? owned_id = null;
-        try {
-            // need total count, including those marked for removal, to accurately calculate position
-            // from server's point of view, not client's
-            local_count = yield local_folder.get_email_count_async(
-                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
-            local_position = remote_position.value - (reported_remote_count + 1 - local_count);
-            
-            // zero or negative means the message exists beyond the local vector's range, so
-            // nothing to do there
-            if (local_position > 0) {
-                debug("%s do_replay_removed_message: local_count=%d local_position=%d", to_string(),
-                    local_count, local_position);
-                
-                owned_id = yield local_folder.get_id_at_async(local_position, null);
-            } else {
-                debug("%s do_replay_removed_message: message not stored locally (local_count=%d 
local_position=%d)",
-                    to_string(), local_count, local_position);
-            }
-        } catch (Error err) {
-            debug("%s do_replay_removed_message: unable to determine ID of removed message %s: %s",
-                to_string(), remote_position.to_string(), err.message);
-        }
-        
-        bool marked = false;
-        if (owned_id != null) {
-            debug("%s do_replay_removed_message: detaching from local store Email ID %s", to_string(),
-                owned_id.to_string());
-            try {
-                // Reflect change in the local store and notify subscribers
-                yield local_folder.detach_single_email_async(owned_id, out marked, null);
-            } catch (Error err) {
-                debug("%s do_replay_removed_message: unable to remove message #%s: %s", to_string(),
-                    remote_position.to_string(), err.message);
-            }
-            
-            // Notify queued replay operations that the email has been removed (by EmailIdentifier)
-            replay_queue.notify_remote_removed_ids(
-                Geary.iterate<ImapDB.EmailIdentifier>(owned_id).to_array_list());
-        } else {
-            debug("%s do_replay_removed_message: remote_position=%d unknown in local store "
-                + "(reported_remote_count=%d local_position=%d local_count=%d)",
-                to_string(), remote_position.value, reported_remote_count, local_position, local_count);
-        }
-        
-        // for debugging
-        int new_local_count = -1;
-        try {
-            new_local_count = yield local_folder.get_email_count_async(
-                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
-        } catch (Error err) {
-            debug("%s do_replay_removed_message: error fetching new local count: %s", to_string(),
-                err.message);
-        }
-        
-        // as with on_remote_appended(), only update in local store inside a queue operation, to
-        // ensure serial commits
-        try {
-            yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
-        } catch (Error err) {
-            debug("%s do_replay_removed_message: unable to save removed remote count: %s", to_string(),
-                err.message);
-        }
-        
-        // notify of change
-        if (!marked && owned_id != null)
-            notify_email_removed(Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list());
-        
-        if (!marked)
-            notify_email_count_changed(reported_remote_count, CountChangeReason.REMOVED);
-        
-        debug("%s do_replay_remove_message: completed, current remote_count=%d "
-            + "(reported_remote_count=%d local_count=%d starting local_count=%d remote_position=%d 
local_position=%d marked=%s)",
-            to_string(), remote_count, reported_remote_count, new_local_count, local_count, 
remote_position.value,
-            local_position, marked.to_string());
-    }
-    
-    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));
-    }
-    
-    //
-    // list_email_by_id variants
-    //
-    
-    public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
-        int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
-        Cancellable? cancellable = null) throws Error {
-        Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
-        yield do_list_email_by_id_async("list_email_by_id_async", initial_id, count, required_fields,
-            flags, accumulator, null, cancellable);
-        
-        return !accumulator.is_empty ? accumulator : null;
-    }
-    
-    public override void lazy_list_email_by_id(Geary.EmailIdentifier? initial_id, int count,
-        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
-        Cancellable? cancellable = null) {
-        do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
-    }
-    
-    private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier? initial_id, int count,
-        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable) {
-        try {
-            yield do_list_email_by_id_async("lazy_list_email_by_id", initial_id, count, required_fields,
-                flags, null, cb, cancellable);
-        } catch (Error err) {
-            cb(null, err);
-        }
-    }
-    
-    private async void do_list_email_by_id_async(string method, Geary.EmailIdentifier? initial_id,
-        int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
-        Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable) throws Error {
-        check_open(method);
-        check_flags(method, flags);
-        if (initial_id != null)
-            check_id(method, initial_id);
-        
-        if (count == 0) {
-            // signal finished
-            if (cb != null)
-                cb(null, null);
-            
-            return;
-        }
-        
-        // Schedule list operation and wait for completion.
-        ListEmailByID op = new ListEmailByID(this, (ImapDB.EmailIdentifier) initial_id, count,
-            required_fields, flags, accumulator, cb, cancellable);
-        replay_queue.schedule(op);
-        
-        yield op.wait_for_ready_async(cancellable);
-    }
-    
-    //
-    // list_email_by_sparse_id variants
-    //
-    
-    public async override Gee.List<Geary.Email>? list_email_by_sparse_id_async(
-        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
-        Cancellable? cancellable = null) throws Error {
-        Gee.ArrayList<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
-        yield do_list_email_by_sparse_id_async("list_email_by_sparse_id_async", ids, required_fields,
-            flags, accumulator, null, cancellable);
-        
-        return (accumulator.size > 0) ? accumulator : null;
-    }
-    
-    public override void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
-        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable = null) {
-        do_lazy_list_email_by_sparse_id_async.begin(ids, required_fields, flags, cb, cancellable);
-    }
-    
-    private async void do_lazy_list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
-        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable) {
-        try {
-            yield do_list_email_by_sparse_id_async("lazy_list_email_by_sparse_id", ids, required_fields,
-                flags, null, cb, cancellable);
-        } catch (Error err) {
-            cb(null, err);
-        }
-    }
-    
-    private async void do_list_email_by_sparse_id_async(string method,
-        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
-        Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error 
{
-        check_open(method);
-        check_flags(method, flags);
-        check_ids(method, ids);
-        
-        if (ids.size == 0) {
-            // signal finished
-            if (cb != null)
-                cb(null, null);
-            
-            return;
-        }
-        
-        // Schedule list operation and wait for completion.
-        // TODO: Break up requests to avoid hogging the queue
-        ListEmailBySparseID op = new ListEmailBySparseID(this, (Gee.Collection<ImapDB.EmailIdentifier>) ids,
-            required_fields, flags, accumulator, cb, cancellable);
-        replay_queue.schedule(op);
-        
-        yield op.wait_for_ready_async(cancellable);
-    }
-    
-    public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
-        check_open("list_local_email_fields_async");
-        check_ids("list_local_email_fields_async", ids);
-        
-        return yield local_folder.list_email_fields_by_id_async(
-            (Gee.Collection<Geary.ImapDB.EmailIdentifier>) ids, ImapDB.Folder.ListFlags.NONE, cancellable);
-    }
-    
-    public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
-        Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
-        throws Error {
-        check_open("fetch_email_async");
-        check_flags("fetch_email_async", flags);
-        check_id("fetch_email_async", id);
-        
-        FetchEmail op = new FetchEmail(this, (ImapDB.EmailIdentifier) id, required_fields, flags,
-            cancellable);
-        replay_queue.schedule(op);
-        
-        yield op.wait_for_ready_async(cancellable);
-        
-        if (op.email == null) {
-            throw new EngineError.NOT_FOUND("Email %s not found in %s", id.to_string(), to_string());
-        } else if (!op.email.fields.fulfills(required_fields)) {
-            throw new EngineError.INCOMPLETE_MESSAGE("Email %s in %s does not fulfill required fields %Xh 
(has %Xh)",
-                id.to_string(), to_string(), required_fields, op.email.fields);
-        }
-        
-        return op.email;
-    }
-    
-    // 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 {
-        check_open("expunge_email_async");
-        check_ids("expunge_email_async", email_ids);
-        
-        RemoveEmail remove = new RemoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) email_ids,
-            cancellable);
-        replay_queue.schedule(remove);
-        
-        yield remove.wait_for_ready_async(cancellable);
-    }
-    
-    private void check_open(string method) throws EngineError {
-        if (open_count == 0)
-            throw new EngineError.OPEN_REQUIRED("%s failed: folder %s is not open", method, to_string());
-    }
-    
-    private void check_flags(string method, Folder.ListFlags flags) throws EngineError {
-        if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY) && 
flags.is_all_set(Folder.ListFlags.FORCE_UPDATE)) {
-            throw new EngineError.BAD_PARAMETERS("%s %s failed: LOCAL_ONLY and FORCE_UPDATE are mutually 
exclusive",
-                to_string(), method);
-        }
-    }
-    
-    private void check_id(string method, EmailIdentifier id) throws EngineError {
-        if (!(id is ImapDB.EmailIdentifier))
-            throw new EngineError.BAD_PARAMETERS("Email ID %s is not IMAP Email ID", id.to_string());
-    }
-    
-    private void check_ids(string method, Gee.Collection<EmailIdentifier> ids) throws EngineError {
-        foreach (EmailIdentifier id in ids)
-            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, 
-        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);
-    }
-
-    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);
-        
-        CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
-        replay_queue.schedule(copy);
-        yield copy.wait_for_ready_async(cancellable);
-    }
-
-    public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
-        Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
-        check_open("move_email_async");
-        check_ids("move_email_async", to_move);
-        
-        MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
-        replay_queue.schedule(move);
-        yield move.wait_for_ready_async(cancellable);
-    }
-    
-    private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
-        notify_email_flags_changed(changed);
-    }
-    
-    // TODO: A proper public search mechanism; note that this always round-trips to the remote,
-    // doesn't go through the replay queue, and doesn't deal with messages marked for deletion
-    internal async Geary.EmailIdentifier? find_earliest_email_async(DateTime datetime,
-        Geary.EmailIdentifier? before_id, Cancellable? cancellable) throws Error {
-        check_open("find_earliest_email_async");
-        if (before_id != null)
-            check_id("find_earliest_email_async", before_id);
-        
-        Imap.SearchCriteria criteria = new Imap.SearchCriteria();
-        criteria.is_(Imap.SearchCriterion.since_internaldate(new 
Imap.InternalDate.from_date_time(datetime)));
-        
-        // if before_id available, only search for messages before it
-        if (before_id != null) {
-            Imap.UID? before_uid = yield local_folder.get_uid_async((ImapDB.EmailIdentifier) before_id,
-                ImapDB.Folder.ListFlags.NONE, cancellable);
-            if (before_uid == null) {
-                throw new EngineError.NOT_FOUND("before_id %s not found in %s", before_id.to_string(),
-                    to_string());
-            }
-            
-            criteria.and(Imap.SearchCriterion.message_set(
-                new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous(true))));
-        }
-        
-        Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
-        ServerSearchEmail op = new ServerSearchEmail(this, criteria, Geary.Email.Field.NONE,
-            accumulator, cancellable);
-        
-        // need to check again due to the yield in the above conditional block
-        check_open("find_earliest_email_async.schedule operation");
-        
-        replay_queue.schedule(op);
-        
-        if (!yield op.wait_for_ready_async(cancellable))
-            return null;
-        
-        // find earliest ID; because all Email comes from Folder, UID should always be present
-        ImapDB.EmailIdentifier? earliest_id = null;
-        foreach (Geary.Email email in accumulator) {
-            ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
-            
-            if (earliest_id == null || email_id.uid.compare_to(earliest_id.uid) < 0)
-                earliest_id = email_id;
-        }
-        
-        return earliest_id;
+        yield expunge_email_async(email_ids, cancellable);
     }
     
-    internal 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);
-        
-        Error? cancel_error = null;
-        Geary.EmailIdentifier? ret = null;
-        try {
-            CreateEmail create = new CreateEmail(this, rfc822, flags, date_received, cancellable);
-            replay_queue.schedule(create);
-            yield create.wait_for_ready_async(cancellable);
-            
-            ret = create.created_id;
-        } catch (Error e) {
-            if (e is IOError.CANCELLED)
-                cancel_error = e;
-            else
-                throw e;
-        }
-        
-        Geary.FolderSupport.Remove? remove_folder = this as Geary.FolderSupport.Remove;
-        
-        // Remove old message.
-        if (id != null && remove_folder != null)
-            yield remove_folder.remove_single_email_async(id, null);
-        
-        // If the user cancelled the operation, throw the error here.
-        if (cancel_error != null)
-            throw cancel_error;
-        
-        // 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);
-        
-        return ret;
+    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
new file mode 100644
index 0000000..f1e6297
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -0,0 +1,1295 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.FolderSupport.Copy,
+    Geary.FolderSupport.Mark, Geary.FolderSupport.Move {
+    private const int FORCE_OPEN_REMOTE_TIMEOUT_SEC = 10;
+    private const int DEFAULT_REESTABLISH_DELAY_MSEC = 10;
+    private const int MAX_REESTABLISH_DELAY_MSEC = 1000;
+    
+    public override Account account { get { return _account; } }
+    
+    public override FolderProperties properties { get { return _properties; } }
+    
+    public override FolderPath path {
+        get {
+            return local_folder.get_path();
+        }
+    }
+    
+    private SpecialFolderType _special_folder_type;
+    public override SpecialFolderType special_folder_type {
+        get {
+            return _special_folder_type;
+        }
+    }
+    
+    internal ImapDB.Folder local_folder  { get; protected set; }
+    internal Imap.Folder? remote_folder { get; protected set; default = null; }
+    internal EmailPrefetcher email_prefetcher { get; private set; }
+    internal EmailFlagWatcher email_flag_watcher;
+    
+    private weak GenericAccount _account;
+    private Geary.AggregatedFolderProperties _properties = new Geary.AggregatedFolderProperties(
+        false, false);
+    private Imap.Account remote;
+    private ImapDB.Account local;
+    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 int remote_count = -1;
+    private uint open_remote_timer_id = 0;
+    private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+    
+    public MinimalFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
+        ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
+        _account = account;
+        this.remote = remote;
+        this.local = local;
+        this.local_folder = local_folder;
+        _special_folder_type = special_folder_type;
+        _properties.add(local_folder.get_properties());
+        
+        email_flag_watcher = new EmailFlagWatcher(this);
+        email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
+        
+        email_prefetcher = new EmailPrefetcher(this);
+        
+        local_folder.email_complete.connect(on_email_complete);
+    }
+    
+    ~EngineFolder() {
+        if (open_count > 0)
+            warning("Folder %s destroyed without closing", to_string());
+        
+        local_folder.email_complete.disconnect(on_email_complete);
+    }
+    
+    public void set_special_folder_type(SpecialFolderType new_type) {
+        SpecialFolderType old_type = _special_folder_type;
+        _special_folder_type = new_type;
+        if(old_type != new_type)
+            notify_special_folder_type_changed(old_type, new_type);
+    }
+    
+    public override Geary.Folder.OpenState get_open_state() {
+        if (open_count == 0)
+            return Geary.Folder.OpenState.CLOSED;
+        
+        return (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL;
+    }
+    
+    // Returns the synchronized remote count (-1 if not opened) and the last seen remote count (stored
+    // locally, -1 if not available)
+    //
+    // Return value is the remote_count, unless the remote is unopened, in which case it's the
+    // last_seen_remote_count (which may be -1).
+    //
+    // remote_count, last_seen_remote_count, and returned value do not reflect any notion of
+    // messages marked for removal
+    internal int get_remote_counts(out int remote_count, out int last_seen_remote_count) {
+        remote_count = this.remote_count;
+        last_seen_remote_count = local_folder.get_properties().select_examine_messages;
+        if (last_seen_remote_count < 0)
+            last_seen_remote_count = local_folder.get_properties().status_messages;
+        
+        return (remote_count >= 0) ? remote_count : last_seen_remote_count;
+    }
+    
+    private async bool normalize_folders(Geary.Imap.Folder remote_folder, Geary.Folder.OpenFlags open_flags,
+        Cancellable? cancellable) throws Error {
+        debug("%s: Begin normalizing remote and local folders", to_string());
+        
+        Geary.Imap.FolderProperties local_properties = local_folder.get_properties();
+        Geary.Imap.FolderProperties remote_properties = remote_folder.properties;
+        
+        // and both must have their next UID's (it's possible they don't if it's a non-selectable
+        // folder)
+        if (local_properties.uid_next == null || local_properties.uid_validity == null) {
+            debug("%s: Unable to verify UIDs: missing local UIDNEXT (%s) and/or UIDVALIDITY (%s)",
+                to_string(), (local_properties.uid_next == null).to_string(),
+                (local_properties.uid_validity == null).to_string());
+            
+            return false;
+        }
+        
+        if (remote_properties.uid_next == null || remote_properties.uid_validity == null) {
+            debug("%s: Unable to verify UIDs: missing remote UIDNEXT (%s) and/or UIDVALIDITY (%s)",
+                to_string(), (remote_properties.uid_next == null).to_string(),
+                (remote_properties.uid_validity == null).to_string());
+            
+            return false;
+        }
+        
+        // If UIDVALIDITY changes, all email in the folder must be removed as the UIDs are now
+        // invalid ... we merely detach the emails (leaving their contents behind) so duplicate
+        // detection can fix them up.  But once all UIDs are removed, it's much like the next
+        // if case where no earliest UID available, so simply exit.
+        //
+        // see http://tools.ietf.org/html/rfc3501#section-2.3.1.1
+        if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
+            debug("%s: UID validity changed, detaching all email: %s -> %s", to_string(),
+                local_properties.uid_validity.value.to_string(),
+                remote_properties.uid_validity.value.to_string());
+            
+            yield local_folder.detach_all_emails_async(cancellable);
+            
+            return true;
+        }
+        
+        // fetch email from earliest email to last to (a) remove any deletions and (b) update
+        // any flags that may have changed
+        ImapDB.EmailIdentifier? local_earliest_id = yield local_folder.get_earliest_id_async(cancellable);
+        ImapDB.EmailIdentifier? local_latest_id = yield local_folder.get_latest_id_async(cancellable);
+        
+        // verify still open; this is required throughout after each yield, as a close_async() can
+        // come in ay any time since this does not run in the context of open_async()
+        check_open("normalize_folders (local earliest/latest UID)");
+        
+        // if no earliest UID, that means no messages in local store, so nothing to update
+        if (local_earliest_id == null || local_latest_id == null) {
+            debug("%s: local store empty, nothing to normalize", to_string());
+            
+            return true;
+        }
+        
+        assert(local_earliest_id.has_uid());
+        assert(local_latest_id.has_uid());
+        
+        // if any messages are still marked for removal from last time, that means the EXPUNGE
+        // never arrived from the server, in which case the folder is "dirty" and needs a full
+        // normalization
+        Gee.Set<ImapDB.EmailIdentifier>? already_marked_ids = yield local_folder.get_marked_ids_async(
+            cancellable);
+        
+        // however, there may be enqueue ReplayOperations waiting to remove messages on the server
+        // that marked some or all of those messages
+        Gee.HashSet<ImapDB.EmailIdentifier> to_be_removed = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        replay_queue.get_ids_to_be_remote_removed(to_be_removed);
+        
+        // don't consider those already marked as "already marked" if they were not leftover from
+        // the last open of this folder
+        if (already_marked_ids != null)
+            already_marked_ids.remove_all(to_be_removed);
+        
+        bool is_dirty = (already_marked_ids != null && already_marked_ids.size > 0);
+        
+        if (is_dirty)
+            debug("%s: %d remove markers found, folder is dirty", to_string(), already_marked_ids.size);
+        
+        // if UIDNEXT has changed, that indicates messages have been appended (and possibly removed)
+        int64 uidnext_diff = remote_properties.uid_next.value - local_properties.uid_next.value;
+        
+        int local_message_count = (local_properties.select_examine_messages >= 0)
+            ? local_properties.select_examine_messages : 0;
+        int remote_message_count = (remote_properties.select_examine_messages >= 0)
+            ? remote_properties.select_examine_messages : 0;
+        
+        // if UIDNEXT is the same as last time AND the total count of email is the same, then
+        // nothing has been added or removed
+        if (!is_dirty && uidnext_diff == 0 && local_message_count == remote_message_count) {
+            debug("%s: No messages added/removed since last opened, normalization completed", to_string());
+            
+            return true;
+        }
+        
+        // a full normalize works from the highest possible UID on the remote and work down to the lowest 
UID on
+        // the local; this covers all messages appended since last seen as well as any removed
+        Imap.UID last_uid = remote_properties.uid_next.previous(true);
+        
+        // if the difference in UIDNEXT values equals the difference in message count, then only
+        // an append could have happened, so only pull in the new messages ... note that this is not 
foolproof,
+        // as UIDs are not guaranteed to increase by 1; however, this is a standard implementation practice,
+        // so it's worth looking for
+        //
+        // (Also, this cannot fail; if this situation exists, then it cannot by definition indicate another
+        // situation, esp. messages being removed.)
+        Imap.UID first_uid;
+        if (!is_dirty && uidnext_diff == (remote_message_count - local_message_count)) {
+            first_uid = local_latest_id.uid.next(true);
+            
+            debug("%s: Messages only appended (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering 
mail UIDs %s:%s",
+                to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
+                local_properties.select_examine_messages, remote_properties.select_examine_messages, 
uidnext_diff.to_string(),
+                first_uid.to_string(), last_uid.to_string());
+        } else {
+            first_uid = local_earliest_id.uid;
+            
+            debug("%s: Messages appended/removed (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering 
mail UIDs %s:%s",
+                to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
+                local_properties.select_examine_messages, remote_properties.select_examine_messages, 
uidnext_diff.to_string(),
+                first_uid.to_string(), last_uid.to_string());
+        }
+        
+        // get all the UIDs in said range from the local store, sorted; convert to non-null
+        // for ease of use later
+        Gee.Set<Imap.UID>? local_uids = yield local_folder.list_uids_by_range_async(
+            first_uid, last_uid, true, cancellable);
+        if (local_uids == null)
+            local_uids = new Gee.HashSet<Imap.UID>();
+        
+        check_open("normalize_folders (list local)");
+        
+        // Do the same on the remote ... make non-null for ease of use later
+        Gee.Set<Imap.UID>? remote_uids = yield remote_folder.list_uids_async(
+            new Imap.MessageSet.uid_range(first_uid, last_uid), cancellable);
+        if (remote_uids == null)
+            remote_uids = new Gee.HashSet<Imap.UID>();
+        
+        check_open("normalize_folders (list remote)");
+        
+        debug("%s: Loaded local (%d) and remote (%d) UIDs, normalizing...", to_string(),
+            local_uids.size, remote_uids.size);
+        
+        Gee.HashSet<Imap.UID> removed_uids = new Gee.HashSet<Imap.UID>();
+        Gee.HashSet<Imap.UID> appended_uids = new Gee.HashSet<Imap.UID>();
+        Gee.HashSet<Imap.UID> inserted_uids = new Gee.HashSet<Imap.UID>();
+        
+        // Because the number of UIDs being processed can be immense in large folders, process
+        // in a background thread
+        yield Nonblocking.Concurrent.global.schedule_async(() => {
+            // walk local UIDs looking for UIDs no longer on remote, removing those that are available
+            // make the next pass that much shorter
+            foreach (Imap.UID local_uid in local_uids) {
+                // if in local but not remote, consider removed from remote
+                if (!remote_uids.remove(local_uid))
+                    removed_uids.add(local_uid);
+            }
+            
+            // everything remaining in remote has been added since folder last seen ... whether they're
+            // discovered (inserted) or appended depends on the highest local UID
+            foreach (Imap.UID remote_uid in remote_uids) {
+                if (remote_uid.compare_to(local_latest_id.uid) > 0)
+                    appended_uids.add(remote_uid);
+                else
+                    inserted_uids.add(remote_uid);
+            }
+            
+            // the UIDs marked for removal are going to be re-inserted into the vector once they're
+            // cleared, so add them here as well
+            if (already_marked_ids != null) {
+                foreach (ImapDB.EmailIdentifier id in already_marked_ids) {
+                    assert(id.has_uid());
+                    
+                    if (!appended_uids.contains(id.uid))
+                        inserted_uids.add(id.uid);
+                }
+            }
+        }, cancellable);
+        
+        debug("%s: changes since last seen: removed=%d appended=%d inserted=%d", to_string(),
+            removed_uids.size, appended_uids.size, inserted_uids.size);
+        
+        // fetch from the server the local store's required flags for all appended/inserted messages
+        // (which is simply equal to all remaining remote UIDs)
+        Gee.List<Geary.Email>? to_create = null;
+        if (remote_uids.size > 0) {
+            // for new messages, get the local store's required fields (which provide duplicate
+            // detection)
+            to_create = yield remote_folder.list_email_async(
+                new Imap.MessageSet.uid_sparse(remote_uids.to_array()), ImapDB.Folder.REQUIRED_FIELDS,
+                cancellable);
+        }
+        
+        check_open("normalize_folders (list remote appended/inserted required fields)");
+        
+        // store new messages and add IDs to the appended/discovered EmailIdentifier buckets
+        Gee.Set<ImapDB.EmailIdentifier> appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        Gee.Set<ImapDB.EmailIdentifier> locally_appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        Gee.Set<ImapDB.EmailIdentifier> inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        Gee.Set<ImapDB.EmailIdentifier> locally_inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+        if (to_create != null && to_create.size > 0) {
+            Gee.Map<Email, bool>? created_or_merged = yield local_folder.create_or_merge_email_async(
+                to_create, cancellable);
+            assert(created_or_merged != null);
+            
+            // it's possible a large number of messages have come in, so process them in the
+            // background
+            yield Nonblocking.Concurrent.global.schedule_async(() => {
+                foreach (Email email in created_or_merged.keys) {
+                    ImapDB.EmailIdentifier id = (ImapDB.EmailIdentifier) email.id;
+                    bool created = created_or_merged.get(email);
+                    
+                    // report all appended email, but separate out email never seen before (created)
+                    // as locally-appended
+                    if (appended_uids.contains(id.uid)) {
+                        appended_ids.add(id);
+                        
+                        if (created)
+                            locally_appended_ids.add(id);
+                    } else if (inserted_uids.contains(id.uid)) {
+                        inserted_ids.add(id);
+                        
+                        if (created)
+                            locally_inserted_ids.add(id);
+                    }
+                }
+            }, cancellable);
+            
+            debug("%s: Finished creating/merging %d emails", to_string(), created_or_merged.size);
+        }
+        
+        check_open("normalize_folders (created/merged appended/inserted emails)");
+        
+        // Convert removed UIDs into EmailIdentifiers and detach immediately
+        Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
+        if (removed_uids.size > 0) {
+            removed_ids = yield local_folder.get_ids_async(removed_uids,
+                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
+            if (removed_ids != null && removed_ids.size > 0) {
+                yield local_folder.detach_multiple_emails_async(removed_ids, cancellable);
+            }
+        }
+        
+        check_open("normalize_folders (removed emails)");
+        
+        // remove any extant remove markers, as everything is accounted for now, except for those
+        // waiting to be removed in the queue
+        yield local_folder.clear_remove_markers_async(to_be_removed, cancellable);
+        
+        check_open("normalize_folders (clear remove markers)");
+        
+        //
+        // now normalized
+        // notify subscribers of changes
+        //
+        
+        Folder.CountChangeReason count_change_reason = Folder.CountChangeReason.NONE;
+        
+        if (removed_ids != null && removed_ids.size > 0) {
+            // there may be operations pending on the remote queue for these removed emails; notify
+            // operations that the email has shuffled off this mortal coil
+            replay_queue.notify_remote_removed_ids(removed_ids);
+            
+            // notify subscribers about emails that have been removed
+            debug("%s: Notifying of %d removed emails since last opened", to_string(), removed_ids.size);
+            notify_email_removed(removed_ids);
+            
+            count_change_reason |= Folder.CountChangeReason.REMOVED;
+        }
+        
+        // notify inserted (new email located somewhere inside the local vector)
+        if (inserted_ids.size > 0) {
+            debug("%s: Notifying of %d inserted emails since last opened", to_string(), inserted_ids.size);
+            notify_email_inserted(inserted_ids);
+            
+            count_change_reason |= Folder.CountChangeReason.INSERTED;
+        }
+        
+        // notify inserted (new email located somewhere inside the local vector that had to be
+        // created, i.e. no portion was stored locally)
+        if (locally_inserted_ids.size > 0) {
+            debug("%s: Notifying of %d locally inserted emails since last opened", to_string(),
+                locally_inserted_ids.size);
+            notify_email_locally_inserted(locally_inserted_ids);
+            
+            count_change_reason |= Folder.CountChangeReason.INSERTED;
+        }
+        
+        // notify appended (new email added since the folder was last opened)
+        if (appended_ids.size > 0) {
+            debug("%s: Notifying of %d appended emails since last opened", to_string(), appended_ids.size);
+            notify_email_appended(appended_ids);
+            
+            count_change_reason |= Folder.CountChangeReason.APPENDED;
+        }
+        
+        // notify locally appended (new email never seen before added since the folder was last
+        // opened)
+        if (locally_appended_ids.size > 0) {
+            debug("%s: Notifying of %d locally appended emails since last opened", to_string(),
+                locally_appended_ids.size);
+            notify_email_locally_appended(locally_appended_ids);
+            
+            count_change_reason |= Folder.CountChangeReason.APPENDED;
+        }
+        
+        if (count_change_reason != Folder.CountChangeReason.NONE) {
+            debug("%s: Notifying of %Xh count change reason (%d remote messages)", to_string(),
+                count_change_reason, remote_message_count);
+            notify_email_count_changed(remote_message_count, count_change_reason);
+        }
+        
+        debug("%s: Completed normalize_folder", to_string());
+        
+        return true;
+    }
+    
+    public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
+        if (open_count == 0 || remote_semaphore == null)
+            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
+        // an open_async, it's reset at close time
+        if (!remote_opened) {
+            debug("wait_for_open_async %s: opening remote on demand...", to_string());
+            
+            remote_opened = true;
+            open_remote_async.begin(open_flags, null);
+        }
+        
+        if (!yield remote_semaphore.wait_for_result_async(cancellable))
+            throw new EngineError.ALREADY_CLOSED("%s failed to open", to_string());
+    }
+    
+    public override async bool open_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable = null)
+        throws Error {
+        if (open_count++ > 0) {
+            // even if opened or opening, respect the NO_DELAY flag
+            if (open_flags.is_all_set(OpenFlags.NO_DELAY)) {
+                cancel_remote_open_timer();
+                wait_for_open_async.begin();
+            }
+            
+            debug("Not opening %s: already open (open_count=%d)", to_string(), open_count);
+            
+            return false;
+        }
+        
+        this.open_flags = open_flags;
+        
+        open_internal(open_flags, cancellable);
+        
+        return true;
+    }
+    
+    private void open_internal(Folder.OpenFlags open_flags, Cancellable? cancellable) {
+        remote_semaphore = new Geary.Nonblocking.ReportingSemaphore<bool>(false);
+        
+        // start the replay queue
+        replay_queue = new ReplayQueue(this);
+        
+        // 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
+        // fast local-only operations to occur, local-only either because (a) the folder has all
+        // the information required (for a list or fetch operation), or (b) the operation was de
+        // facto local-only.  In particular, EmailStore will open and close lots of folders,
+        // causing a lot of connection setup and teardown
+        //
+        // However, want to eventually open, otherwise if there's no user interaction (i.e. a
+        // second account Inbox they don't manipulate), no remote connection will ever be made,
+        // meaning that folder normalization never happens and unsolicited notifications never
+        // arrive
+        if (open_flags.is_all_set(OpenFlags.NO_DELAY))
+            wait_for_open_async.begin();
+        else
+            start_remote_open_timer();
+    }
+    
+    private void start_remote_open_timer() {
+        if (open_remote_timer_id != 0)
+            Source.remove(open_remote_timer_id);
+        
+        open_remote_timer_id = Timeout.add_seconds(FORCE_OPEN_REMOTE_TIMEOUT_SEC, on_open_remote_timeout);
+    }
+    
+    private void cancel_remote_open_timer() {
+        if (open_remote_timer_id == 0)
+            return;
+        
+        Source.remove(open_remote_timer_id);
+        open_remote_timer_id = 0;
+    }
+    
+    private bool on_open_remote_timeout() {
+        open_remote_timer_id = 0;
+        
+        // remote was not forced open due to caller, so open now
+        wait_for_open_async.begin();
+        
+        return false;
+    }
+    
+    private async void open_remote_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable) {
+        cancel_remote_open_timer();
+        
+        // watch for folder closing before this call got a chance to execute
+        if (open_count == 0)
+            return;
+        
+        try {
+            debug("Fetching information for remote folder %s", to_string());
+            Imap.Folder folder = yield remote.fetch_folder_async(local_folder.get_path(),
+                cancellable);
+            
+            debug("Opening remote folder %s", folder.to_string());
+            yield folder.open_async(cancellable);
+            
+            // allow subclasses to examine the opened folder and resolve any vital
+            // inconsistencies
+            if (yield normalize_folders(folder, open_flags, cancellable)) {
+                // update flags, properties, etc.
+                yield local.update_folder_select_examine_async(folder, cancellable);
+                
+                // signals
+                folder.appended.connect(on_remote_appended);
+                folder.removed.connect(on_remote_removed);
+                folder.disconnected.connect(on_remote_disconnected);
+            
+                // state
+                remote_count = folder.properties.email_total;
+                
+                // all set; bless the remote folder as opened
+                remote_folder = folder;
+            } else {
+                debug("Unable to prepare remote folder %s: normalize_folders() failed", to_string());
+                notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
+                
+                // schedule immediate close
+                close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
+                    cancellable);
+                
+                return;
+            }
+        } catch (Error open_err) {
+            debug("Unable to open or prepare remote folder %s: %s", to_string(), open_err.message);
+            notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, open_err);
+            
+            // schedule immediate close and force reestablishment
+            close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, true,
+                cancellable);
+            
+            return;
+        }
+        
+        // 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", count_err.message);
+            
+            count = 0;
+        }
+        
+        // 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);
+        } catch (Error notify_err) {
+            debug("Unable to fire semaphore notifying remote folder ready/not ready: %s",
+                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);
+    }
+    
+    public override async void close_async(Cancellable? cancellable = null) throws Error {
+        if (open_count == 0 || --open_count > 0)
+            return;
+        
+        if (remote_folder != null)
+            _properties.remove(remote_folder.properties);
+        
+        yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
+            cancellable);
+    }
+    
+    // NOTE: This bypasses open_count and forces the Folder closed.
+    internal async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason 
remote_reason,
+        bool force_reestablish, Cancellable? cancellable) {
+        cancel_remote_open_timer();
+        
+        // only flushing pending ReplayOperations if this is a "clean" close, not forced due to
+        // error
+        bool flush_pending = !remote_reason.is_error();
+        
+        // If closing due to error, notify all operations waiting for the remote that it's not
+        // coming available ... this wakes up any ReplayOperation blocking on wait_for_open_async(),
+        // necessary in order to finish ReplayQueue.close_async (i.e. to prevent deadlock); this
+        // is necessary because it's possible for this method to be called before the remote_folder
+        // has even had a chance to open.
+        //
+        // Note that we don't want to do this for a clean close, because we want to flush out
+        // pending operations first
+        Imap.Folder? closing_remote_folder = null;
+        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());
+            }
+        } catch (Error replay_queue_err) {
+            debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
+        }
+        
+        replay_queue = null;
+        
+        // if a "clean" close, now go ahead and close the folder
+        if (flush_pending)
+            closing_remote_folder = clear_remote_folder();
+        
+        if (closing_remote_folder != null || force_reestablish) {
+            // to avoid keeping the caller waiting while the remote end closes (i.e. drops the
+            // connection or performs an IMAP CLOSE operation), close it in the background and
+            // reestablish connection there, if necessary
+            //
+            // TODO: Problem with this is that we cannot effectively signal or report a close error,
+            // because by the time this operation completes the folder is considered closed.  That
+            // may not be important to most callers, however.
+            //
+            // It also means the reference to the Folder must be maintained until completely
+            // closed.  Also not a problem, as GenericAccount does that internally.  However, this
+            // might be an issue if GenericAccount removes this folder due to a user command or
+            // detection on the server, so this background op keeps a reference to the Folder
+            close_remote_folder_async.begin(this, closing_remote_folder, remote_reason,
+                force_reestablish);
+        }
+        
+        remote_opened = false;
+        
+        // if remote reason is an error, then close_remote_folder_async() will be performing
+        // reestablishment, so go no further
+        if ((remote_reason.is_error() && closing_remote_folder != null) || force_reestablish)
+            return;
+        
+        // forced closed one way or another, so reset state
+        open_count = 0;
+        reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+        
+        // use remote_reason even if remote_folder was null; it could be that the error occurred
+        // while opening and remote_folder was yet unassigned ... also, need to call this every
+        // time, even if remote was not fully opened, as some callers rely on order of signals
+        notify_closed(remote_reason);
+        
+        // see above note for why this must be called every time
+        notify_closed(local_reason);
+        
+        notify_closed(CloseReason.FOLDER_CLOSED);
+        
+        debug("Folder %s closed", to_string());
+    }
+    
+    // Returns the remote_folder, if it was set
+    private Imap.Folder? clear_remote_folder() {
+        if (remote_folder != null) {
+            // disconnect signals before ripping out reference
+            remote_folder.appended.disconnect(on_remote_appended);
+            remote_folder.removed.disconnect(on_remote_removed);
+            remote_folder.disconnected.disconnect(on_remote_disconnected);
+        }
+        
+        Imap.Folder? old_remote_folder = remote_folder;
+        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);
+        }
+        
+        return old_remote_folder;
+    }
+    
+    // See note in close_async() for why this method is static and uses an owned ref
+    private static async void close_remote_folder_async(owned MinimalFolder folder,
+        owned Imap.Folder remote_folder, Folder.CloseReason remote_reason, bool force_reestablish) {
+        // force the remote closed; if due to a remote disconnect and plan on reopening, *still*
+        // need to do this
+        try {
+            if (remote_folder != null)
+                yield remote_folder.close_async(null);
+        } catch (Error err) {
+            debug("Unable to close remote %s: %s", remote_folder.to_string(), err.message);
+            
+            // fallthrough
+        }
+        
+        // reestablish connection (which requires renormalizing the remote with the local) if
+        // close was in error
+        if (remote_reason.is_error() || force_reestablish) {
+            debug("Reestablishing broken connection to %s in %dms", folder.to_string(),
+                folder.reestablish_delay_msec);
+            Timeout.add(folder.reestablish_delay_msec, () => {
+                folder.open_internal(OpenFlags.NO_DELAY, null);
+                
+                return false;
+            });
+            
+            folder.reestablish_delay_msec = (folder.reestablish_delay_msec * 2).clamp(
+                DEFAULT_REESTABLISH_DELAY_MSEC, MAX_REESTABLISH_DELAY_MSEC);
+        }
+    }
+    
+    public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
+        out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+        Cancellable? cancellable = null) throws Error {
+        low = null;
+        high = null;
+        
+        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? map
+            = yield account.get_containing_folders_async(ids, cancellable);
+        
+        if (map != null) {
+            Gee.ArrayList<Geary.EmailIdentifier> in_folder = new Gee.ArrayList<Geary.EmailIdentifier>();
+            foreach (Geary.EmailIdentifier id in map.get_keys()) {
+                if (path in map.get(id))
+                    in_folder.add(id);
+            }
+            
+            if (in_folder.size > 0) {
+                Gee.SortedSet<Geary.EmailIdentifier> sorted = Geary.EmailIdentifier.sort(in_folder);
+                
+                low = sorted.first();
+                high = sorted.last();
+            }
+        }
+    }
+    
+    private void on_email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids) {
+        notify_email_locally_complete(email_ids);
+    }
+    
+    private void on_remote_appended(int reported_remote_count) {
+        debug("%s on_remote_appended: remote_count=%d reported_remote_count=%d", to_string(), remote_count,
+            reported_remote_count);
+        
+        if (reported_remote_count < 0)
+            return;
+        
+        // from the new remote total and the old remote total, glean the SequenceNumbers of the
+        // new email(s)
+        Gee.List<Imap.SequenceNumber> positions = new Gee.ArrayList<Imap.SequenceNumber>();
+        for (int pos = remote_count + 1; pos <= reported_remote_count; pos++)
+            positions.add(new Imap.SequenceNumber(pos));
+        
+        // store the remote count NOW, as further appended messages could arrive before the
+        // ReplayAppend executes
+        remote_count = reported_remote_count;
+        
+        if (positions.size > 0)
+            replay_queue.schedule_server_notification(new ReplayAppend(this, reported_remote_count, 
positions));
+    }
+    
+    // Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
+    // normalized placeholder in the local database of the message, so all positions are
+    // properly relative to the end of the message list; once this is done, notify user of new
+    // messages.  If duplicates, create_email_async() will fall through to an updated merge,
+    // which is exactly what we want.
+    //
+    // This MUST only be called from ReplayAppend.
+    internal async void do_replay_appended_messages(int reported_remote_count,
+        Gee.List<Imap.SequenceNumber> remote_positions) {
+        StringBuilder positions_builder = new StringBuilder("( ");
+        foreach (Imap.SequenceNumber remote_position in remote_positions)
+            positions_builder.append_printf("%s ", remote_position.to_string());
+        positions_builder.append(")");
+        
+        debug("%s do_replay_appended_message: current remote_count=%d reported_remote_count=%d 
remote_positions=%s",
+            to_string(), remote_count, reported_remote_count, positions_builder.str);
+        
+        if (remote_positions.size == 0)
+            return;
+        
+        Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>();
+        Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>();
+        try {
+            Imap.MessageSet msg_set = new Imap.MessageSet.sparse(remote_positions.to_array());
+            Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(msg_set,
+                ImapDB.Folder.REQUIRED_FIELDS, null);
+            if (list != null && list.size > 0) {
+                debug("%s do_replay_appended_message: %d new messages in %s", to_string(),
+                    list.size, msg_set.to_string());
+                
+                // need to report both if it was created (not known before) and appended (which
+                // could mean created or simply a known email associated with this folder)
+                Gee.Map<Geary.Email, bool> created_or_merged =
+                    yield local_folder.create_or_merge_email_async(list, null);
+                foreach (Geary.Email email in created_or_merged.keys) {
+                    // true means created
+                    if (created_or_merged.get(email)) {
+                        debug("%s do_replay_appended_message: appended email ID %s added",
+                            to_string(), email.id.to_string());
+                        
+                        created.add(email.id);
+                    } else {
+                        debug("%s do_replay_appended_message: appended email ID %s associated",
+                            to_string(), email.id.to_string());
+                    }
+                    
+                    appended.add(email.id);
+                }
+            } else {
+                debug("%s do_replay_appended_message: no new messages in %s", to_string(),
+                    msg_set.to_string());
+            }
+        } catch (Error err) {
+            debug("%s do_replay_appended_message: Unable to process: %s",
+                to_string(), err.message);
+        }
+        
+        // store the reported count, *not* the current count (which is updated outside the of
+        // the queue) to ensure that updates happen serially and reflect committed local changes
+        try {
+            yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
+        } catch (Error err) {
+            debug("%s do_replay_appended_message: Unable to save appended remote count %d: %s",
+                to_string(), reported_remote_count, err.message);
+        }
+        
+        if (appended.size > 0)
+            notify_email_appended(appended);
+        
+        if (created.size > 0)
+            notify_email_locally_appended(created);
+        
+        notify_email_count_changed(reported_remote_count, CountChangeReason.APPENDED);
+        
+        debug("%s do_replay_appended_message: completed, current remote_count=%d reported_remote_count=%d",
+            to_string(), remote_count, reported_remote_count);
+    }
+    
+    private void on_remote_removed(Imap.SequenceNumber position, int reported_remote_count) {
+        debug("%s on_remote_removed: remote_count=%d position=%s reported_remote_count=%d", to_string(),
+            remote_count, position.to_string(), reported_remote_count);
+        
+        if (reported_remote_count < 0)
+            return;
+        
+        // notify of removal to all pending replay operations
+        replay_queue.notify_remote_removed_position(position);
+        
+        // update remote count NOW, as further appended and removed messages can arrive before
+        // ReplayRemoval executes
+        //
+        // something to note at this point: the ExpungeEmail operation marks messages as removed,
+        // then signals they're removed and reports an adjusted count in its replay_local_async().
+        // remote_count is *not* updated, which is why it's safe to do that here without worry.
+        // similarly, signals are only fired here if marked, so the same EmailIdentifier isn't
+        // reported twice
+        remote_count = reported_remote_count;
+        
+        replay_queue.schedule_server_notification(new ReplayRemoval(this, reported_remote_count, position));
+    }
+    
+    // This MUST only be called from ReplayRemoval.
+    internal async void do_replay_removed_message(int reported_remote_count, Imap.SequenceNumber 
remote_position) {
+        debug("%s do_replay_removed_message: current remote_count=%d remote_position=%d 
reported_remote_count=%d",
+            to_string(), remote_count, remote_position.value, reported_remote_count);
+        
+        if (!remote_position.is_valid()) {
+            debug("%s do_replay_removed_message: ignoring, invalid remote position or count",
+                to_string());
+            
+            return;
+        }
+        
+        int local_count = -1;
+        int local_position = -1;
+        
+        ImapDB.EmailIdentifier? owned_id = null;
+        try {
+            // need total count, including those marked for removal, to accurately calculate position
+            // from server's point of view, not client's
+            local_count = yield local_folder.get_email_count_async(
+                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
+            local_position = remote_position.value - (reported_remote_count + 1 - local_count);
+            
+            // zero or negative means the message exists beyond the local vector's range, so
+            // nothing to do there
+            if (local_position > 0) {
+                debug("%s do_replay_removed_message: local_count=%d local_position=%d", to_string(),
+                    local_count, local_position);
+                
+                owned_id = yield local_folder.get_id_at_async(local_position, null);
+            } else {
+                debug("%s do_replay_removed_message: message not stored locally (local_count=%d 
local_position=%d)",
+                    to_string(), local_count, local_position);
+            }
+        } catch (Error err) {
+            debug("%s do_replay_removed_message: unable to determine ID of removed message %s: %s",
+                to_string(), remote_position.to_string(), err.message);
+        }
+        
+        bool marked = false;
+        if (owned_id != null) {
+            debug("%s do_replay_removed_message: detaching from local store Email ID %s", to_string(),
+                owned_id.to_string());
+            try {
+                // Reflect change in the local store and notify subscribers
+                yield local_folder.detach_single_email_async(owned_id, out marked, null);
+            } catch (Error err) {
+                debug("%s do_replay_removed_message: unable to remove message #%s: %s", to_string(),
+                    remote_position.to_string(), err.message);
+            }
+            
+            // Notify queued replay operations that the email has been removed (by EmailIdentifier)
+            replay_queue.notify_remote_removed_ids(
+                Geary.iterate<ImapDB.EmailIdentifier>(owned_id).to_array_list());
+        } else {
+            debug("%s do_replay_removed_message: remote_position=%d unknown in local store "
+                + "(reported_remote_count=%d local_position=%d local_count=%d)",
+                to_string(), remote_position.value, reported_remote_count, local_position, local_count);
+        }
+        
+        // for debugging
+        int new_local_count = -1;
+        try {
+            new_local_count = yield local_folder.get_email_count_async(
+                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
+        } catch (Error err) {
+            debug("%s do_replay_removed_message: error fetching new local count: %s", to_string(),
+                err.message);
+        }
+        
+        // as with on_remote_appended(), only update in local store inside a queue operation, to
+        // ensure serial commits
+        try {
+            yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
+        } catch (Error err) {
+            debug("%s do_replay_removed_message: unable to save removed remote count: %s", to_string(),
+                err.message);
+        }
+        
+        // notify of change
+        if (!marked && owned_id != null)
+            notify_email_removed(Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list());
+        
+        if (!marked)
+            notify_email_count_changed(reported_remote_count, CountChangeReason.REMOVED);
+        
+        debug("%s do_replay_remove_message: completed, current remote_count=%d "
+            + "(reported_remote_count=%d local_count=%d starting local_count=%d remote_position=%d 
local_position=%d marked=%s)",
+            to_string(), remote_count, reported_remote_count, new_local_count, local_count, 
remote_position.value,
+            local_position, marked.to_string());
+    }
+    
+    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));
+    }
+    
+    //
+    // list_email_by_id variants
+    //
+    
+    public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
+        int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
+        Cancellable? cancellable = null) throws Error {
+        Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+        yield do_list_email_by_id_async("list_email_by_id_async", initial_id, count, required_fields,
+            flags, accumulator, null, cancellable);
+        
+        return !accumulator.is_empty ? accumulator : null;
+    }
+    
+    public override void lazy_list_email_by_id(Geary.EmailIdentifier? initial_id, int count,
+        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
+        Cancellable? cancellable = null) {
+        do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
+    }
+    
+    private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier? initial_id, int count,
+        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable) {
+        try {
+            yield do_list_email_by_id_async("lazy_list_email_by_id", initial_id, count, required_fields,
+                flags, null, cb, cancellable);
+        } catch (Error err) {
+            cb(null, err);
+        }
+    }
+    
+    private async void do_list_email_by_id_async(string method, Geary.EmailIdentifier? initial_id,
+        int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
+        Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable) throws Error {
+        check_open(method);
+        check_flags(method, flags);
+        if (initial_id != null)
+            check_id(method, initial_id);
+        
+        if (count == 0) {
+            // signal finished
+            if (cb != null)
+                cb(null, null);
+            
+            return;
+        }
+        
+        // Schedule list operation and wait for completion.
+        ListEmailByID op = new ListEmailByID(this, (ImapDB.EmailIdentifier) initial_id, count,
+            required_fields, flags, accumulator, cb, cancellable);
+        replay_queue.schedule(op);
+        
+        yield op.wait_for_ready_async(cancellable);
+    }
+    
+    //
+    // list_email_by_sparse_id variants
+    //
+    
+    public async override Gee.List<Geary.Email>? list_email_by_sparse_id_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
+        Cancellable? cancellable = null) throws Error {
+        Gee.ArrayList<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+        yield do_list_email_by_sparse_id_async("list_email_by_sparse_id_async", ids, required_fields,
+            flags, accumulator, null, cancellable);
+        
+        return (accumulator.size > 0) ? accumulator : null;
+    }
+    
+    public override void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
+        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable = null) {
+        do_lazy_list_email_by_sparse_id_async.begin(ids, required_fields, flags, cb, cancellable);
+    }
+    
+    private async void do_lazy_list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
+        Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable? 
cancellable) {
+        try {
+            yield do_list_email_by_sparse_id_async("lazy_list_email_by_sparse_id", ids, required_fields,
+                flags, null, cb, cancellable);
+        } catch (Error err) {
+            cb(null, err);
+        }
+    }
+    
+    private async void do_list_email_by_sparse_id_async(string method,
+        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
+        Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error 
{
+        check_open(method);
+        check_flags(method, flags);
+        check_ids(method, ids);
+        
+        if (ids.size == 0) {
+            // signal finished
+            if (cb != null)
+                cb(null, null);
+            
+            return;
+        }
+        
+        // Schedule list operation and wait for completion.
+        // TODO: Break up requests to avoid hogging the queue
+        ListEmailBySparseID op = new ListEmailBySparseID(this, (Gee.Collection<ImapDB.EmailIdentifier>) ids,
+            required_fields, flags, accumulator, cb, cancellable);
+        replay_queue.schedule(op);
+        
+        yield op.wait_for_ready_async(cancellable);
+    }
+    
+    public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
+        check_open("list_local_email_fields_async");
+        check_ids("list_local_email_fields_async", ids);
+        
+        return yield local_folder.list_email_fields_by_id_async(
+            (Gee.Collection<Geary.ImapDB.EmailIdentifier>) ids, ImapDB.Folder.ListFlags.NONE, cancellable);
+    }
+    
+    public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
+        Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
+        throws Error {
+        check_open("fetch_email_async");
+        check_flags("fetch_email_async", flags);
+        check_id("fetch_email_async", id);
+        
+        FetchEmail op = new FetchEmail(this, (ImapDB.EmailIdentifier) id, required_fields, flags,
+            cancellable);
+        replay_queue.schedule(op);
+        
+        yield op.wait_for_ready_async(cancellable);
+        
+        if (op.email == null) {
+            throw new EngineError.NOT_FOUND("Email %s not found in %s", id.to_string(), to_string());
+        } else if (!op.email.fields.fulfills(required_fields)) {
+            throw new EngineError.INCOMPLETE_MESSAGE("Email %s in %s does not fulfill required fields %Xh 
(has %Xh)",
+                id.to_string(), to_string(), required_fields, op.email.fields);
+        }
+        
+        return op.email;
+    }
+    
+    // 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 {
+        check_open("expunge_email_async");
+        check_ids("expunge_email_async", email_ids);
+        
+        RemoveEmail remove = new RemoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) email_ids,
+            cancellable);
+        replay_queue.schedule(remove);
+        
+        yield remove.wait_for_ready_async(cancellable);
+    }
+    
+    private void check_open(string method) throws EngineError {
+        if (open_count == 0)
+            throw new EngineError.OPEN_REQUIRED("%s failed: folder %s is not open", method, to_string());
+    }
+    
+    private void check_flags(string method, Folder.ListFlags flags) throws EngineError {
+        if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY) && 
flags.is_all_set(Folder.ListFlags.FORCE_UPDATE)) {
+            throw new EngineError.BAD_PARAMETERS("%s %s failed: LOCAL_ONLY and FORCE_UPDATE are mutually 
exclusive",
+                to_string(), method);
+        }
+    }
+    
+    private void check_id(string method, EmailIdentifier id) throws EngineError {
+        if (!(id is ImapDB.EmailIdentifier))
+            throw new EngineError.BAD_PARAMETERS("Email ID %s is not IMAP Email ID", id.to_string());
+    }
+    
+    private void check_ids(string method, Gee.Collection<EmailIdentifier> ids) throws EngineError {
+        foreach (EmailIdentifier id in ids)
+            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, 
+        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);
+    }
+
+    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);
+        
+        CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
+        replay_queue.schedule(copy);
+        yield copy.wait_for_ready_async(cancellable);
+    }
+
+    public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+        Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+        check_open("move_email_async");
+        check_ids("move_email_async", to_move);
+        
+        MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
+        replay_queue.schedule(move);
+        yield move.wait_for_ready_async(cancellable);
+    }
+    
+    private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
+        notify_email_flags_changed(changed);
+    }
+    
+    // TODO: A proper public search mechanism; note that this always round-trips to the remote,
+    // doesn't go through the replay queue, and doesn't deal with messages marked for deletion
+    internal async Geary.EmailIdentifier? find_earliest_email_async(DateTime datetime,
+        Geary.EmailIdentifier? before_id, Cancellable? cancellable) throws Error {
+        check_open("find_earliest_email_async");
+        if (before_id != null)
+            check_id("find_earliest_email_async", before_id);
+        
+        Imap.SearchCriteria criteria = new Imap.SearchCriteria();
+        criteria.is_(Imap.SearchCriterion.since_internaldate(new 
Imap.InternalDate.from_date_time(datetime)));
+        
+        // if before_id available, only search for messages before it
+        if (before_id != null) {
+            Imap.UID? before_uid = yield local_folder.get_uid_async((ImapDB.EmailIdentifier) before_id,
+                ImapDB.Folder.ListFlags.NONE, cancellable);
+            if (before_uid == null) {
+                throw new EngineError.NOT_FOUND("before_id %s not found in %s", before_id.to_string(),
+                    to_string());
+            }
+            
+            criteria.and(Imap.SearchCriterion.message_set(
+                new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous(true))));
+        }
+        
+        Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+        ServerSearchEmail op = new ServerSearchEmail(this, criteria, Geary.Email.Field.NONE,
+            accumulator, cancellable);
+        
+        // need to check again due to the yield in the above conditional block
+        check_open("find_earliest_email_async.schedule operation");
+        
+        replay_queue.schedule(op);
+        
+        if (!yield op.wait_for_ready_async(cancellable))
+            return null;
+        
+        // find earliest ID; because all Email comes from Folder, UID should always be present
+        ImapDB.EmailIdentifier? earliest_id = null;
+        foreach (Geary.Email email in accumulator) {
+            ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
+            
+            if (earliest_id == null || email_id.uid.compare_to(earliest_id.uid) < 0)
+                earliest_id = email_id;
+        }
+        
+        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 {
+        check_open("create_email_async");
+        if (id != null)
+            check_id("create_email_async", id);
+        
+        Error? cancel_error = null;
+        Geary.EmailIdentifier? ret = null;
+        try {
+            CreateEmail create = new CreateEmail(this, rfc822, flags, date_received, cancellable);
+            replay_queue.schedule(create);
+            yield create.wait_for_ready_async(cancellable);
+            
+            ret = create.created_id;
+        } catch (Error e) {
+            if (e is IOError.CANCELLED)
+                cancel_error = e;
+            else
+                throw e;
+        }
+        
+        Geary.FolderSupport.Remove? remove_folder = this as Geary.FolderSupport.Remove;
+        
+        // Remove old message.
+        if (id != null && remove_folder != null)
+            yield remove_folder.remove_single_email_async(id, null);
+        
+        // If the user cancelled the operation, throw the error here.
+        if (cancel_error != null)
+            throw cancel_error;
+        
+        // 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);
+        
+        return ret;
+    }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala 
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index 55524ed..9b78bd1 100644
--- a/src/engine/imap-engine/imap-engine-replay-queue.vala
+++ b/src/engine/imap-engine/imap-engine-replay-queue.vala
@@ -49,7 +49,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
         return remote_queue.size;
     } }
     
-    private weak GenericFolder owner;
+    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 ReplayOperation? local_op_active = null;
@@ -124,7 +124,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
      * each ReplayOperation waiting to perform a remote operation, cancelling it if the remote
      * folder is not ready.
      */
-    public ReplayQueue(GenericFolder owner) {
+    public ReplayQueue(MinimalFolder owner) {
         this.owner = owner;
         
         // fire off background queue processors
diff --git a/src/engine/imap-engine/other/imap-engine-other-account.vala 
b/src/engine/imap-engine/other/imap-engine-other-account.vala
index 1a4dda2..3832e1b 100644
--- a/src/engine/imap-engine/other/imap-engine-other-account.vala
+++ b/src/engine/imap-engine/other/imap-engine-other-account.vala
@@ -10,7 +10,7 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
         base (name, account_information, false, remote, local);
     }
     
-    protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+    protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
         ImapDB.Account local_account, ImapDB.Folder local_folder) {
         SpecialFolderType type;
         if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
@@ -18,22 +18,7 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
         else
             type = local_folder.get_properties().attrs.get_special_folder_type();
         
-        switch (type) {
-            case SpecialFolderType.SENT:
-                return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
-                    type);
-            
-            case SpecialFolderType.TRASH:
-                return new GenericTrashFolder(this, remote_account, local_account, local_folder,
-                    type);
-            
-            case SpecialFolderType.DRAFTS:
-                return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
-                    type);
-            
-            default:
-                return new OtherFolder(this, remote_account, local_account, local_folder, type);
-        }
+        return new OtherFolder(this, remote_account, local_account, local_folder, type);
     }
 }
 
diff --git a/src/engine/imap-engine/other/imap-engine-other-folder.vala 
b/src/engine/imap-engine/other/imap-engine-other-folder.vala
index e234590..3728faf 100644
--- a/src/engine/imap-engine/other/imap-engine-other-folder.vala
+++ b/src/engine/imap-engine/other/imap-engine-other-folder.vala
@@ -4,15 +4,10 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.OtherFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.OtherFolder : GenericFolder {
     public OtherFolder(OtherAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
         base (account, remote, local, local_folder, special_folder_type);
     }
-    
-    public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
-        Cancellable? cancellable = null) throws Error {
-        yield expunge_email_async(email_ids, cancellable);
-    }
 }
 
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala 
b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
index 8c08d17..c40d0ea 100644
--- a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
@@ -36,7 +36,7 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
         base (name, account_information, false, remote, local);
     }
     
-    protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+    protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
         ImapDB.Account local_account, ImapDB.Folder local_folder) {
         // use the Folder's attributes to determine if it's a special folder type, unless it's
         // INBOX; that's determined by name
@@ -46,23 +46,10 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
         else
             special_folder_type = local_folder.get_properties().attrs.get_special_folder_type();
         
-        // generate properly-interfaced Folder depending on the special type
-        // Proper Drafts support depends on Outlook.com supporting UIDPLUS or us devising another
-        // mechanism to associate new messages with drafts-in-progress; see
-        // http://redmine.yorba.org/issues/7495
-        switch (special_folder_type) {
-            case SpecialFolderType.SENT:
-                return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.TRASH:
-                return new GenericTrashFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            default:
-                return new OutlookFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-        }
+        if (special_folder_type == Geary.SpecialFolderType.DRAFTS)
+            return new OutlookDraftsFolder(this, remote_account, local_account, local_folder, 
special_folder_type);
+        
+        return new OutlookFolder(this, remote_account, local_account, local_folder, special_folder_type);
     }
 }
 
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala 
b/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
new file mode 100644
index 0000000..605808d
--- /dev/null
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
@@ -0,0 +1,19 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * Since Outlook doesn't support UIDPLUS, we can't delete old drafts before
+ * saving a new one.  Instead of allowing their drafts folder to fill up with
+ * countless revisions of every message, we simply don't expose the
+ * Geary.FolderSupport.Create interface from the drafts folder, so nothing gets
+ * saved at all.
+ */
+private class Geary.ImapEngine.OutlookDraftsFolder : MinimalFolder {
+    public OutlookDraftsFolder(OutlookAccount account, Imap.Account remote, ImapDB.Account local,
+        ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
+        base (account, remote, local, local_folder, special_folder_type);
+    }
+}
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala 
b/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
index 8c9a2bc..b65e3bc 100644
--- a/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
@@ -4,15 +4,10 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.OutlookFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.OutlookFolder : GenericFolder {
     public OutlookFolder(OutlookAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
         base (account, remote, local, local_folder, special_folder_type);
     }
-    
-    public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
-        Cancellable? cancellable = null) throws Error {
-        yield expunge_email_async(email_ids, cancellable);
-    }
 }
 
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 57da967..ebe90c0 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
@@ -7,7 +7,7 @@
 private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.SendReplayOperation {
     private class RemoteBatchOperation : Nonblocking.BatchOperation {
         // IN
-        public GenericFolder owner;
+        public MinimalFolder owner;
         public Imap.MessageSet msg_set;
         public Geary.Email.Field unfulfilled_fields;
         public Geary.Email.Field required_fields;
@@ -15,7 +15,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
         // OUT
         public Gee.Set<Geary.EmailIdentifier> created_ids = new Gee.HashSet<Geary.EmailIdentifier>();
         
-        public RemoteBatchOperation(GenericFolder owner, Imap.MessageSet msg_set,
+        public RemoteBatchOperation(MinimalFolder owner, Imap.MessageSet msg_set,
             Geary.Email.Field unfulfilled_fields, Geary.Email.Field required_fields) {
             this.owner = owner;
             this.msg_set = msg_set;
@@ -53,7 +53,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
         }
     }
     
-    protected GenericFolder owner;
+    protected MinimalFolder owner;
     protected Geary.Email.Field required_fields;
     protected Gee.List<Geary.Email>? accumulator = null;
     protected weak EmailCallback? cb;
@@ -62,7 +62,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
     
     private Gee.HashMap<Imap.UID, Geary.Email.Field> unfulfilled = new Gee.HashMap<Imap.UID, 
Geary.Email.Field>();
     
-    public AbstractListEmail(string name, GenericFolder owner, Geary.Email.Field required_fields,
+    public AbstractListEmail(string name, MinimalFolder owner, Geary.Email.Field required_fields,
         Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator, EmailCallback? cb,
         Cancellable? cancellable) {
         base(name);
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 422d0af..9fec2ae 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,12 +5,12 @@
  */
 
 private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation {
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private Gee.HashSet<ImapDB.EmailIdentifier> to_copy = new Gee.HashSet<ImapDB.EmailIdentifier>();
     private Geary.FolderPath destination;
     private Cancellable? cancellable;
 
-    public CopyEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_copy, 
+    public CopyEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_copy, 
         Geary.FolderPath destination, Cancellable? cancellable = null) {
         base("CopyEmail");
         
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 4477df2..b27170b 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
@@ -7,13 +7,13 @@
 private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperation {
     public Geary.EmailIdentifier? created_id { get; private set; default = null; }
     
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private RFC822.Message rfc822;
     private Geary.EmailFlags? flags;
     private DateTime? date_received;
     private Cancellable? cancellable;
     
-    public CreateEmail(GenericFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags,
+    public CreateEmail(MinimalFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags,
         DateTime? date_received, Cancellable? cancellable) {
         base.only_remote("CreateEmail");
         
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 5a92736..a1c6f78 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
@@ -7,7 +7,7 @@
 private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation {
     public Email? email = null;
     
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private ImapDB.EmailIdentifier id;
     private Email.Field required_fields;
     private Email.Field remaining_fields;
@@ -16,7 +16,7 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
     private Imap.UID? uid = null;
     private bool remote_removed = false;
     
-    public FetchEmail(GenericFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
+    public FetchEmail(MinimalFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
         Folder.ListFlags flags, Cancellable? cancellable) {
         base ("FetchEmail");
         
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
index adc7b03..bd3f3d1 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
@@ -10,7 +10,7 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
     private int fulfilled_count = 0;
     private Imap.UID? initial_uid = null;
     
-    public ListEmailByID(GenericFolder owner, ImapDB.EmailIdentifier? initial_id, int count,
+    public ListEmailByID(MinimalFolder owner, ImapDB.EmailIdentifier? initial_id, int count,
         Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
         EmailCallback? cb, Cancellable? cancellable) {
         base ("ListEmailByID", owner, required_fields, flags, accumulator, cb, cancellable);
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
index 02534a1..c7e42d4 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
@@ -7,7 +7,7 @@
 private class Geary.ImapEngine.ListEmailBySparseID : Geary.ImapEngine.AbstractListEmail {
     private Gee.HashSet<ImapDB.EmailIdentifier> ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
     
-    public ListEmailBySparseID(GenericFolder owner, Gee.Collection<ImapDB.EmailIdentifier> ids,
+    public ListEmailBySparseID(MinimalFolder owner, Gee.Collection<ImapDB.EmailIdentifier> ids,
         Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
         EmailCallback cb, Cancellable? cancellable) {
         base ("ListEmailBySparseID", owner, required_fields, flags, accumulator, cb, cancellable);
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 e2a3fe3..e40f3fa 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
@@ -5,14 +5,14 @@
  */
 
 private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation {
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private Gee.List<Geary.EmailIdentifier> to_mark = new Gee.ArrayList<Geary.EmailIdentifier>();
     private Geary.EmailFlags? flags_to_add;
     private Geary.EmailFlags? flags_to_remove;
     private Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? original_flags = null;
     private Cancellable? cancellable;
     
-    public MarkEmail(GenericFolder engine, Gee.List<Geary.EmailIdentifier> to_mark, 
+    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");
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 c3f7c62..40157d3 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
@@ -5,14 +5,14 @@
  */
 
 private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation {
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
     private Geary.FolderPath destination;
     private Cancellable? cancellable;
     private Gee.Set<ImapDB.EmailIdentifier>? moved_ids = null;
     private int original_count = 0;
 
-    public MoveEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move, 
+    public MoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move, 
         Geary.FolderPath destination, Cancellable? cancellable = null) {
         base("MoveEmail");
 
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 f26f85c..b2a5ab3 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
@@ -5,13 +5,13 @@
  */
 
 private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperation {
-    private GenericFolder engine;
+    private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_remove = new Gee.ArrayList<ImapDB.EmailIdentifier>();
     private Cancellable? cancellable;
     private Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
     private int original_count = 0;
     
-    public RemoveEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
+    public RemoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
         Cancellable? cancellable = null) {
         base("RemoveEmail");
         
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 874847d..6d561be 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,11 +5,11 @@
  */
 
 private class Geary.ImapEngine.ReplayAppend : Geary.ImapEngine.ReplayOperation {
-    public GenericFolder owner;
+    public MinimalFolder owner;
     public int remote_count;
     public Gee.List<Imap.SequenceNumber> positions;
     
-    public ReplayAppend(GenericFolder owner, int remote_count, Gee.List<Imap.SequenceNumber> positions) {
+    public ReplayAppend(MinimalFolder owner, int remote_count, Gee.List<Imap.SequenceNumber> positions) {
         base ("Append", Scope.REMOTE_ONLY);
         
         this.owner = owner;
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 f4590c0..9ac2663 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,10 +5,10 @@
  */
 
 private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperation {
-    public GenericFolder owner;
+    public MinimalFolder owner;
     public Imap.ClientSession.DisconnectReason reason;
     
-    public ReplayDisconnect(GenericFolder owner, Imap.ClientSession.DisconnectReason reason) {
+    public ReplayDisconnect(MinimalFolder owner, Imap.ClientSession.DisconnectReason reason) {
         base ("Disconnect", Scope.LOCAL_ONLY);
         
         this.owner = owner;
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 0690bb5..4195168 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,11 +5,11 @@
  */
 
 private class Geary.ImapEngine.ReplayRemoval : Geary.ImapEngine.ReplayOperation {
-    public GenericFolder owner;
+    public MinimalFolder owner;
     public int remote_count;
     public Imap.SequenceNumber position;
     
-    public ReplayRemoval(GenericFolder owner, int remote_count, Imap.SequenceNumber position) {
+    public ReplayRemoval(MinimalFolder owner, int remote_count, Imap.SequenceNumber position) {
         base ("Removal", Scope.LOCAL_ONLY);
         
         this.owner = owner;
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 d5b7c8f..5a96e0c 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
@@ -13,7 +13,7 @@
 private class Geary.ImapEngine.ServerSearchEmail : Geary.ImapEngine.AbstractListEmail {
     private Imap.SearchCriteria criteria;
     
-    public ServerSearchEmail(GenericFolder owner, Imap.SearchCriteria criteria, Geary.Email.Field 
required_fields,
+    public ServerSearchEmail(MinimalFolder owner, Imap.SearchCriteria criteria, Geary.Email.Field 
required_fields,
         Gee.List<Geary.Email>? accumulator, Cancellable? cancellable) {
         // OLDEST_TO_NEWEST used for vector expansion, if necessary
         base ("ServerSearchEmail", owner, required_fields, Geary.Folder.ListFlags.OLDEST_TO_NEWEST,
diff --git a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala 
b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
index 4a91945..68958e5 100644
--- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
+++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
@@ -48,27 +48,12 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
         }
     }
     
-    protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+    protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
         ImapDB.Account local_account, ImapDB.Folder local_folder) {
         SpecialFolderType special_folder_type = special_map.has_key(path) ? special_map.get(path)
             : Geary.SpecialFolderType.NONE;
-        switch (special_folder_type) {
-            case SpecialFolderType.SENT:
-                return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.TRASH:
-                return new GenericTrashFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            case SpecialFolderType.DRAFTS:
-                return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-            
-            default:
-                return new YahooFolder(this, remote_account, local_account, local_folder,
-                    special_folder_type);
-        }
+        return new YahooFolder(this, remote_account, local_account, local_folder,
+            special_folder_type);
     }
 }
 
diff --git a/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala 
b/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
index 50d9469..3c97019 100644
--- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
+++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
@@ -4,15 +4,10 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.ImapEngine.YahooFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.YahooFolder : GenericFolder {
     public YahooFolder(YahooAccount account, Imap.Account remote, ImapDB.Account local,
         ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
         base (account, remote, local, local_folder, special_folder_type);
     }
-    
-    public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
-        Cancellable? cancellable = null) throws Error {
-        yield expunge_email_async(email_ids, cancellable);
-    }
 }
 
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 7e46c04..090e582 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -152,6 +152,18 @@ private class Geary.Imap.Account : BaseObject {
         return path_to_mailbox.has_key(path);
     }
     
+    public async void create_folder_async(FolderPath path, Cancellable? cancellable) throws Error {
+        check_open();
+        
+        StatusResponse response = yield send_command_async(new CreateCommand(
+            new MailboxSpecifier.from_folder_path(path, null)), null, null, cancellable);
+        
+        if (response.status != Status.OK) {
+            throw new ImapError.SERVER_ERROR("Server reports error creating path %s: %s", path.to_string(),
+                response.to_string());
+        }
+    }
+    
     public async Imap.Folder fetch_folder_async(FolderPath path, Cancellable? cancellable)
         throws Error {
         check_open();
diff --git a/src/engine/imap/command/imap-create-command.vala 
b/src/engine/imap/command/imap-create-command.vala
new file mode 100644
index 0000000..83ff2be
--- /dev/null
+++ b/src/engine/imap/command/imap-create-command.vala
@@ -0,0 +1,23 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * See [[http://tools.ietf.org/html/rfc3501#section-6.3.3]]
+ */
+
+public class Geary.Imap.CreateCommand : Command {
+    public const string NAME = "create";
+    
+    public MailboxSpecifier mailbox { get; private set; }
+    
+    public CreateCommand(MailboxSpecifier mailbox) {
+        base (NAME);
+        
+        this.mailbox = mailbox;
+        
+        add(mailbox.to_parameter());
+    }
+}
diff --git a/src/engine/util/util-iterable.vala b/src/engine/util/util-iterable.vala
index 10ad20a..bccabb3 100644
--- a/src/engine/util/util-iterable.vala
+++ b/src/engine/util/util-iterable.vala
@@ -27,6 +27,13 @@ namespace Geary {
         
         return Geary.traverse<G>(list);
     }
+    
+    /**
+     * Take an array of items and return a Geary.Iterable for convenience.
+     */
+    public Geary.Iterable<G> iterate_array<G>(G[] a) {
+        return Geary.traverse<G>(new Gee.ArrayList<G>.wrap(a));
+    }
 }
 
 /**


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