[geary/wip/181-special-folder-dupes: 6/7] Convert Geary.FolderRoot to be an actual root, not just a top-level



commit 960ed1994d78ccab99bfcb581914594a89e47037
Author: Michael Gratton <mike vee net>
Date:   Mon Jan 14 12:10:48 2019 +1100

    Convert Geary.FolderRoot to be an actual root, not just a top-level
    
    Instead of each top-level IMAP folder being a FolderRoot object, then
    children of that being FolderPath objects, this makes FolderRoot an
    "empty" FolderPath, so that both top-level and descendant folders are
    plain FolderPath objects. Aside from being more technically correct,
    this means that empty namespace roots can now be used interchangably
    with non-empty namespace roots (addressing issue #181), and custom
    folder implementations no longer need to provide their own trivial,
    custom FolderRoot.
    
    To support this, a notion of an IMAP root and a local root have been
    added from which all remote and local folder paths are now derived,
    existing places that assume top-level == root have been fixed, and
    unit tests have been added.

 po/POTFILES.in                                     |   2 -
 .../folder-list/folder-list-account-branch.vala    |   2 +-
 src/engine/api/geary-account-information.vala      |   7 +-
 src/engine/api/geary-folder-path.vala              | 111 ++++++++-----
 src/engine/imap-db/imap-db-account.vala            | 180 +++++++++++++--------
 .../imap-db/search/imap-db-search-folder-root.vala |  14 --
 .../imap-db/search/imap-db-search-folder.vala      |  25 ++-
 .../gmail/imap-engine-gmail-account.vala           |   3 +-
 .../gmail/imap-engine-gmail-search-folder.vala     |   4 +-
 .../imap-engine/imap-engine-generic-account.vala   |  46 ++++--
 .../yahoo/imap-engine-yahoo-account.vala           |  27 +++-
 src/engine/imap/api/imap-account-session.vala      |  36 ++++-
 src/engine/imap/api/imap-folder-root.vala          |  71 ++++----
 .../imap/message/imap-mailbox-specifier.vala       |  91 +++++++----
 .../imap/response/imap-mailbox-information.vala    |  13 +-
 src/engine/imap/transport/imap-client-session.vala |   9 +-
 src/engine/meson.build                             |   2 -
 src/engine/outbox/outbox-folder-root.vala          |  18 ---
 src/engine/outbox/outbox-folder.vala               |  22 ++-
 test/engine/api/geary-folder-path-mock.vala        |  14 --
 test/engine/api/geary-folder-path-test.vala        |  41 +++++
 test/engine/app/app-conversation-monitor-test.vala |  14 +-
 test/engine/app/app-conversation-set-test.vala     |  20 ++-
 test/engine/app/app-conversation-test.vala         |  21 ++-
 test/engine/imap-db/imap-db-account-test.vala      |  24 +--
 .../imap/message/imap-mailbox-specifier-test.vala  |  85 ++++++----
 test/meson.build                                   |   2 +-
 test/test-engine.vala                              |   1 +
 28 files changed, 572 insertions(+), 333 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5a5b0e98..ca8fcc94 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -221,7 +221,6 @@ src/engine/imap-db/imap-db-message-addresses.vala
 src/engine/imap-db/imap-db-message-row.vala
 src/engine/imap-db/search/imap-db-search-email-identifier.vala
 src/engine/imap-db/search/imap-db-search-folder-properties.vala
-src/engine/imap-db/search/imap-db-search-folder-root.vala
 src/engine/imap-db/search/imap-db-search-folder.vala
 src/engine/imap-db/search/imap-db-search-query.vala
 src/engine/imap-db/search/imap-db-search-term.vala
@@ -346,7 +345,6 @@ src/engine/nonblocking/nonblocking-variants.vala
 src/engine/outbox/outbox-email-identifier.vala
 src/engine/outbox/outbox-email-properties.vala
 src/engine/outbox/outbox-folder-properties.vala
-src/engine/outbox/outbox-folder-root.vala
 src/engine/outbox/outbox-folder.vala
 src/engine/rfc822/rfc822-error.vala
 src/engine/rfc822/rfc822-gmime-filter-blockquotes.vala
diff --git a/src/client/folder-list/folder-list-account-branch.vala 
b/src/client/folder-list/folder-list-account-branch.vala
index ac6a5457..6a87da02 100644
--- a/src/client/folder-list/folder-list-account-branch.vala
+++ b/src/client/folder-list/folder-list-account-branch.vala
@@ -90,7 +90,7 @@ public class FolderList.AccountBranch : Sidebar.Branch {
             
             // Special folders go in the root of the account.
             graft_point = get_root();
-        } else if (folder.path.get_parent() == null) {
+        } else if (folder.path.is_top_level) {
             // Top-level folders get put in our special user folders group.
             graft_point = user_folder_group;
 
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 7e9d443c..edc867a1 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -29,9 +29,10 @@ public class Geary.AccountInformation : BaseObject {
         if (parts == null || parts.size == 0)
             return null;
 
-        Geary.FolderPath path = new Imap.FolderRoot(parts[0]);
-        for (int i = 1; i < parts.size; i++)
-            path = path.get_child(parts.get(i));
+        Geary.FolderPath path = new Imap.FolderRoot();
+        foreach (string basename in parts) {
+            path = path.get_child(basename);
+        }
         return path;
     }
 
diff --git a/src/engine/api/geary-folder-path.vala b/src/engine/api/geary-folder-path.vala
index 5193a638..4d398d99 100644
--- a/src/engine/api/geary-folder-path.vala
+++ b/src/engine/api/geary-folder-path.vala
@@ -15,11 +15,13 @@
 
 public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
     Gee.Comparable<Geary.FolderPath> {
+
+
     /**
      * The name of this folder (without any child or parent names or delimiters).
      */
     public string basename { get; private set; }
-    
+
     /**
      * Whether this path is lexiographically case-sensitive.
      *
@@ -27,16 +29,34 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
      */
     public bool case_sensitive { get; private set; }
 
+    /**
+     * Determines if this path is a root folder path.
+     */
+    public virtual bool is_root {
+        get { return this.path == null || this.path.size == 0; }
+    }
+
+    /**
+     * Determines if this path is a child of the root folder.
+     */
+    public bool is_top_level {
+        get {
+            FolderPath? parent = get_parent();
+            return parent != null && parent.is_root;
+        }
+    }
+
+
     private Gee.List<Geary.FolderPath>? path = null;
     private uint stored_hash = uint.MAX;
-    
-    protected FolderPath(string basename, bool case_sensitive) {
-        assert(this is FolderRoot);
-        
-        this.basename = basename;
-        this.case_sensitive = case_sensitive;
+
+
+    /** Constructor only for use by {@link FolderRoot}. */
+    internal FolderPath() {
+        this.basename = "";
+        this.case_sensitive = false;
     }
-    
+
     private FolderPath.child(Gee.List<Geary.FolderPath> path, string basename, bool case_sensitive) {
         assert(path[0] is FolderRoot);
         
@@ -44,19 +64,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
         this.basename = basename;
         this.case_sensitive = case_sensitive;
     }
-    
-    /**
-     * Returns true if this {@link FolderPath} is a root folder.
-     *
-     * This means that the FolderPath ''should'' be castable into {@link FolderRoot}, which is
-     * enforced through the constructor and accessor styles of this class.  However, this test
-     * merely checks if this FolderPath has any children.  A GObject "is" operation is the
-     * reliable way to cast to FolderRoot.
-     */
-    public bool is_root() {
-        return (path == null || path.size == 0);
-    }
-    
+
     /**
      * Returns the {@link FolderRoot} of this path.
      */
@@ -127,24 +135,31 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
         
         return list;
     }
-    
+
     /**
-     * Creates a {@link FolderPath} object that is a child of this folder.
+     * Creates a path that is a child of this folder.
      *
-     * {@link Trillian.TRUE} and {@link Trillian.FALSE} force case-sensitivity.
-     * {@link Trillian.UNKNOWN} indicates to use {@link FolderRoot.default_case_sensitivity}.
+     * Specifying {@link Trillian.TRUE} or {@link Trillian.FALSE} for
+     * `is_case_sensitive` forces case-sensitivity either way. If
+     * {@link Trillian.UNKNOWN}, then {@link
+     * FolderRoot.default_case_sensitivity} is used.
      */
-    public Geary.FolderPath get_child(string basename, Trillian child_case_sensitive = Trillian.UNKNOWN) {
+    public virtual Geary.FolderPath
+        get_child(string basename,
+                  Trillian is_case_sensitive = Trillian.UNKNOWN) {
         // Build the child's path, which is this node's path plus this node
         Gee.List<FolderPath> child_path = new Gee.ArrayList<FolderPath>();
         if (path != null)
             child_path.add_all(path);
         child_path.add(this);
-        
-        return new FolderPath.child(child_path, basename,
-            child_case_sensitive.to_boolean(get_root().default_case_sensitivity));
+
+        return new FolderPath.child(
+            child_path,
+            basename,
+            is_case_sensitive.to_boolean(get_root().default_case_sensitivity)
+        );
     }
-    
+
     /**
      * Returns true if the other {@link FolderPath} has the same parent as this one.
      *
@@ -312,28 +327,36 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
 }
 
 /**
- * The root of a folder heirarchy.
- *
- * A {@link FolderPath} can only be created by starting with a FolderRoot and adding children
- * via {@link FolderPath.get_child}.  Because all FolderPaths hold references to their parents,
- * this element can be retrieved with {@link FolderPath.get_root}.
+ * The root of a folder hierarchy.
  *
- * Since each email system may have different requirements for its paths, this is an abstract
- * class.
+ * A {@link FolderPath} can only be created by starting with a
+ * FolderRoot and adding children via {@link FolderPath.get_child}.
+ * Because all FolderPaths hold references to their parents, this
+ * element can be retrieved with {@link FolderPath.get_root}.
  */
-public abstract class Geary.FolderRoot : Geary.FolderPath {
+public class Geary.FolderRoot : Geary.FolderPath {
+
+
+    /** {@inheritDoc} */
+    public override bool is_root {
+        get { return true; }
+    }
+
     /**
-     * The default case sensitivity of each element in the {@link FolderPath}.
+     * The default case sensitivity of descendant folders.
      *
      * @see FolderRoot.case_sensitive
      * @see FolderPath.get_child
      */
     public bool default_case_sensitivity { get; private set; }
-    
-    protected FolderRoot(string basename, bool case_sensitive, bool default_case_sensitivity) {
-        base (basename, case_sensitive);
-        
+
+
+    /**
+     * Constructs a new folder root with given default sensitivity.
+     */
+    public FolderRoot(bool default_case_sensitivity) {
+        base();
         this.default_case_sensitivity = default_case_sensitivity;
     }
-}
 
+}
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index f6f77f31..2b67edd7 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -1,7 +1,9 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Michael Gratton <mike vee net>.
  *
  * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 private class Geary.ImapDB.Account : BaseObject {
@@ -74,9 +76,22 @@ private class Geary.ImapDB.Account : BaseObject {
 
     public signal void contacts_loaded();
 
+    /**
+     * The root path for all remote IMAP folders.
+     *
+     * No folder exists for this path locally or on the remote server,
+     * it merely exists to provide a common root for the paths of all
+     * IMAP folders.
+     *
+     * @see list_folders_async
+     */
+    public Imap.FolderRoot imap_folder_root {
+        get; private set; default = new Imap.FolderRoot();
+    }
+
     // Only available when the Account is opened
     public ImapEngine.ContactStore contact_store { get; private set; }
-    public IntervalProgressMonitor search_index_monitor { get; private set; 
+    public IntervalProgressMonitor search_index_monitor { get; private set;
         default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
     public SimpleProgressMonitor upgrade_monitor { get; private set; default = new SimpleProgressMonitor(
         ProgressType.DB_UPGRADE); }
@@ -476,11 +491,19 @@ private class Geary.ImapDB.Account : BaseObject {
             contacts_loaded();
         }
     }
-    
-    public async Gee.Collection<Geary.ImapDB.Folder> list_folders_async(Geary.FolderPath? parent,
-        Cancellable? cancellable = null) throws Error {
+
+    /**
+     * Lists all children of a given folder.
+     *
+     * To list all top-level folders, pass in {@link imap_folder_root}
+     * as the parent.
+     */
+    public async Gee.Collection<Geary.ImapDB.Folder>
+        list_folders_async(Geary.FolderPath parent,
+                           GLib.Cancellable? cancellable)
+        throws GLib.Error {
         check_open();
-        
+
         // TODO: A better solution here would be to only pull the FolderProperties if the Folder
         // object itself doesn't already exist
         Gee.HashMap<Geary.FolderPath, int64?> id_map = new Gee.HashMap<
@@ -489,17 +512,14 @@ private class Geary.ImapDB.Account : BaseObject {
             Geary.FolderPath, Geary.Imap.FolderProperties>();
         yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
             int64 parent_id = Db.INVALID_ROWID;
-            if (parent != null) {
-                if (!do_fetch_folder_id(cx, parent, false, out parent_id, cancellable)) {
-                    debug("Unable to find folder ID for %s to list folders", parent.to_string());
-                    
-                    return Db.TransactionOutcome.ROLLBACK;
-                }
-                
-                if (parent_id == Db.INVALID_ROWID)
-                    throw new EngineError.NOT_FOUND("Folder %s not found", parent.to_string());
+            if (!parent.is_root &&
+                !do_fetch_folder_id(
+                    cx, parent, false, out parent_id, cancellable
+                )) {
+                debug("Unable to find folder ID for \"%s\" to list folders", parent.to_string());
+                return Db.TransactionOutcome.ROLLBACK;
             }
-            
+
             Db.Statement stmt;
             if (parent_id != Db.INVALID_ROWID) {
                 stmt = cx.prepare(
@@ -511,15 +531,11 @@ private class Geary.ImapDB.Account : BaseObject {
                     "SELECT id, name, last_seen_total, unread_count, last_seen_status_total, "
                     + "uid_validity, uid_next, attributes FROM FolderTable WHERE parent_id IS NULL");
             }
-            
+
             Db.Result result = stmt.exec(cancellable);
             while (!result.finished) {
                 string basename = result.string_for("name");
-                
-                Geary.FolderPath path = (parent != null)
-                    ? parent.get_child(basename)
-                    : new Imap.FolderRoot(basename);
-
+                Geary.FolderPath path = parent.get_child(basename);
                 Geary.Imap.FolderProperties properties = new Geary.Imap.FolderProperties.from_imapdb(
                     Geary.Imap.MailboxAttributes.deserialize(result.string_for("attributes")),
                     result.int_for("last_seen_total"),
@@ -544,12 +560,13 @@ private class Geary.ImapDB.Account : BaseObject {
         }, cancellable);
         
         assert(id_map.size == prop_map.size);
-        
+
         if (id_map.size == 0) {
-            throw new EngineError.NOT_FOUND("No local folders in %s",
-                (parent != null) ? parent.to_string() : "root");
+            throw new EngineError.NOT_FOUND(
+                "No local folders under \"%s\"", parent.to_string()
+            );
         }
-        
+
         Gee.Collection<Geary.ImapDB.Folder> folders = new Gee.ArrayList<Geary.ImapDB.Folder>();
         foreach (Geary.FolderPath path in id_map.keys) {
             Geary.ImapDB.Folder? folder = get_local_folder(path);
@@ -1555,23 +1572,32 @@ private class Geary.ImapDB.Account : BaseObject {
         
         folder_stmt.exec(cancellable);
     }
-    
-    // If the FolderPath has no parent, returns true and folder_id will be set to Db.INVALID_ROWID.
-    // If cannot create path or there is a logical problem traversing it, returns false with folder_id
-    // set to Db.INVALID_ROWID.
-    internal bool do_fetch_folder_id(Db.Connection cx, Geary.FolderPath path, bool create, out int64 
folder_id,
-        Cancellable? cancellable) throws Error {
-        int length = path.get_path_length();
-        if (length < 0)
-            throw new EngineError.BAD_PARAMETERS("Invalid path %s", path.to_string());
-        
+
+    // If the FolderPath has no parent, returns true and folder_id
+    // will be set to Db.INVALID_ROWID.  If cannot create path or
+    // there is a logical problem traversing it, returns false with
+    // folder_id set to Db.INVALID_ROWID.
+    internal bool do_fetch_folder_id(Db.Connection cx,
+                                     Geary.FolderPath path,
+                                     bool create,
+                                     out int64 folder_id,
+                                     GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        if (path.is_root) {
+            throw new EngineError.BAD_PARAMETERS(
+                "Cannot fetch folder for root path"
+            );
+        }
+
+        // Don't include the root since top-level folders are stored
+        // with no parent.
+        Gee.List<string> parts = path.as_list();
+        parts.remove_at(0);
+
         folder_id = Db.INVALID_ROWID;
         int64 parent_id = Db.INVALID_ROWID;
-        
-        // walk the folder tree to the final node (which is at length - 1 - 1)
-        for (int ctr = 0; ctr < length; ctr++) {
-            string basename = path.get_folder_at(ctr).basename;
-            
+
+        foreach (string basename in parts) {
             Db.Statement stmt;
             if (parent_id != Db.INVALID_ROWID) {
                 stmt = cx.prepare("SELECT id FROM FolderTable WHERE parent_id=? AND name=?");
@@ -1616,19 +1642,28 @@ private class Geary.ImapDB.Account : BaseObject {
         
         return true;
     }
-    
-    // See do_fetch_folder_id() for return semantics.
-    internal bool do_fetch_parent_id(Db.Connection cx, Geary.FolderPath path, bool create, out int64 
parent_id,
-        Cancellable? cancellable = null) throws Error {
-        if (path.is_root()) {
+
+    internal bool do_fetch_parent_id(Db.Connection cx,
+                                     FolderPath path,
+                                     bool create,
+                                     out int64 parent_id,
+                                     GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        // See do_fetch_folder_id() for return semantics
+        bool ret = true;
+
+        // No folder for the root is saved in the database, so
+        // top-levels should not have a parent.
+        if (path.is_top_level) {
             parent_id = Db.INVALID_ROWID;
-            
-            return true;
+        } else {
+            ret = do_fetch_folder_id(
+                cx, path.get_parent(), create, out parent_id, cancellable
+            );
         }
-        
-        return do_fetch_folder_id(cx, path.get_parent(), create, out parent_id, cancellable);
+        return ret;
     }
-    
+
     private bool do_has_children(Db.Connection cx, int64 folder_id, Cancellable? cancellable) throws Error {
         Db.Statement stmt = cx.prepare("SELECT 1 FROM FolderTable WHERE parent_id = ?");
         stmt.bind_rowid(0, folder_id);
@@ -1700,8 +1735,12 @@ private class Geary.ImapDB.Account : BaseObject {
     
     // For a message row id, return a set of all folders it's in, or null if
     // it's not in any folders.
-    private static Gee.Set<Geary.FolderPath>? do_find_email_folders(Db.Connection cx, int64 message_id,
-        bool include_removed, Cancellable? cancellable) throws Error {
+    private Gee.Set<Geary.FolderPath>?
+        do_find_email_folders(Db.Connection cx,
+                              int64 message_id,
+                              bool include_removed,
+                              GLib.Cancellable? cancellable)
+        throws GLib.Error {
         string sql = "SELECT folder_id FROM MessageLocationTable WHERE message_id=?";
         if (!include_removed)
             sql += " AND remove_marker=0";
@@ -1724,16 +1763,20 @@ private class Geary.ImapDB.Account : BaseObject {
         
         return (folder_paths.size == 0 ? null : folder_paths);
     }
-    
+
     // For a folder row id, return the folder path (constructed with default
     // separator and case sensitivity) of that folder, or null in the event
     // it's not found.
-    private static Geary.FolderPath? do_find_folder_path(Db.Connection cx, int64 folder_id,
-        Cancellable? cancellable) throws Error {
-        Db.Statement stmt = cx.prepare("SELECT parent_id, name FROM FolderTable WHERE id=?");
+    private Geary.FolderPath? do_find_folder_path(Db.Connection cx,
+                                                  int64 folder_id,
+                                                  GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        Db.Statement stmt = cx.prepare(
+            "SELECT parent_id, name FROM FolderTable WHERE id=?"
+        );
         stmt.bind_int64(0, folder_id);
         Db.Result result = stmt.exec(cancellable);
-        
+
         if (result.finished)
             return null;
         
@@ -1746,12 +1789,19 @@ private class Geary.ImapDB.Account : BaseObject {
                 folder_id.to_string(), parent_id.to_string());
             return null;
         }
-        
-        if (parent_id <= 0)
-            return new Imap.FolderRoot(name);
-        
-        Geary.FolderPath? parent_path = do_find_folder_path(cx, parent_id, cancellable);
-        return (parent_path == null ? null : parent_path.get_child(name));
+
+        Geary.FolderPath? path = null;
+        if (parent_id <= 0) {
+            path = this.imap_folder_root.get_child(name);
+        } else {
+            Geary.FolderPath? parent_path = do_find_folder_path(
+                cx, parent_id, cancellable
+            );
+            if (parent_path != null) {
+                path = parent_path.get_child(name);
+            }
+        }
+        return path;
     }
 
     private void on_unread_updated(ImapDB.Folder source, Gee.Map<ImapDB.EmailIdentifier, bool>
diff --git a/src/engine/imap-db/search/imap-db-search-folder.vala 
b/src/engine/imap-db/search/imap-db-search-folder.vala
index 16b957fb..5f590635 100644
--- a/src/engine/imap-db/search/imap-db-search-folder.vala
+++ b/src/engine/imap-db/search/imap-db-search-folder.vala
@@ -5,23 +5,34 @@
  */
 
 private class Geary.ImapDB.SearchFolder : Geary.SearchFolder, Geary.FolderSupport.Remove {
-    // Max number of emails that can ever be in the folder.
+
+
+    /** Max number of emails that can ever be in the folder. */
     public const int MAX_RESULT_EMAILS = 1000;
-    
+
+    /** The canonical name of the search folder. */
+    public const string MAGIC_BASENAME = "$GearySearchFolder$";
+
     private const Geary.SpecialFolderType[] exclude_types = {
         Geary.SpecialFolderType.SPAM,
         Geary.SpecialFolderType.TRASH,
         Geary.SpecialFolderType.DRAFTS,
         // Orphan emails (without a folder) are also excluded; see ctor.
     };
-    
+
+
     private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
     private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
     private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
-    
-    public SearchFolder(Geary.Account account) {
-        base (account, new SearchFolderProperties(0, 0), new SearchFolderRoot());
-        
+
+
+    public SearchFolder(Geary.Account account, FolderRoot root) {
+        base(
+            account,
+            new SearchFolderProperties(0, 0),
+            root.get_child(MAGIC_BASENAME, Trillian.TRUE)
+        );
+
         account.folders_available_unavailable.connect(on_folders_available_unavailable);
         account.email_locally_complete.connect(on_email_locally_complete);
         account.email_removed.connect(on_account_email_removed);
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 52127abc..f246e5b6 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
@@ -75,6 +75,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
     }
 
     protected override SearchFolder new_search_folder() {
-        return new GmailSearchFolder(this);
+        return new GmailSearchFolder(this, this.local_folder_root);
     }
+
 }
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 9dea5b2a..ef47256d 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
@@ -12,8 +12,8 @@ private class Geary.ImapEngine.GmailSearchFolder : ImapDB.SearchFolder {
 
     private Geary.App.EmailStore email_store;
 
-    public GmailSearchFolder(Geary.Account account) {
-        base (account);
+    public GmailSearchFolder(Geary.Account account, FolderRoot root) {
+        base (account, root);
 
         this.email_store = new Geary.App.EmailStore(account);
     }
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 7276ca9f..48e0697c 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -1,6 +1,6 @@
 /*
  * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2017-2018 Michael Gratton <mike vee net>.
+ * Copyright 2017-2019 Michael Gratton <mike vee net>.
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
@@ -34,6 +34,14 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
     /** Local database for the account. */
     public ImapDB.Account local { get; private set; }
 
+    /**
+     * The root path for all local folders.
+     *
+     * No folder exists for this path, it merely exists to provide a
+     * common root for the paths of all local folders.
+     */
+    protected FolderRoot local_folder_root = new Geary.FolderRoot(true);
+
     private bool open = false;
     private Cancellable? open_cancellable = null;
     private Nonblocking.Semaphore? remote_ready_lock = null;
@@ -78,7 +86,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         );
         this.imap = imap;
 
-        smtp.outbox = new Outbox.Folder(this, local);
+        smtp.outbox = new Outbox.Folder(this, local_folder_root, local);
         smtp.email_sent.connect(on_email_sent);
         smtp.report_problem.connect(notify_report_problem);
         this.smtp = smtp;
@@ -139,10 +147,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
 
         // Create/load local folders
 
-        local_only.set(new Outbox.FolderRoot(), this.smtp.outbox);
+        local_only.set(this.smtp.outbox.path, this.smtp.outbox);
 
         this.search_folder = new_search_folder();
-        local_only.set(new ImapDB.SearchFolderRoot(), this.search_folder);
+        local_only.set(this.search_folder.path, this.search_folder);
 
         this.open = true;
         notify_opened();
@@ -300,7 +308,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         yield this.remote_ready_lock.wait_async(cancellable);
         Imap.ClientSession client =
             yield this.imap.claim_authorized_session_async(cancellable);
-        return new Imap.AccountSession(this.information.id, client);
+        return new Imap.AccountSession(
+            this.information.id, this.local.imap_folder_root, client
+        );
     }
 
     /**
@@ -350,7 +360,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         Imap.ClientSession? client =
             yield this.imap.claim_authorized_session_async(cancellable);
         Imap.AccountSession account = new Imap.AccountSession(
-            this.information.id, client
+            this.information.id, this.local.imap_folder_root, client
         );
 
         Imap.Folder? folder = null;
@@ -688,11 +698,15 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
                                                             Cancellable? cancellable)
         throws Error {
         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.
+        if (!remote.is_folder_path_valid(path)) {
+            debug("Ignoring bad special folder path '%s' for type %s",
+                  path.to_string(),
+                  special.to_string());
+            path = null;
+        }
+        if (path == null) {
+            debug("Guessing path for special folder type: %s",
+                  special.to_string());
             Geary.FolderPath root =
                 yield remote.get_default_personal_namespace(cancellable);
             Gee.List<string> search_names = special_search_names.get(special);
@@ -779,7 +793,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
      * override this to return the correct subclass.
      */
     protected virtual SearchFolder new_search_folder() {
-        return new ImapDB.SearchFolder(this);
+        return new ImapDB.SearchFolder(this, this.local_folder_root);
     }
 
     /** {@inheritDoc} */
@@ -1028,7 +1042,9 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation {
         GenericAccount generic = (GenericAccount) this.account;
         Gee.List<ImapDB.Folder> folders = new Gee.LinkedList<ImapDB.Folder>();
 
-        yield enumerate_local_folders_async(folders, null, cancellable);
+        yield enumerate_local_folders_async(
+            folders, generic.local.imap_folder_root, cancellable
+        );
         generic.add_folders(folders, true);
         if (!folders.is_empty) {
             // If we have some folders to load, then this isn't the
@@ -1039,7 +1055,7 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation {
     }
 
     private async void enumerate_local_folders_async(Gee.List<ImapDB.Folder> folders,
-                                                     Geary.FolderPath? parent,
+                                                     Geary.FolderPath parent,
                                                      Cancellable? cancellable)
         throws Error {
         Gee.Collection<ImapDB.Folder>? children = null;
@@ -1074,7 +1090,7 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation {
                     Geary.Folder target = yield generic.fetch_folder_async(path, cancellable);
                     specials.set(special, target);
                 } catch (Error err) {
-                    debug("%s: Previously used special folder %s does not exist: %s",
+                    debug("%s: Previously used special folder %s not loaded: %s",
                           generic.information.id, special.to_string(), err.message);
                 }
             }
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 b64c5afb..cf0d6bc0 100644
--- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
+++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
@@ -1,7 +1,9 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
@@ -36,11 +38,22 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
         if (special_map == null) {
             special_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
 
-            special_map.set(Imap.MailboxSpecifier.inbox.to_folder_path(null, null), 
Geary.SpecialFolderType.INBOX);
-            special_map.set(new Imap.FolderRoot("Sent"), Geary.SpecialFolderType.SENT);
-            special_map.set(new Imap.FolderRoot("Draft"), Geary.SpecialFolderType.DRAFTS);
-            special_map.set(new Imap.FolderRoot("Bulk Mail"), Geary.SpecialFolderType.SPAM);
-            special_map.set(new Imap.FolderRoot("Trash"), Geary.SpecialFolderType.TRASH);
+            FolderRoot root = this.local.imap_folder_root;
+            special_map.set(
+                this.local.imap_folder_root.inbox, Geary.SpecialFolderType.INBOX
+            );
+            special_map.set(
+                root.get_child("Sent"), Geary.SpecialFolderType.SENT
+            );
+            special_map.set(
+                root.get_child("Draft"), Geary.SpecialFolderType.DRAFTS
+            );
+            special_map.set(
+                root.get_child("Bulk Mail"), Geary.SpecialFolderType.SPAM
+            );
+            special_map.set(
+                root.get_child("Trash"), Geary.SpecialFolderType.TRASH
+            );
         }
     }
 
diff --git a/src/engine/imap/api/imap-account-session.vala b/src/engine/imap/api/imap-account-session.vala
index 99823f6c..99645990 100644
--- a/src/engine/imap/api/imap-account-session.vala
+++ b/src/engine/imap/api/imap-account-session.vala
@@ -23,6 +23,7 @@
  */
 internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
 
+    private FolderRoot root;
     private Gee.HashMap<FolderPath,Imap.Folder> folders =
         new Gee.HashMap<FolderPath,Imap.Folder>();
 
@@ -32,8 +33,10 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
 
 
     internal AccountSession(string account_id,
+                            FolderRoot root,
                             ClientSession session) {
         base("%s:account".printf(account_id), session);
+        this.root = root;
 
         session.list.connect(on_list_data);
         session.status.connect(on_status_data);
@@ -56,7 +59,26 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
             prefix = prefix.substring(0, prefix.length - delim.length);
         }
 
-        return new FolderRoot(prefix);
+        return Geary.String.is_empty(prefix)
+            ? this.root
+            : this.root.get_child(prefix);
+    }
+
+    /**
+     * Determines if the given folder path appears to a valid mailbox.
+     */
+    public bool is_folder_path_valid(FolderPath? path) throws GLib.Error {
+        bool is_valid = false;
+        if (path != null) {
+            ClientSession session = claim_session();
+            try {
+                session.get_mailbox_for_path(path);
+                is_valid = true;
+            } catch (GLib.Error err) {
+                // still not valid
+            }
+        }
+        return is_valid;
     }
 
     /**
@@ -172,7 +194,9 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
                 // Mailbox is unselectable, so doesn't need a STATUS,
                 // so we can create it now if it does not already
                 // exist
-                FolderPath path = session.get_path_for_mailbox(mailbox_info.mailbox);
+                FolderPath path = session.get_path_for_mailbox(
+                    this.root, mailbox_info.mailbox
+                );
                 Folder? child = this.folders.get(path);
                 if (child == null) {
                     child = new Imap.Folder(
@@ -223,7 +247,9 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
                 }
                 status_results.remove(status);
 
-                FolderPath child_path = session.get_path_for_mailbox(mailbox_info.mailbox);
+                FolderPath child_path = session.get_path_for_mailbox(
+                    this.root, mailbox_info.mailbox
+                );
                 Imap.Folder? child = this.folders.get(child_path);
 
                 if (child != null) {
@@ -314,7 +340,9 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
         if (folder != null && list_children) {
             Gee.Iterator<MailboxInformation> iter = list_results.iterator();
             while (iter.next()) {
-                FolderPath list_path = session.get_path_for_mailbox(iter.get().mailbox);
+                FolderPath list_path = session.get_path_for_mailbox(
+                    this.root, iter.get().mailbox
+                );
                 if (list_path.equal_to(folder)) {
                     debug("Removing parent from LIST results: %s", list_path.to_string());
                     iter.remove();
diff --git a/src/engine/imap/api/imap-folder-root.vala b/src/engine/imap/api/imap-folder-root.vala
index e19a4a1f..0f8a39ee 100644
--- a/src/engine/imap/api/imap-folder-root.vala
+++ b/src/engine/imap/api/imap-folder-root.vala
@@ -1,40 +1,55 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
  *
  * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 /**
  * The root of all IMAP mailbox paths.
  *
- * Because IMAP has peculiar requirements about its mailbox paths (in particular, Inbox is
- * guaranteed at the root and is named case-insensitive, and that delimiters are particular to
- * each path), this class ensure certain requirements are held throughout the library.
+ * Because IMAP has peculiar requirements about its mailbox paths (in
+ * particular, Inbox is guaranteed at the root and is named
+ * case-insensitive, and that delimiters are particular to each path),
+ * this class ensure certain requirements are held throughout the
+ * library.
  */
+public class Geary.Imap.FolderRoot : Geary.FolderRoot {
 
-private class Geary.Imap.FolderRoot : Geary.FolderRoot {
-    public bool is_inbox { get; private set; }
-    
-    public FolderRoot(string basename) {
-        bool init_is_inbox;
-        string normalized_basename = init(basename, out init_is_inbox);
-        
-        base (normalized_basename, !init_is_inbox, true);
-        
-        is_inbox = init_is_inbox;
+
+    /**
+     * The canonical path for the IMAP inbox.
+     *
+     * This specific path object will always be returned when a child
+     * with some case-insensitive version of the IMAP inbox mailbox is
+     * obtained via {@link get_child} from this root folder. However
+     * since multiple folder roots may be constructed, in general
+     * {@link FolderPath.equal_to} or {@link FolderPath.compare_to}
+     * should still be used for testing equality with this path.
+     */
+    public FolderPath inbox { get; private set; }
+
+
+    public FolderRoot() {
+        base(false);
+        this.inbox = base.get_child(
+            MailboxSpecifier.CANONICAL_INBOX_NAME,
+            Trillian.FALSE
+        );
     }
-    
-    // This is the magic that ensures the canonical IMAP Inbox name is used throughout the engine
-    private static string init(string basename, out bool is_inbox) {
-        if (MailboxSpecifier.is_inbox_name(basename)) {
-            is_inbox = true;
-            
-            return MailboxSpecifier.CANONICAL_INBOX_NAME;
-        }
-        
-        is_inbox = false;
-        
-        return basename;
+
+    /**
+     * Creates a path that is a child of this folder.
+     *
+     * If the given basename is that of the IMAP inbox, then {@link
+     * inbox} will be returned.
+     */
+    public override
+        FolderPath get_child(string basename,
+                             Trillian is_case_sensitive = Trillian.UNKNOWN) {
+        return (MailboxSpecifier.is_inbox_name(basename))
+            ? this.inbox
+            : base.get_child(basename, is_case_sensitive);
     }
-}
 
+}
diff --git a/src/engine/imap/message/imap-mailbox-specifier.vala 
b/src/engine/imap/message/imap-mailbox-specifier.vala
index 1984338b..ad6f638e 100644
--- a/src/engine/imap/message/imap-mailbox-specifier.vala
+++ b/src/engine/imap/message/imap-mailbox-specifier.vala
@@ -84,9 +84,9 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
      * Returns true if the {@link Geary.FolderPath} points to the IMAP Inbox.
      */
     public static bool folder_path_is_inbox(FolderPath path) {
-        return path.is_root() && is_inbox_name(path.basename);
+        return path.is_top_level && is_inbox_name(path.basename);
     }
-    
+
     /**
      * Returns true if the string is the name of the IMAP Inbox.
      *
@@ -115,30 +115,50 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
     public static bool is_canonical_inbox_name(string name) {
         return Ascii.str_equal(name, CANONICAL_INBOX_NAME);
     }
-    
+
     /**
      * Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
      */
-    public MailboxSpecifier.from_folder_path(FolderPath path, MailboxSpecifier inbox, string? delim)
-    throws ImapError {
+    public MailboxSpecifier.from_folder_path(FolderPath path,
+                                             MailboxSpecifier inbox,
+                                             string? delim)
+        throws ImapError {
+        if (path.is_root) {
+            throw new ImapError.INVALID(
+                "Cannot convert root path into a mailbox"
+            );
+        }
+
         Gee.List<string> parts = path.as_list();
+        // Don't include the root so that mailboxes do not begin with
+        // the delim.
+        parts.remove_at(0);
+
         if (parts.size > 1 && delim == null) {
-            // XXX not quite right
-            throw new ImapError.INVALID("Path has more than one part but no delimiter given");
+            throw new ImapError.INVALID(
+                "Path has more than one part but no delimiter given"
+            );
         }
 
-        // Don't include the root if it is an empty string so that
-        // mailboxes do not begin with the delim.
-        if (parts.size > 1 && parts[0] == "") {
-            parts.remove_at(0);
+        if (String.is_empty_or_whitespace(parts[0])) {
+            throw new ImapError.INVALID(
+                "Path contains empty base part: '%s'", path.to_string()
+            );
         }
 
         StringBuilder builder = new StringBuilder(
-            is_inbox_name(parts[0]) ? inbox.name : parts[0]);
+            is_inbox_name(parts[0]) ? inbox.name : parts[0]
+        );
 
         for (int i = 1; i < parts.size; i++) {
+            string basename = parts[i];
+            if (String.is_empty_or_whitespace(basename)) {
+                throw new ImapError.INVALID(
+                    "Path contains empty part: '%s'", path.to_string()
+                );
+            }
             builder.append(delim);
-            builder.append(parts[i]);
+            builder.append(basename);
         }
 
         init(builder.str);
@@ -156,7 +176,7 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
      * the name is returned as a single element.
      */
     public Gee.List<string> to_list(string? delim) {
-        Gee.List<string> path = new Gee.ArrayList<string>();
+        Gee.List<string> path = new Gee.LinkedList<string>();
         
         if (!String.is_empty(delim)) {
             string[] split = name.split(delim);
@@ -171,33 +191,34 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
         
         return path;
     }
-    
+
     /**
-     * Converts the {@link MailboxSpecifier} into a {@link FolderPath}.
+     * Converts the mailbox into a folder path.
      *
-     * If the inbox_specifier is supplied, if the root element matches it, the canonical Inbox
-     * name is used in its place.  This is useful for XLIST where that command returns a translated
-     * name but the standard IMAP name ("INBOX") must be used in addressing its children.
+     * If the inbox_specifier is supplied and the first element
+     * matches it, the canonical Inbox name is used in its place.
+     * This is useful for XLIST where that command returns a
+     * translated name but the standard IMAP name ("INBOX") must be
+     * used in addressing its children.
      */
-    public FolderPath to_folder_path(string? delim, MailboxSpecifier? inbox_specifier) {
-        // convert path to list of elements
+    public FolderPath to_folder_path(FolderRoot root,
+                                     string? delim,
+                                     MailboxSpecifier? inbox_specifier) {
         Gee.List<string> list = to_list(delim);
-        
-        // if root element is same as supplied inbox specifier, use canonical inbox name, otherwise
-        // keep
-        FolderPath path;
-        if (inbox_specifier != null && list[0] == inbox_specifier.name)
-            path = new Imap.FolderRoot(CANONICAL_INBOX_NAME);
-        else
-            path = new Imap.FolderRoot(list[0]);
-        
-        // walk down rest of elements adding as we go
-        for (int ctr = 1; ctr < list.size; ctr++)
-            path = path.get_child(list[ctr]);
-        
+
+        // If the first element is same as supplied inbox specifier,
+        // use canonical inbox name, otherwise keep
+        FolderPath? path =
+            (inbox_specifier != null && list[0] == inbox_specifier.name)
+            ? root.get_child(CANONICAL_INBOX_NAME)
+            : root.get_child(list[0]);
+
+        foreach (string name in list) {
+            path = path.get_child(name);
+        }
         return path;
     }
-    
+
     /**
      * The mailbox's name without parent folders.
      *
diff --git a/src/engine/imap/response/imap-mailbox-information.vala 
b/src/engine/imap/response/imap-mailbox-information.vala
index 6d8c3a0c..0d00324f 100644
--- a/src/engine/imap/response/imap-mailbox-information.vala
+++ b/src/engine/imap/response/imap-mailbox-information.vala
@@ -86,19 +86,8 @@ public class Geary.Imap.MailboxInformation : BaseObject {
         );
     }
 
-    /**
-     * The {@link Geary.FolderPath} for the {@link mailbox}.
-     *
-     * This is constructed from the supplied {@link mailbox} and {@link delim} returned from the
-     * server.  If the mailbox is the same as the supplied inbox_specifier, a canonical name for
-     * the Inbox is returned.
-     */
-    public Geary.FolderPath get_path(MailboxSpecifier? inbox_specifier) {
-        return mailbox.to_folder_path(delim, inbox_specifier);
-    }
-    
     public string to_string() {
         return "%s/%s".printf(mailbox.to_string(), attrs.to_string());
     }
-}
 
+}
diff --git a/src/engine/imap/transport/imap-client-session.vala 
b/src/engine/imap/transport/imap-client-session.vala
index 4613a0d6..0e7910ba 100644
--- a/src/engine/imap/transport/imap-client-session.vala
+++ b/src/engine/imap/transport/imap-client-session.vala
@@ -509,7 +509,7 @@ public class Geary.Imap.ClientSession : BaseObject {
      * Determines the SELECT-able mailbox name for a specific folder path.
      */
     public MailboxSpecifier get_mailbox_for_path(FolderPath path)
-    throws ImapError {
+        throws ImapError {
         string? delim = get_delimiter_for_path(path);
         return new MailboxSpecifier.from_folder_path(path, this.inbox.mailbox, delim);
     }
@@ -517,10 +517,11 @@ public class Geary.Imap.ClientSession : BaseObject {
     /**
      * Determines the folder path for a mailbox name.
      */
-    public FolderPath get_path_for_mailbox(MailboxSpecifier mailbox)
-    throws ImapError {
+    public FolderPath get_path_for_mailbox(FolderRoot root,
+                                           MailboxSpecifier mailbox)
+        throws ImapError {
         string? delim = get_delimiter_for_mailbox(mailbox);
-        return mailbox.to_folder_path(delim, this.inbox.mailbox);
+        return mailbox.to_folder_path(root, delim, this.inbox.mailbox);
     }
 
     /**
diff --git a/src/engine/meson.build b/src/engine/meson.build
index 85d256a5..067a400d 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -181,7 +181,6 @@ geary_engine_vala_sources = files(
   'imap-db/search/imap-db-search-email-identifier.vala',
   'imap-db/search/imap-db-search-folder.vala',
   'imap-db/search/imap-db-search-folder-properties.vala',
-  'imap-db/search/imap-db-search-folder-root.vala',
   'imap-db/search/imap-db-search-query.vala',
   'imap-db/search/imap-db-search-term.vala',
 
@@ -264,7 +263,6 @@ geary_engine_vala_sources = files(
   'outbox/outbox-email-properties.vala',
   'outbox/outbox-folder.vala',
   'outbox/outbox-folder-properties.vala',
-  'outbox/outbox-folder-root.vala',
 
   'rfc822/rfc822.vala',
   'rfc822/rfc822-error.vala',
diff --git a/src/engine/outbox/outbox-folder.vala b/src/engine/outbox/outbox-folder.vala
index cf8be4a0..6f72194f 100644
--- a/src/engine/outbox/outbox-folder.vala
+++ b/src/engine/outbox/outbox-folder.vala
@@ -16,6 +16,10 @@ private class Geary.Outbox.Folder :
     Geary.FolderSupport.Remove {
 
 
+    /** The canonical name of the outbox folder. */
+    public const string MAGIC_BASENAME = "$GearyOutbox$";
+
+
     private class OutboxRow {
         public int64 id;
         public int position;
@@ -38,19 +42,32 @@ private class Geary.Outbox.Folder :
     }
 
 
+    /** {@inheritDoc} */
     public override Account account { get { return this._account; } }
 
+    /** {@inheritDoc} */
     public override Geary.FolderProperties properties {
         get { return _properties; }
     }
 
-    private FolderRoot _path = new FolderRoot();
+    /**
+     * Returns the path to this folder.
+     *
+     * This is always the child of the root given to the constructor,
+     * with the name given by @{link MAGIC_BASENAME}.
+     */
     public override FolderPath path {
         get {
             return _path;
         }
     }
+    private FolderPath _path;
 
+    /**
+     * Returns the type of this folder.
+     *
+     * This is always {@link Geary.SpecialFolderType.OUTBOX}
+     */
     public override SpecialFolderType special_folder_type {
         get {
             return Geary.SpecialFolderType.OUTBOX;
@@ -66,8 +83,9 @@ private class Geary.Outbox.Folder :
 
     // Requires the Database from the get-go because it runs a background task that access it
     // whether open or not
-    public Folder(Account account, ImapDB.Account local) {
+    public Folder(Account account, FolderRoot root, ImapDB.Account local) {
         this._account = account;
+        this._path = root.get_child(MAGIC_BASENAME, Trillian.TRUE);
         this.local = local;
     }
 
diff --git a/test/engine/api/geary-folder-path-test.vala b/test/engine/api/geary-folder-path-test.vala
new file mode 100644
index 00000000..9cce3dbd
--- /dev/null
+++ b/test/engine/api/geary-folder-path-test.vala
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class Geary.FolderPathTest : TestCase {
+
+    public FolderPathTest() {
+        base("Geary.FolderPathTest");
+        add_test("get_child_from_root", get_child_from_root);
+        add_test("get_child_from_child", get_child_from_child);
+        add_test("root_is_root", root_is_root);
+        add_test("child_is_not_root", root_is_root);
+    }
+
+    public void get_child_from_root() throws GLib.Error {
+        assert_string(
+            ">test",
+            new Geary.FolderRoot(false).get_child("test").to_string()
+        );
+    }
+
+    public void get_child_from_child() throws GLib.Error {
+        assert_string(
+            ">test1>test2",
+            new Geary.FolderRoot(false)
+            .get_child("test1").get_child("test2").to_string()
+        );
+    }
+
+    public void root_is_root() throws GLib.Error {
+        assert_true(new Geary.FolderRoot(false).is_root);
+    }
+
+    public void child_root_is_not_root() throws GLib.Error {
+        assert_false(new Geary.FolderRoot(false).get_child("test").is_root);
+    }
+
+}
diff --git a/test/engine/app/app-conversation-monitor-test.vala 
b/test/engine/app/app-conversation-monitor-test.vala
index 8f3c2a7e..f65720ec 100644
--- a/test/engine/app/app-conversation-monitor-test.vala
+++ b/test/engine/app/app-conversation-monitor-test.vala
@@ -11,6 +11,7 @@ class Geary.App.ConversationMonitorTest : TestCase {
 
     AccountInformation? account_info = null;
     MockAccount? account = null;
+    FolderRoot? folder_root = null;
     MockFolder? base_folder = null;
     MockFolder? other_folder = null;
 
@@ -35,22 +36,31 @@ class Geary.App.ConversationMonitorTest : TestCase {
             new RFC822.MailboxAddress(null, "test1 example com")
         );
         this.account = new MockAccount(this.account_info);
+        this.folder_root = new FolderRoot(false);
         this.base_folder = new MockFolder(
             this.account,
             null,
-            new MockFolderRoot("base"),
+            this.folder_root.get_child("base"),
             SpecialFolderType.NONE,
             null
         );
         this.other_folder = new MockFolder(
             this.account,
             null,
-            new MockFolderRoot("other"),
+            this.folder_root.get_child("other"),
             SpecialFolderType.NONE,
             null
         );
     }
 
+    public override void tear_down() {
+        this.other_folder = null;
+        this.base_folder = null;
+        this.folder_root = null;
+        this.account_info = null;
+        this.account = null;
+    }
+
     public void start_stop_monitoring() throws Error {
         ConversationMonitor monitor = new ConversationMonitor(
             this.base_folder, Folder.OpenFlags.NONE, Email.Field.NONE, 10
diff --git a/test/engine/app/app-conversation-set-test.vala b/test/engine/app/app-conversation-set-test.vala
index 1bc34210..a662e9e3 100644
--- a/test/engine/app/app-conversation-set-test.vala
+++ b/test/engine/app/app-conversation-set-test.vala
@@ -9,6 +9,7 @@ class Geary.App.ConversationSetTest : TestCase {
 
 
     ConversationSet? test = null;
+    FolderRoot? folder_root = null;
     Folder? base_folder = null;
 
     public ConversationSetTest() {
@@ -26,14 +27,21 @@ class Geary.App.ConversationSetTest : TestCase {
     }
 
     public override void set_up() {
-        this.test = new ConversationSet();
+        this.folder_root = new FolderRoot(false);
         this.base_folder = new MockFolder(
             null,
             null,
-            new MockFolderRoot("test"),
+            this.folder_root.get_child("test"),
             SpecialFolderType.NONE,
             null
         );
+        this.test = new ConversationSet();
+    }
+
+    public override void tear_down() {
+        this.test = null;
+        this.folder_root = null;
+        this.base_folder = null;
     }
 
     public void add_all_basic() throws Error {
@@ -144,7 +152,7 @@ class Geary.App.ConversationSetTest : TestCase {
         Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath> email_paths =
             new Gee.HashMultiMap<Geary.EmailIdentifier, Geary.FolderPath>();
         email_paths.set(e1.id, this.base_folder.path);
-        email_paths.set(e2.id, new MockFolderRoot("other"));
+        email_paths.set(e2.id, this.folder_root.get_child("other"));
 
         Gee.Collection<Conversation>? added = null;
         Gee.MultiMap<Conversation,Email>? appended = null;
@@ -310,7 +318,7 @@ class Geary.App.ConversationSetTest : TestCase {
 
     public void add_all_multi_path() throws Error {
         Email e1 = setup_email(1);
-        MockFolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
 
         Gee.LinkedList<Email> emails = new Gee.LinkedList<Email>();
         emails.add(e1);
@@ -340,7 +348,7 @@ class Geary.App.ConversationSetTest : TestCase {
         Email e1 = setup_email(1);
         add_email_to_test_set(e1);
 
-        MockFolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
 
         Gee.LinkedList<Email> emails = new Gee.LinkedList<Email>();
         emails.add(e1);
@@ -426,7 +434,7 @@ class Geary.App.ConversationSetTest : TestCase {
     }
 
     public void remove_all_remove_path() throws Error {
-        MockFolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
         Email e1 = setup_email(1);
         add_email_to_test_set(e1, other_path);
 
diff --git a/test/engine/app/app-conversation-test.vala b/test/engine/app/app-conversation-test.vala
index d3f3d429..709d88ca 100644
--- a/test/engine/app/app-conversation-test.vala
+++ b/test/engine/app/app-conversation-test.vala
@@ -10,6 +10,8 @@ class Geary.App.ConversationTest : TestCase {
 
     Conversation? test = null;
     Folder? base_folder = null;
+    FolderRoot? folder_root = null;
+
 
     public ConversationTest() {
         base("Geary.App.ConversationTest");
@@ -24,16 +26,23 @@ class Geary.App.ConversationTest : TestCase {
     }
 
     public override void set_up() {
+        this.folder_root = new FolderRoot(false);
         this.base_folder = new MockFolder(
             null,
             null,
-            new MockFolderRoot("test"),
+            this.folder_root.get_child("test"),
             SpecialFolderType.NONE,
             null
         );
         this.test = new Conversation(this.base_folder);
     }
 
+    public override void tear_down() {
+        this.test = null;
+        this.folder_root = null;
+        this.base_folder = null;
+    }
+
     public void add_basic() throws Error {
         Geary.Email e1 = setup_email(1);
         Geary.Email e2 = setup_email(2);
@@ -78,8 +87,8 @@ class Geary.App.ConversationTest : TestCase {
         Geary.Email e2 = setup_email(2);
         this.test.add(e2, singleton(this.base_folder.path));
 
-        FolderRoot other_path = new MockFolderRoot("other");
-        Gee.LinkedList<FolderRoot> other_paths = new Gee.LinkedList<FolderRoot>();
+        FolderPath other_path = this.folder_root.get_child("other");
+        Gee.LinkedList<FolderPath> other_paths = new Gee.LinkedList<FolderPath>();
         other_paths.add(other_path);
 
         assert(this.test.add(e1, other_paths) == false);
@@ -145,7 +154,7 @@ class Geary.App.ConversationTest : TestCase {
         Geary.Email e1 = setup_email(1);
         this.test.add(e1, singleton(this.base_folder.path));
 
-        FolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
         Geary.Email e2 = setup_email(2);
         this.test.add(e2, singleton(other_path));
 
@@ -158,7 +167,7 @@ class Geary.App.ConversationTest : TestCase {
         Geary.Email e1 = setup_email(1);
         this.test.add(e1, singleton(this.base_folder.path));
 
-        FolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
         Geary.Email e2 = setup_email(2);
         this.test.add(e2, singleton(other_path));
 
@@ -193,7 +202,7 @@ class Geary.App.ConversationTest : TestCase {
         Geary.Email e1 = setup_email(1);
         this.test.add(e1, singleton(this.base_folder.path));
 
-        FolderRoot other_path = new MockFolderRoot("other");
+        FolderPath other_path = this.folder_root.get_child("other");
         Geary.Email e2 = setup_email(2);
         this.test.add(e2, singleton(other_path));
 
diff --git a/test/engine/imap-db/imap-db-account-test.vala b/test/engine/imap-db/imap-db-account-test.vala
index 146d7127..27c33de8 100644
--- a/test/engine/imap-db/imap-db-account-test.vala
+++ b/test/engine/imap-db/imap-db-account-test.vala
@@ -12,6 +12,7 @@ class Geary.ImapDB.AccountTest : TestCase {
     private GLib.File? tmp_dir = null;
     private Geary.AccountInformation? config = null;
     private Account? account = null;
+    private FolderRoot? root = null;
 
 
     public AccountTest() {
@@ -47,9 +48,12 @@ class Geary.ImapDB.AccountTest : TestCase {
             (obj, ret) => { async_complete(ret); }
         );
         this.account.open_async.end(async_result());
+
+        this.root = new FolderRoot(false);
     }
 
     public override void tear_down() throws GLib.Error {
+        this.root = null;
         this.account.close_async.begin(
             null,
             (obj, ret) => { async_complete(ret); }
@@ -62,7 +66,7 @@ class Geary.ImapDB.AccountTest : TestCase {
 
     public void create_base_folder() throws GLib.Error {
         Imap.Folder folder = new Imap.Folder(
-            new Imap.FolderRoot("test"),
+            this.root.get_child("test"),
             new Imap.FolderProperties.selectable(
                 new Imap.MailboxAttributes(
                     Gee.Collection.empty<Geary.Imap.MailboxAttribute>()
@@ -101,7 +105,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         );
 
         Imap.Folder folder = new Imap.Folder(
-            new Imap.FolderRoot("test").get_child("child"),
+            this.root.get_child("test").get_child("child"),
             new Imap.FolderProperties.selectable(
                 new Imap.MailboxAttributes(
                     Gee.Collection.empty<Geary.Imap.MailboxAttribute>()
@@ -144,7 +148,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.list_folders_async.begin(
-            null,
+            this.account.imap_folder_root,
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -187,14 +191,14 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.delete_folder_async.begin(
-            new Imap.FolderRoot("test1").get_child("test2"),
+            this.root.get_child("test1").get_child("test2"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
         this.account.delete_folder_async.end(async_result());
 
         this.account.delete_folder_async.begin(
-            new Imap.FolderRoot("test1"),
+            this.root.get_child("test1"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -210,7 +214,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.delete_folder_async.begin(
-            new Imap.FolderRoot("test1"),
+            this.root.get_child("test1"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -231,7 +235,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.delete_folder_async.begin(
-            new Imap.FolderRoot("test3"),
+            this.root.get_child("test3"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -252,7 +256,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.fetch_folder_async.begin(
-            new Imap.FolderRoot("test1"),
+            this.root.get_child("test1"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -271,7 +275,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.fetch_folder_async.begin(
-            new Imap.FolderRoot("test1").get_child("test2"),
+            this.root.get_child("test1").get_child("test2"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
@@ -290,7 +294,7 @@ class Geary.ImapDB.AccountTest : TestCase {
         """);
 
         this.account.fetch_folder_async.begin(
-            new Imap.FolderRoot("test3"),
+            this.root.get_child("test3"),
             null,
             (obj, ret) => { async_complete(ret); }
         );
diff --git a/test/engine/imap/message/imap-mailbox-specifier-test.vala 
b/test/engine/imap/message/imap-mailbox-specifier-test.vala
index 741f279e..6488e5e9 100644
--- a/test/engine/imap/message/imap-mailbox-specifier-test.vala
+++ b/test/engine/imap/message/imap-mailbox-specifier-test.vala
@@ -13,6 +13,7 @@ class Geary.Imap.MailboxSpecifierTest : TestCase {
         add_test("to_parameter", to_parameter);
         add_test("from_parameter", from_parameter);
         add_test("from_folder_path", from_folder_path);
+        add_test("folder_path_is_inbox", folder_path_is_inbox);
     }
 
     public void to_parameter() throws Error {
@@ -59,54 +60,82 @@ class Geary.Imap.MailboxSpecifierTest : TestCase {
     }
 
     public void from_folder_path() throws Error {
-        MockFolderRoot empty_root = new MockFolderRoot("");
-        MailboxSpecifier empty_inbox = new MailboxSpecifier("Inbox");
+        FolderRoot root = new FolderRoot();
+        MailboxSpecifier inbox = new MailboxSpecifier("Inbox");
         assert_string(
             "Foo",
             new MailboxSpecifier.from_folder_path(
-                empty_root.get_child("Foo"), empty_inbox, "$"
+                root.get_child("Foo"), inbox, "$"
             ).name
         );
         assert_string(
             "Foo$Bar",
             new MailboxSpecifier.from_folder_path(
-                empty_root.get_child("Foo").get_child("Bar"), empty_inbox, "$"
+                root.get_child("Foo").get_child("Bar"), inbox, "$"
             ).name
         );
         assert_string(
             "Inbox",
             new MailboxSpecifier.from_folder_path(
-                empty_root.get_child(MailboxSpecifier.CANONICAL_INBOX_NAME),
-                empty_inbox,
+                root.get_child(MailboxSpecifier.CANONICAL_INBOX_NAME),
+                inbox,
                 "$"
             ).name
         );
 
-        MockFolderRoot non_empty_root = new MockFolderRoot("Root");
-        MailboxSpecifier non_empty_inbox = new MailboxSpecifier("Inbox");
-        assert_string(
-            "Root$Foo",
+        try {
             new MailboxSpecifier.from_folder_path(
-                non_empty_root.get_child("Foo"),
-                non_empty_inbox,
-                "$"
-            ).name
-        );
-        assert_string(
-            "Root$Foo$Bar",
+                root.get_child(""), inbox, "$"
+            );
+            assert_not_reached();
+        } catch (GLib.Error err) {
+            // all good
+        }
+
+        try {
             new MailboxSpecifier.from_folder_path(
-                non_empty_root.get_child("Foo").get_child("Bar"),
-                non_empty_inbox,
-                "$"
-            ).name
+                root.get_child("test").get_child(""), inbox, "$"
+            );
+            assert_not_reached();
+        } catch (GLib.Error err) {
+            // all good
+        }
+
+        try {
+            new MailboxSpecifier.from_folder_path(root, inbox, "$");
+            assert_not_reached();
+        } catch (GLib.Error err) {
+            // all good
+        }
+    }
+
+    public void folder_path_is_inbox() throws GLib.Error {
+        FolderRoot root = new FolderRoot();
+        assert_true(
+            MailboxSpecifier.folder_path_is_inbox(root.get_child("Inbox"))
         );
-        assert_string(
-            "Root$INBOX",
-            new MailboxSpecifier.from_folder_path(
-                non_empty_root.get_child(MailboxSpecifier.CANONICAL_INBOX_NAME),
-                non_empty_inbox,
-                "$"
-            ).name
+        assert_true(
+            MailboxSpecifier.folder_path_is_inbox(root.get_child("inbox"))
+        );
+        assert_true(
+            MailboxSpecifier.folder_path_is_inbox(root.get_child("INBOX"))
+        );
+
+        assert_false(
+            MailboxSpecifier.folder_path_is_inbox(root)
+        );
+        assert_false(
+            MailboxSpecifier.folder_path_is_inbox(root.get_child("blah"))
+        );
+        assert_false(
+            MailboxSpecifier.folder_path_is_inbox(
+                root.get_child("blah").get_child("Inbox")
+            )
+        );
+        assert_false(
+            MailboxSpecifier.folder_path_is_inbox(
+                root.get_child("Inbox").get_child("Inbox")
+            )
         );
     }
 
diff --git a/test/meson.build b/test/meson.build
index c8f45530..26eed436 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -18,11 +18,11 @@ geary_test_engine_sources = [
   'engine/api/geary-email-identifier-mock.vala',
   'engine/api/geary-email-properties-mock.vala',
   'engine/api/geary-folder-mock.vala',
-  'engine/api/geary-folder-path-mock.vala',
 
   'engine/api/geary-account-information-test.vala',
   'engine/api/geary-attachment-test.vala',
   'engine/api/geary-engine-test.vala',
+  'engine/api/geary-folder-path-test.vala',
   'engine/api/geary-service-information-test.vala',
   'engine/app/app-conversation-test.vala',
   'engine/app/app-conversation-monitor-test.vala',
diff --git a/test/test-engine.vala b/test/test-engine.vala
index 3c2309d8..a520c379 100644
--- a/test/test-engine.vala
+++ b/test/test-engine.vala
@@ -25,6 +25,7 @@ int main(string[] args) {
     engine.add_suite(new Geary.AccountInformationTest().get_suite());
     engine.add_suite(new Geary.AttachmentTest().get_suite());
     engine.add_suite(new Geary.EngineTest().get_suite());
+    engine.add_suite(new Geary.FolderPathTest().get_suite());
     engine.add_suite(new Geary.IdleManagerTest().get_suite());
     engine.add_suite(new Geary.TimeoutManagerTest().get_suite());
     engine.add_suite(new Geary.TlsNegotiationMethodTest().get_suite());


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