[geary/wip/768422-namespace-support: 6/6] Use Imap.ClientSession for mapping between folder and mailbox names.



commit cac73f41cf402fe6a4f27a5c82fbbc5153690600
Author: Michael James Gratton <mike vee net>
Date:   Thu Nov 2 18:05:54 2017 +1100

    Use Imap.ClientSession for mapping between folder and mailbox names.
    
    Now that ClientSession is storing per-connection inbox and namespace
    data, we want to always consult with the account or folder's session to
    determine the correct Imap.MailboxSpecifier name for a high-level
    Geary.FolderPath, and vice versa.
    
    This is enforced by removing all inbox and delim information from Imap.Accout
    and Imap.Folder, and providing methods to translate between
    MailboxSpecifier and FolderPath, then fixing up the fallout.
    
    * src/engine/imap/transport/imap-client-session.vala (ClientSession): Add
      methods for obtaining the delimiter and converting between foler paths
      and mailbox names, based on information obtainined from the IMAP server
      for this connection.
    
    * src/engine/imap/api/imap-account.vala (Account): Remove
      path_to_mailbox, inbox_specifier, and hierarchy_delimiter object
      attributes since these are connection specific. Convert from caching
      all mailbox specifiers retreived from the server and some Imap.Folder
      instances to simply cache all Folder instances. Substantially rework
      the public API implementation as below to support this and update all
      call sites.
      (Account::folder_exists_async): Actually hit the server rather than
      justing looking at the cache to determine if the folder exists, remove
      a folder from the cache if not.
      (Account::fetch_folder_async): Renamed from
      "fetch_unrecycled_folder_async" so as to be a bit less obtuse, updated
      call sites.
      (Account::fetch_folder_cached_async): Renamed from fetch_folder_async
      to be a bit more obvious. Substantially simplified, deleates to
      fetch_folder_async() rather than list_children_async() method, so we
      don't need list all children of its parent and hence to do a status for
      each. Rather than passing a "created" flag out and forcing call sites
      to then do a second call to update message counts, just simply take a
      boolean param indicating if we should do it here.
      (Account::list_child_folders_async): Simplified a bit, updated to
      create and cache an Imap.Folder for each child.
      (Account::fetch_counts_async): Removed and merged into
      fetch_folder_cached_async() since the only call site was replaced by
      the new refresh_counts param on that method.
      (Account::send_list_async, send_status_async): renamed from
      list_children_async and fetch_status_async, to make it obvious about
      what they are actually doing. Added an instance of ClientSession as
      first param so they don't have to re-claim the same session.
      (Account::send_command_async, send_multiple_async): Also add an
      instance of ClientSession as first param so they don't have to re-claim
      a session after the public methods calling them have already done so.
    
    * src/engine/imap/api/imap-folder.vala (Folder): As for Imap.Account,
      remove mailbox info delim (these were redundant anyway) and replace
      uses on the folder's client session for converting between folder paths
      and mailbox names.
    
    * src/engine/api/geary-folder-path.vala (FolderPath): Remove
      get_fullpath(), since having it would only encourage API users to do
      the wrong thing.
    
    * src/engine/imap/message/imap-mailbox-specifier.vala
      (MailboxSpecifier::from_folder_path): Ensure the inbox name is also
      passed in and manually construct a mailbox name from a FolderPath so
      the connection's actual inbox name ca be substituted in.

 src/engine/api/geary-folder-path.vala              |   52 +-
 src/engine/imap-db/imap-db-folder.vala             |   15 +-
 .../imap-engine/imap-engine-generic-account.vala   |   47 +-
 .../imap-engine/imap-engine-minimal-folder.vala    |   36 +-
 src/engine/imap/api/imap-account.vala              |  685 ++++++++++----------
 src/engine/imap/api/imap-folder.vala               |   43 +-
 .../imap/message/imap-mailbox-specifier.vala       |   21 +-
 src/engine/imap/transport/imap-client-session.vala |   68 ++-
 8 files changed, 524 insertions(+), 443 deletions(-)
---
diff --git a/src/engine/api/geary-folder-path.vala b/src/engine/api/geary-folder-path.vala
index 9752f21..7f0bf7b 100644
--- a/src/engine/api/geary-folder-path.vala
+++ b/src/engine/api/geary-folder-path.vala
@@ -26,10 +26,8 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
      * This has implications, as {@link FolderPath} is Comparable and Hashable.
      */
     public bool case_sensitive { get; private set; }
-    
+
     private Gee.List<Geary.FolderPath>? path = null;
-    private string? fullpath = null;
-    private string? fullpath_separator = null;
     private uint stored_hash = uint.MAX;
     
     protected FolderPath(string basename, bool case_sensitive) {
@@ -165,35 +163,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
         
         return false;
     }
-    
-    /**
-     * Returns the {@link FolderPath} as a single string with the supplied separator used as a
-     * delimiter.
-     *
-     * The separator is not appended to the fullpath.
-     */
-    public string get_fullpath(string separator) {
-        // use cached copy if the stars align
-        if (fullpath != null && fullpath_separator == separator)
-            return fullpath;
-        
-        StringBuilder builder = new StringBuilder();
-        
-        if (path != null) {
-            foreach (Geary.FolderPath folder in path) {
-                builder.append(folder.basename);
-                builder.append(separator);
-            }
-        }
-        
-        builder.append(basename);
-        
-        fullpath = builder.str;
-        fullpath_separator = separator;
-        
-        return fullpath;
-    }
-    
+
     private uint get_basename_hash() {
         return case_sensitive ? str_hash(basename) : str_hash(basename.down());
     }
@@ -303,14 +273,24 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
         
         return true;
     }
-    
+
     /**
-     * Returns the fullpath using the default separator.
+     * Returns a string version of the path using a default separator.
      *
-     * Use only for debugging and logging.
+     * Do not use this for obtaining an IMAP mailbox name to send to a
+     * server, use {@link Geary.Imap.MailboxSpecifier.from_folder_path}
+     * instead. This method is useful for debugging and logging only.
      */
     public string to_string() {
-        return get_fullpath(">");
+        StringBuilder builder = new StringBuilder();
+        if (this.path != null) {
+            foreach (Geary.FolderPath folder in this.path) {
+                builder.append(folder.basename);
+                builder.append_c('>');
+            }
+        }
+        builder.append(basename);
+        return builder.str;
     }
 }
 
diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala
index af31564..ebcd0f1 100644
--- a/src/engine/imap-db/imap-db-folder.vala
+++ b/src/engine/imap-db/imap-db-folder.vala
@@ -198,11 +198,18 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
             Imap.UIDValidity? uid_validity = !result.is_null_for("uid_validity")
                 ? new Imap.UIDValidity(result.int64_for("uid_validity"))
                 : null;
-            
+
             // Note that recent is not stored
-            status_data = new Imap.StatusData(new Imap.MailboxSpecifier.from_folder_path(path, "?"),
-                messages, 0, uid_next, uid_validity, result.int_for("unread_count"));
-            
+            status_data = new Imap.StatusData(
+                // XXX using to_string here very sketchy
+                new Imap.MailboxSpecifier(this.path.to_string()),
+                messages,
+                0,
+                uid_next,
+                uid_validity,
+                result.int_for("unread_count")
+            );
+
             return Db.TransactionOutcome.DONE;
         }, cancellable);
         
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index fdadb40..86ae9c5 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -349,33 +349,25 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
             debug("Error refreshing unseen counts: %s", e.message);
         }
     }
-    
-    private async void refresh_unseen_async(Geary.Folder folder, Cancellable? cancellable) throws Error {
+
+    private async void refresh_unseen_async(Geary.Folder folder, Cancellable? cancellable)
+    throws Error {
         debug("Refreshing unseen counts for %s", folder.to_string());
-        
+
         try {
-            bool folder_created;
-            Imap.Folder remote_folder = yield remote.fetch_folder_async(folder.path,
-                out folder_created, null, cancellable);
-            
-            // if created, don't need to fetch count because it was fetched when it was created
-            int unseen, total;
-            if (!folder_created) {
-                yield remote.fetch_counts_async(folder.path, out unseen, out total, cancellable);
-                remote_folder.properties.set_status_unseen(unseen);
-                remote_folder.properties.set_status_message_count(total, false);
-            } else {
-                unseen = remote_folder.properties.unseen;
-                total = remote_folder.properties.email_total;
-            }
-            
+            Imap.Folder remote_folder = yield remote.fetch_folder_cached_async(
+                folder.path,
+                true,
+                cancellable
+            );
+
             yield local.update_folder_status_async(remote_folder, false, true, cancellable);
         } finally {
             // added when call scheduled (above)
             in_refresh_unseen.remove(folder);
         }
     }
-    
+
     private void reschedule_folder_refresh(bool immediate) {
         if (in_refresh_enumerate)
             return;
@@ -541,13 +533,14 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         int length = path.get_path_length();
         for (int ctr = 0; ctr < length; ctr++) {
             Geary.FolderPath folder = path.get_folder_at(ctr);
-            
+
             if (yield local.folder_exists_async(folder))
                 continue;
-            
-            Imap.Folder remote_folder = (Imap.Folder) yield remote.fetch_folder_async(folder,
-                null, null, cancellable);
-            
+
+            Imap.Folder remote_folder = yield remote.fetch_folder_cached_async(
+                folder, false, cancellable
+            );
+
             yield local.clone_folder_async(remote_folder, cancellable);
         }
         
@@ -574,13 +567,13 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
     public async Imap.Folder fetch_detached_folder_async(Geary.FolderPath path, Cancellable? cancellable)
         throws Error {
         check_open();
-        
+
         if (local_only.has_key(path)) {
             throw new EngineError.NOT_FOUND("%s: path %s points to local-only folder, not IMAP",
                 to_string(), path.to_string());
         }
-        
-        return yield remote.fetch_unrecycled_folder_async(path, cancellable);
+
+        return yield remote.fetch_folder_async(path, cancellable);
     }
 
     private void compile_special_search_names() {
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index b0b3f72..bd69b45 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -637,18 +637,38 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
         // following blocks of code are fairly tricky because if the remote open fails need to
         // carefully back out and possibly retry
         Imap.Folder? opening_folder = null;
+        FolderPath path = local_folder.get_path();
         try {
-            debug("Fetching STATUS for remote %s from local", to_string());
-            Imap.StatusData local_status = yield local_folder.fetch_status_data(
-                ImapDB.Folder.ListFlags.NONE, cancellable);
-            
+            // Fetch the local status first anyway, since if it
+            // doesn't exist we haven't seen the folder before anyway
+            Imap.StatusData? local_status = yield local_folder.fetch_status_data(
+                ImapDB.Folder.ListFlags.NONE,
+                cancellable
+            );
+
             debug("Fetching information for remote folder %s", to_string());
-            opening_folder = yield remote.fetch_folder_async(local_folder.get_path(), null, local_status,
-                cancellable);
-            
+            try {
+                opening_folder = yield this.remote.fetch_folder_cached_async(
+                    path, false, cancellable
+                );
+            } catch (EngineError.NOT_FOUND err) {
+                if (err is EngineError.NOT_FOUND) {
+                    throw err;
+                }
+
+                // Use local STATUS data cache to be able to present
+                // something to the user at least. XXX get the attrs
+                // from somewhere for Bug 714775
+                opening_folder = this.remote.newSelectableFolder(
+                    path,
+                    local_status,
+                    new Imap.MailboxAttributes(new Gee.ArrayList<Geary.Imap.MailboxAttribute>())
+                );
+            }
+
             debug("Opening remote folder %s", opening_folder.to_string());
             yield opening_folder.open_async(cancellable);
-            
+
             // allow subclasses to examine the opened folder and resolve any vital
             // inconsistencies
             if (yield normalize_folders(opening_folder, cancellable)) {
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 3613441..00251b5 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -21,26 +21,25 @@
  * higher layers of the stack.
  */
 private class Geary.Imap.Account : BaseObject {
+
+
     public bool is_open { get; private set; default = false; }
-    
+
     private string name;
     private AccountInformation account_information;
     private ClientSessionManager session_mgr;
     private ClientSession? account_session = null;
     private Nonblocking.Mutex account_session_mutex = new Nonblocking.Mutex();
     private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
-    private Gee.HashMap<FolderPath, MailboxInformation> path_to_mailbox = new Gee.HashMap<
-        FolderPath, MailboxInformation>();
     private Gee.HashMap<FolderPath, Imap.Folder> folders = new Gee.HashMap<FolderPath, Imap.Folder>();
     private Gee.List<MailboxInformation>? list_collector = null;
     private Gee.List<StatusData>? status_collector = null;
     private Gee.List<ServerData>? server_data_collector = null;
-    private Imap.MailboxSpecifier? inbox_specifier = null;
-    private string hierarchy_delimiter = null;
 
-    
+
     public signal void login_failed(Geary.Credentials? cred, StatusResponse? response);
-    
+
+
     public Account(Geary.AccountInformation account_information) {
         name = "IMAP Account for %s".printf(account_information.imap_credentials.to_string());
         this.account_information = account_information;
@@ -77,24 +76,245 @@ private class Geary.Imap.Account : BaseObject {
         
         is_open = false;
     }
-    
+
+    public async bool folder_exists_async(FolderPath path, Cancellable? cancellable)
+    throws Error {
+        ClientSession session = yield claim_session_async(cancellable);
+        Gee.List<MailboxInformation> mailboxes = yield send_list_async(session, path, false, cancellable);
+        bool exists = mailboxes.is_empty;
+        if (!exists) {
+            this.folders.remove(path);
+        }
+
+        // XXX fire some signal here
+
+        return exists;
+    }
+
+    public async void create_folder_async(FolderPath path, Cancellable? cancellable)
+    throws Error {
+        ClientSession session = yield claim_session_async(cancellable);
+        StatusResponse response = yield send_command_async(
+            session,
+            new CreateCommand(session.get_mailbox_for_path(path)),
+            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());
+        }
+    }
+
+    /**
+     * Returns a single folder from the server.
+     *
+     * The folder is not cached by the account and hence will not be
+     * used my multiple callers or containers.  This is useful for
+     * one-shot operations on the server.
+     */
+    public async Imap.Folder fetch_folder_async(FolderPath path, Cancellable? cancellable)
+        throws Error {
+        ClientSession session = yield claim_session_async(cancellable);
+
+        Gee.List<MailboxInformation>? mailboxes = yield send_list_async(session, path, false, cancellable);
+        if (mailboxes.is_empty)
+            throw_not_found(path);
+
+        Imap.Folder? folder = null;
+        MailboxInformation mailbox_info = mailboxes.get(0);
+        if (!mailbox_info.attrs.is_no_select) {
+            StatusData status = yield send_status_async(
+                session,
+                mailbox_info.mailbox,
+                StatusDataType.all(),
+                cancellable
+            );
+            folder = newSelectableFolder(path, status, mailbox_info.attrs);
+        } else {
+            folder = newUnselectableFolder(path, mailbox_info.attrs);
+        }
+
+        return folder;
+    }
+
+    /**
+     * Returns a single folder, from the account's cache or fetched fresh.
+     *
+     * If the folder has previously been retrieved, that is returned
+     * instead of fetching it again. If not, it is fetched from the
+     * server and cached for future use.
+     */
+    public async Imap.Folder fetch_folder_cached_async(FolderPath path,
+                                                       bool refresh_counts,
+                                                       Cancellable? cancellable)
+    throws Error {
+        check_open();
+
+        Imap.Folder? folder = this.folders.get(path);
+        if (folder != null) {
+            if (refresh_counts && folder.properties.attrs.is_no_select) {
+                try {
+                    ClientSession session = yield claim_session_async(cancellable);
+                    StatusData data = yield send_status_async(
+                        session,
+                        session.get_mailbox_for_path(path),
+                        { StatusDataType.UNSEEN, StatusDataType.MESSAGES },
+                        cancellable
+                     );
+                    folder.properties.set_status_unseen(data.unseen);
+                    folder.properties.set_status_message_count(data.messages, false);
+                } catch (ImapError e) {
+                    this.folders.remove(path);
+                    // XXX notify someone
+                    throw_not_found(path);
+                }
+            }
+        } else {
+            folder = yield fetch_folder_async(path, cancellable);
+            this.folders.set(path, folder);
+        }
+        return folder;
+    }
+
+    /**
+     * Returns a list of children of the given folder.
+     *
+     * If the parent folder is `null`, then the root of the server
+     * will be listed.
+     *
+     * This method will perform a pipe-lined IMAP SELECT for all
+     * folders found, and hence should be used with care.
+     */
+    public async Gee.List<Imap.Folder>? list_child_folders_async(FolderPath? parent, Cancellable? 
cancellable)
+    throws Error {
+        ClientSession session = yield claim_session_async(cancellable);
+
+        Gee.List<MailboxInformation> mailboxes = yield send_list_async(session, parent, true, cancellable);
+        if (mailboxes.size == 0) {
+            return null;
+        }
+
+        Gee.List<Imap.Folder> children = new Gee.ArrayList<Imap.Folder>();
+
+        // Work out which folders need a STATUS and send them all
+        // pipe-lined to minimise network and server latency.
+        Gee.Map<MailboxSpecifier, MailboxInformation> info_map = new Gee.HashMap<
+            MailboxSpecifier, MailboxInformation>();
+        Gee.Map<StatusCommand, MailboxSpecifier> cmd_map = new Gee.HashMap<
+            StatusCommand, MailboxSpecifier>();
+        foreach (MailboxInformation mailbox_info in mailboxes) {
+            if (!mailbox_info.attrs.is_no_select) {
+                // Mailbox needs a SELECT
+                info_map.set(mailbox_info.mailbox, mailbox_info);
+                cmd_map.set(
+                    new StatusCommand(mailbox_info.mailbox, StatusDataType.all()),
+                    mailbox_info.mailbox
+                );
+            } else {
+                // 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);
+                Folder? child = this.folders.get(path);
+                if (child == null) {
+                    child = newUnselectableFolder(path, mailbox_info.attrs);
+                    this.folders.set(path, child);
+                }
+                children.add(child);
+            }
+        }
+
+        if (!cmd_map.is_empty) {
+            Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
+            Gee.Map<Command, StatusResponse> responses = yield send_multiple_async(
+                session,
+                cmd_map.keys,
+                null,
+                status_results,
+                cancellable
+            );
+
+            foreach (Command cmd in responses.keys) {
+                StatusCommand status_cmd = (StatusCommand) cmd;
+                StatusResponse response = responses.get(cmd);
+
+                MailboxSpecifier mailbox = cmd_map.get(status_cmd);
+                MailboxInformation mailbox_info = info_map.get(mailbox);
+
+                if (response.status != Status.OK) {
+                    message("Unable to get STATUS of %s: %s", mailbox.to_string(), response.to_string());
+                    message("STATUS command: %s", cmd.to_string());
+                    continue;
+                }
+
+                // Server might return results in any order, so need
+                // to find it
+                StatusData? status = null;
+                foreach (StatusData status_data in status_results) {
+                    if (status_data.mailbox.equal_to(mailbox)) {
+                        status = status_data;
+                        break;
+                    }
+                }
+                if (status == null) {
+                    message("Unable to get STATUS of %s: not returned from server", mailbox.to_string());
+                    continue;
+                }
+                status_results.remove(status);
+
+                FolderPath child_path = session.get_path_for_mailbox(mailbox_info.mailbox);
+                Imap.Folder? child = this.folders.get(child_path);
+                if (child != null) {
+                    child.properties.update_status(status);
+                } else {
+                    child = newSelectableFolder(child_path, status, mailbox_info.attrs);
+                    this.folders.set(child_path, child);
+                }
+
+                children.add(child);
+            }
+
+            if (status_results.size > 0)
+                debug("%d STATUS results leftover", status_results.size);
+        }
+
+        return children;
+    }
+
+    internal Imap.Folder newSelectableFolder(FolderPath path, StatusData status, MailboxAttributes attrs) {
+        return new Imap.Folder(
+            path, new Imap.FolderProperties.status(status, attrs), this.session_mgr
+        );
+    }
+
+    internal void folders_removed(Gee.Collection<FolderPath> paths) {
+        foreach (FolderPath path in paths) {
+            if (folders.has_key(path))
+                folders.unset(path);
+        }
+    }
+
     // Claiming session in open_async() would delay opening, which make take too long ... rather,
     // this is used by the various calls to put off claiming a session until needed (which
     // possibly is long enough for ClientSessionManager to get a few ready).
-    private async ClientSession claim_session_async(Cancellable? cancellable) throws Error {
+    private async ClientSession claim_session_async(Cancellable? cancellable)
+    throws Error {
+        check_open();
         // check if available session is in good state
         if (account_session != null
             && account_session.get_protocol_state(null) != ClientSession.ProtocolState.AUTHORIZED) {
             yield drop_session_async(cancellable);
         }
-        
+
         int token = yield account_session_mutex.claim_async(cancellable);
-        
+
         Error? err = null;
         if (account_session == null) {
             try {
                 account_session = yield session_mgr.claim_authorized_session_async(cancellable);
-                
+
                 account_session.list.connect(on_list_data);
                 account_session.status.connect(on_status_data);
                 account_session.server_data_received.connect(on_server_data_received);
@@ -103,13 +323,13 @@ private class Geary.Imap.Account : BaseObject {
                 err = claim_err;
             }
         }
-        
+
         account_session_mutex.release(ref token);
-        
+
         if (err != null) {
             if (account_session != null)
                 yield drop_session_async(null);
-            
+
             throw err;
         }
 
@@ -118,18 +338,18 @@ private class Geary.Imap.Account : BaseObject {
 
     private async void drop_session_async(Cancellable? cancellable) {
         debug("[%s] Dropping account session...", to_string());
-        
+
         int token;
         try {
             token = yield account_session_mutex.claim_async(cancellable);
         } catch (Error err) {
             debug("Unable to claim Imap.Account session mutex: %s", err.message);
-            
+
             return;
         }
-        
+
         string desc = account_session != null ? account_session.to_string() : "(none)";
-        
+
         if (account_session != null) {
             // disconnect signals before releasing (in particular, "disconnected" will in turn
             // reenter this method, so avoid that)
@@ -137,357 +357,129 @@ private class Geary.Imap.Account : BaseObject {
             account_session.status.disconnect(on_status_data);
             account_session.server_data_received.disconnect(on_server_data_received);
             account_session.disconnected.disconnect(on_disconnected);
-            
+
             debug("[%s] Releasing account session %s", to_string(), desc);
-            
+
             try {
                 yield session_mgr.release_session_async(account_session, cancellable);
             } catch (Error err) {
                 // ignored
             }
-            
+
             debug("[%s] Released account session %s", to_string(), desc);
-            
+
             account_session = null;
         }
-        
+
         try {
             account_session_mutex.release(ref token);
         } catch (Error err) {
             // ignored
         }
-        
-        debug("[%s] Dropped account session (%s)", to_string(), desc);
-    }
-    
-    private void on_list_data(MailboxInformation mailbox_info) {
-        if (list_collector != null)
-            list_collector.add(mailbox_info);
-    }
-    
-    private void on_status_data(StatusData status_data) {
-        if (status_collector != null)
-            status_collector.add(status_data);
-    }
-    
-    private void on_server_data_received(ServerData server_data) {
-        if (server_data_collector != null)
-            server_data_collector.add(server_data);
-    }
-    
-    private void on_disconnected() {
-        drop_session_async.begin(null);
-    }
-    
-    public async bool folder_exists_async(FolderPath path, Cancellable? cancellable) throws Error {
-        return path_to_mailbox.has_key(path);
-    }
-    
-    public async void create_folder_async(FolderPath path, Cancellable? cancellable) throws Error {
-        check_open();
-        yield claim_session_async(cancellable);
-
-        StatusResponse response = yield send_command_async(new CreateCommand(
-            new MailboxSpecifier.from_folder_path(path, this.hierarchy_delimiter)), 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());
-        }
-    }
-    
-    // By supplying fallback STATUS, the Folder may be fetched if a network error occurs; if null,
-    // the network error is thrown
-    public async Imap.Folder fetch_folder_async(FolderPath path, out bool created,
-        StatusData? fallback_status_data, Cancellable? cancellable) throws Error {
-        check_open();
-        
-        created = false;
-        
-        if (folders.has_key(path))
-            return folders.get(path);
-        
-        created = true;
-        
-        // if not in map, use list_children_async to add it (if it exists)
-        if (!path_to_mailbox.has_key(path)) {
-            debug("Listing children to find %s", path.to_string());
-            yield list_children_async(path.get_parent(), cancellable);
-        }
-        
-        MailboxInformation? mailbox_info = path_to_mailbox.get(path);
-        if (mailbox_info == null)
-            throw_not_found(path);
-        
-        // construct folder path for new folder, converting XLIST Inbox name to canonical INBOX
-        FolderPath folder_path = mailbox_info.get_path(inbox_specifier);
-        
-        Imap.Folder folder;
-        if (!mailbox_info.attrs.is_no_select) {
-            StatusData status;
-            try {
-                status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
-            } catch (Error err) {
-                if (fallback_status_data == null)
-                    throw err;
-                
-                debug("Unable to fetch STATUS for %s, using fallback from local: %s", 
folder_path.to_string(),
-                    err.message);
-                
-                status = fallback_status_data;
-            }
-
-            folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info, 
this.hierarchy_delimiter);
-        } else {
-            folder = new Imap.Folder.unselectable(folder_path, session_mgr, mailbox_info, 
this.hierarchy_delimiter);
-        }
 
-        folders.set(path, folder);
-        
-        return folder;
-    }
-    
-    /**
-     * Returns an Imap.Folder that is not stored long-term in the Imap.Account object.
-     *
-     * This means the Imap.Folder is not re-used or used by multiple users or containers.  This is
-     * useful for one-shot operations on the server.
-     */
-    public async Imap.Folder fetch_unrecycled_folder_async(FolderPath path, Cancellable? cancellable)
-        throws Error {
-        check_open();
-        
-        MailboxInformation? mailbox_info = path_to_mailbox.get(path);
-        if (mailbox_info == null)
-            throw_not_found(path);
-        
-        // construct canonical folder path
-        FolderPath folder_path = mailbox_info.get_path(inbox_specifier);
-        
-        Imap.Folder folder;
-        if (!mailbox_info.attrs.is_no_select) {
-            StatusData status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
-
-            folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info, 
this.hierarchy_delimiter);
-        } else {
-            folder = new Imap.Folder.unselectable(folder_path, session_mgr, mailbox_info, 
this.hierarchy_delimiter);
-        }
-
-        return folder;
-    }
-    
-    internal void folders_removed(Gee.Collection<FolderPath> paths) {
-        foreach (FolderPath path in paths) {
-            if (path_to_mailbox.has_key(path))
-                path_to_mailbox.unset(path);
-            if (folders.has_key(path))
-                folders.unset(path);
-        }
-    }
-    
-    public async void fetch_counts_async(FolderPath path, out int unseen, out int total,
-        Cancellable? cancellable) throws Error {
-        check_open();
-        
-        MailboxInformation? mailbox_info = path_to_mailbox.get(path);
-        if (mailbox_info == null)
-            throw_not_found(path);
-        if (mailbox_info.attrs.is_no_select) {
-            throw new EngineError.UNSUPPORTED("Can't fetch unseen count for unselectable folder %s",
-                path.to_string());
-        }
-        
-        StatusData data = yield fetch_status_async(path, { StatusDataType.UNSEEN, StatusDataType.MESSAGES },
-            cancellable);
-        unseen = data.unseen;
-        total = data.messages;
-    }
-    
-    private async StatusData fetch_status_async(FolderPath path, StatusDataType[] status_types,
-        Cancellable? cancellable) throws Error {
-        check_open();
-        
-        Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
-        StatusResponse response = yield send_command_async(
-            new StatusCommand(new MailboxSpecifier.from_folder_path(path, this.hierarchy_delimiter), 
status_types),
-            null, status_results, cancellable);
-        
-        throw_fetch_error(response, path, status_results.size);
-        
-        return status_results[0];
-    }
-    
-    private void throw_fetch_error(StatusResponse response, FolderPath path, int result_count)
-        throws Error {
-        assert(response.is_completion);
-        
-        if (response.status != Status.OK) {
-            throw new ImapError.SERVER_ERROR("Server reports error for path %s: %s", path.to_string(),
-                response.to_string());
-        }
-        
-        if (result_count != 1) {
-            throw new ImapError.INVALID("Server reports %d results for fetch of path %s: %s",
-                result_count, path.to_string(), response.to_string());
-        }
+        debug("[%s] Dropped account session (%s)", to_string(), desc);
     }
-    
-    public async Gee.List<Imap.Folder>? list_child_folders_async(FolderPath? parent, Cancellable? 
cancellable)
-        throws Error {
-        check_open();
-        
-        Gee.List<MailboxInformation>? child_info = yield list_children_async(parent, cancellable);
-        if (child_info == null || child_info.size == 0) {
-            debug("No children found listing %s", (parent != null) ? parent.to_string() : "root");
-            
-            return null;
-        }
-        
-        Gee.List<Imap.Folder> child_folders = new Gee.ArrayList<Imap.Folder>();
-        
-        Gee.Map<MailboxSpecifier, MailboxInformation> info_map = new Gee.HashMap<
-            MailboxSpecifier, MailboxInformation>();
-        Gee.Map<StatusCommand, MailboxSpecifier> cmd_map = new Gee.HashMap<
-            StatusCommand, MailboxSpecifier>();
-        foreach (MailboxInformation mailbox_info in child_info) {
-            // if new mailbox is unselectable, don't bother doing a STATUS command
-            if (mailbox_info.attrs.is_no_select) {
-                Imap.Folder folder = new Imap.Folder.unselectable(mailbox_info.get_path(inbox_specifier),
-                    session_mgr, mailbox_info, this.hierarchy_delimiter);
-                folders.set(folder.path, folder);
-                child_folders.add(folder);
-
-                continue;
-            }
-            
-            info_map.set(mailbox_info.mailbox, mailbox_info);
-            cmd_map.set(new StatusCommand(mailbox_info.mailbox, StatusDataType.all()),
-                mailbox_info.mailbox);
-        }
-        
-        // if no STATUS results are needed, bail out with what's been collected
-        if (cmd_map.size == 0)
-            return child_folders;
-        
-        Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
-        Gee.Map<Command, StatusResponse> responses = yield send_multiple_async(cmd_map.keys,
-            null, status_results, cancellable);
-        
-        foreach (Command cmd in responses.keys) {
-            StatusCommand status_cmd = (StatusCommand) cmd;
-            StatusResponse response = responses.get(cmd);
-            
-            MailboxSpecifier mailbox = cmd_map.get(status_cmd);
-            MailboxInformation mailbox_info = info_map.get(mailbox);
-            
-            if (response.status != Status.OK) {
-                message("Unable to get STATUS of %s: %s", mailbox.to_string(), response.to_string());
-                message("STATUS command: %s", cmd.to_string());
-                
-                continue;
-            }
-            
-            StatusData? found_status = null;
-            foreach (StatusData status_data in status_results) {
-                if (status_data.mailbox.equal_to(mailbox)) {
-                    found_status = status_data;
-                    
-                    break;
-                }
-            }
-            
-            if (found_status == null) {
-                message("Unable to get STATUS of %s: not returned from server", mailbox.to_string());
-                
-                continue;
-            }
-            
-            status_results.remove(found_status);
-            
-            FolderPath folder_path = mailbox_info.get_path(inbox_specifier);
-            
-            // if already have an Imap.Folder for this mailbox, use that
-            Imap.Folder? folder = folders.get(folder_path);
-            if (folder != null) {
-                folder.properties.update_status(found_status);
-            } else {
-                folder = new Imap.Folder(folder_path, session_mgr, found_status, mailbox_info, 
this.hierarchy_delimiter);
-                folders.set(folder.path, folder);
-            }
 
-            child_folders.add(folder);
-        }
-        
-        if (status_results.size > 0)
-            debug("%d STATUS results leftover", status_results.size);
-        
-        return child_folders;
-    }
-    
-    private async Gee.List<MailboxInformation>? list_children_async(FolderPath? parent, Cancellable? 
cancellable)
+    // Performs a LIST against the server, returning the results
+    private async Gee.List<MailboxInformation> send_list_async(ClientSession session,
+                                                               FolderPath? folder,
+                                                               bool list_children,
+                                                               Cancellable? cancellable)
         throws Error {
-        ClientSession session = yield claim_session_async(cancellable);
         bool can_xlist = session.capabilities.has_capability(Capabilities.XLIST);
-        
+
         // Request SPECIAL-USE if available and not using XLIST
         ListReturnParameter? return_param = null;
         if (session.capabilities.supports_special_use() && !can_xlist) {
             return_param = new ListReturnParameter();
             return_param.add_special_use();
         }
-        
+
         ListCommand cmd;
-        if (parent == null) {
-            cmd = new ListCommand.wildcarded("", new MailboxSpecifier("%"), can_xlist, return_param);
+        if (folder == null) {
+            // List the server root
+            cmd = new ListCommand.wildcarded(
+                "", new MailboxSpecifier("%"), can_xlist, return_param
+            );
         } else {
-            if (hierarchy_delimiter == null) {
-                throw new ImapError.INVALID("Unable to list children of %s: no delimiter specified",
-                    parent.to_string());
+            // List either the given folder or its children
+            string specifier = session.get_mailbox_for_path(folder).name;
+            if (list_children) {
+                string? delim = session.get_delimiter_for_path(folder);
+                if (delim == null) {
+                    throw new ImapError.INVALID("Cannot list children of namespace with no delimiter");
+                }
+                specifier = specifier + delim + "%";
             }
-            
-            string? specifier = parent.get_fullpath(hierarchy_delimiter);
-            specifier += specifier.has_suffix(hierarchy_delimiter) ? "%" : (hierarchy_delimiter + "%");
-            
             cmd = new ListCommand(new MailboxSpecifier(specifier), can_xlist, return_param);
         }
-        
+
         Gee.List<MailboxInformation> list_results = new Gee.ArrayList<MailboxInformation>();
-        StatusResponse response = yield send_command_async(cmd, list_results, null, cancellable);
-        
+        StatusResponse response = yield send_command_async(session, cmd, list_results, null, cancellable);
         if (response.status != Status.OK) {
             throw new ImapError.SERVER_ERROR("Unable to list children of %s: %s",
-                (parent != null) ? parent.to_string() : "root", response.to_string());
+                (folder != null) ? folder.to_string() : "root", response.to_string());
         }
-        
-        // See note at ListCommand about some servers returning the parent's name alongside their
-        // children ... this filters this out
-        if (parent != null) {
+
+        // See note at ListCommand about some servers returning the
+        // parent's name alongside their children ... this filters
+        // this out
+        if (folder != null && list_children) {
             Gee.Iterator<MailboxInformation> iter = list_results.iterator();
             while (iter.next()) {
-                FolderPath list_path = iter.get().mailbox.to_folder_path(hierarchy_delimiter,
-                    inbox_specifier);
-                if (list_path.equal_to(parent)) {
+                FolderPath list_path = session.get_path_for_mailbox(iter.get().mailbox);
+                if (list_path.equal_to(folder)) {
                     debug("Removing parent from LIST results: %s", list_path.to_string());
                     iter.remove();
                 }
             }
         }
-        
-        // stash all MailboxInformation by path
-        // TODO: remove any MailboxInformation for this parent that is not found (i.e. has been
-        // removed on the server)
-        foreach (MailboxInformation mailbox_info in list_results)
-            path_to_mailbox.set(mailbox_info.get_path(inbox_specifier), mailbox_info);
-        
-        return (list_results.size > 0) ? list_results : null;
+
+        return list_results;
     }
-    
-    private async StatusResponse send_command_async(Command cmd,
-        Gee.List<MailboxInformation>? list_results, Gee.List<StatusData>? status_results,
+
+    private async StatusData send_status_async(ClientSession session,
+                                               MailboxSpecifier mailbox,
+                                               StatusDataType[] status_types,
+                                               Cancellable? cancellable)
+    throws Error {
+        Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
+        StatusResponse response = yield send_command_async(
+            session,
+            new StatusCommand(mailbox, status_types),
+            null,
+            status_results,
+            cancellable
+        );
+
+        if (response.status != Status.OK) {
+            throw new ImapError.SERVER_ERROR("Error fetching \"%s\" STATUS: %s",
+                                             mailbox.to_string(),
+                                             response.to_string());
+        }
+
+        if (status_results.size != 1) {
+            throw new ImapError.INVALID("Invalid result count (%d) \"%s\" STATUS: %s",
+                                        status_results.size,
+                                        mailbox.to_string(),
+                                        response.to_string());
+        }
+
+        return status_results[0];
+    }
+
+    private async StatusResponse send_command_async(ClientSession session,
+                                                    Command cmd,
+                                                    Gee.List<MailboxInformation>? list_results,
+                                                    Gee.List<StatusData>? status_results,
         Cancellable? cancellable) throws Error {
         Gee.Map<Command, StatusResponse> responses = yield send_multiple_async(
-            Geary.iterate<Command>(cmd).to_array_list(), list_results, status_results,
-            cancellable);
+            session,
+            Geary.iterate<Command>(cmd).to_array_list(),
+            list_results,
+            status_results,
+            cancellable
+        );
         
         assert(responses.size == 1);
         
@@ -495,8 +487,12 @@ private class Geary.Imap.Account : BaseObject {
     }
     
     private async Gee.Map<Command, StatusResponse> send_multiple_async(
-        Gee.Collection<Command> cmds, Gee.List<MailboxInformation>? list_results,
-        Gee.List<StatusData>? status_results, Cancellable? cancellable) throws Error {
+        ClientSession session,
+        Gee.Collection<Command> cmds,
+        Gee.List<MailboxInformation>? list_results,
+        Gee.List<StatusData>? status_results,
+        Cancellable? cancellable)
+    throws Error {
         int token = yield cmd_mutex.claim_async(cancellable);
         
         // set up collectors
@@ -506,7 +502,6 @@ private class Geary.Imap.Account : BaseObject {
         Gee.Map<Command, StatusResponse>? responses = null;
         Error? err = null;
         try {
-            ClientSession session = yield claim_session_async(cancellable);
             responses = yield session.send_multiple_commands_async(cmds, cancellable);
         } catch (Error send_err) {
             err = send_err;
@@ -525,19 +520,43 @@ private class Geary.Imap.Account : BaseObject {
         
         return responses;
     }
-    
+
+    private inline Imap.Folder newUnselectableFolder(FolderPath path, MailboxAttributes attrs) {
+        return new Imap.Folder(
+            path, new Imap.FolderProperties(0, 0, 0, null, null, attrs), this.session_mgr
+        );
+    }
+
     [NoReturn]
     private void throw_not_found(Geary.FolderPath? path) throws EngineError {
         throw new EngineError.NOT_FOUND("Folder %s not found on %s",
             (path != null) ? path.to_string() : "root", session_mgr.to_string());
     }
-    
+
     private void on_login_failed(StatusResponse? response) {
         login_failed(account_information.imap_credentials, response);
     }
-    
+
+    private void on_list_data(MailboxInformation mailbox_info) {
+        if (list_collector != null)
+            list_collector.add(mailbox_info);
+    }
+
+    private void on_status_data(StatusData status_data) {
+        if (status_collector != null)
+            status_collector.add(status_data);
+    }
+
+    private void on_server_data_received(ServerData server_data) {
+        if (server_data_collector != null)
+            server_data_collector.add(server_data);
+    }
+
+    private void on_disconnected() {
+        drop_session_async.begin(null);
+    }
+
     public string to_string() {
         return name;
     }
 }
-
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index 3b726e7..368e594 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -26,18 +26,17 @@ private class Geary.Imap.Folder : BaseObject {
     public bool is_open { get; private set; default = false; }
     public FolderPath path { get; private set; }
     public Imap.FolderProperties properties { get; private set; }
-    public MailboxInformation info { get; private set; }
-    public string delim { get; private set; }
     public MessageFlags? permanent_flags { get; private set; default = null; }
     public Trillian readonly { get; private set; default = Trillian.UNKNOWN; }
     public Trillian accepts_user_flags { get; private set; default = Trillian.UNKNOWN; }
+
     /**
      * Set to true when it's detected that the server doesn't allow a space between "header.fields"
      * and the list of email headers to be requested via FETCH; see
      * https://bugzilla.gnome.org/show_bug.cgi?id=714902
      */
     public bool imap_header_fields_hack { get; private set; default = false; }
-    
+
     private ClientSessionManager session_mgr;
     private ClientSession? session = null;
     private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
@@ -81,31 +80,11 @@ private class Geary.Imap.Folder : BaseObject {
      */
     public signal void disconnected(ClientSession.DisconnectReason reason);
 
-    internal Folder(FolderPath path, ClientSessionManager session_mgr, StatusData status, MailboxInformation 
info, string delim) {
-        // Used to assert() here, but that meant that any issue with internationalization/encoding
-        // made Geary unusable for a subset of servers accessed/configured in a non-English language...
-        // this is not the end of the world, but it does suggest an I18N issue, potentially with
-        // how XLIST returns folder names on different servers.
-        if (!status.mailbox.equal_to(info.mailbox)) {
-            message("%s: IMAP folder created with differing mailbox names (STATUS=%s LIST=%s)",
-                path.to_string(), status.to_string(), info.to_string());
-        }
 
-        this.session_mgr = session_mgr;
-        this.info = info;
-        this.delim = delim;
+    internal Folder(FolderPath path, Imap.FolderProperties properties, ClientSessionManager session_mgr) {
         this.path = path;
-
-        properties = new Imap.FolderProperties.status(status, info.attrs);
-    }
-
-    internal Folder.unselectable(FolderPath path, ClientSessionManager session_mgr, MailboxInformation info, 
string delim) {
+        this.properties = properties;
         this.session_mgr = session_mgr;
-        this.info = info;
-        this.delim = delim;
-        this.path = path;
-
-        properties = new Imap.FolderProperties(0, 0, 0, null, null, info.attrs);
     }
 
     public async void open_async(Cancellable? cancellable) throws Error {
@@ -127,11 +106,11 @@ private class Geary.Imap.Folder : BaseObject {
 
         properties.set_from_session_capabilities(session.capabilities);
 
+        MailboxSpecifier mailbox = this.session.get_mailbox_for_path(this.path);
         StatusResponse? response = null;
         Error? select_err = null;
         try {
-            response = yield session.select_async(
-                new MailboxSpecifier.from_folder_path(path, this.delim), cancellable);
+            response = yield this.session.select_async(mailbox, cancellable);
         } catch (Error err) {
             select_err = err;
         }
@@ -700,8 +679,8 @@ private class Geary.Imap.Folder : BaseObject {
         Cancellable? cancellable) throws Error {
         check_open();
 
-        CopyCommand cmd = new CopyCommand(msg_set,
-            new MailboxSpecifier.from_folder_path(destination, this.delim));
+        MailboxSpecifier mailbox = this.session.get_mailbox_for_path(destination);
+        CopyCommand cmd = new CopyCommand(msg_set, mailbox);
 
         Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async(
             Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable);
@@ -1086,8 +1065,10 @@ private class Geary.Imap.Folder : BaseObject {
         if (date_received != null)
             internaldate = new InternalDate.from_date_time(date_received);
 
-        AppendCommand cmd = new AppendCommand(new MailboxSpecifier.from_folder_path(path, this.delim),
-            msg_flags, internaldate, message.get_network_buffer(false));
+        MailboxSpecifier mailbox = this.session.get_mailbox_for_path(this.path);
+        AppendCommand cmd = new AppendCommand(
+            mailbox, msg_flags, internaldate, message.get_network_buffer(false)
+        );
 
         Gee.Map<Command, StatusResponse> responses = yield exec_commands_async(
             Geary.iterate<AppendCommand>(cmd).to_array_list(), null, null, null);
diff --git a/src/engine/imap/message/imap-mailbox-specifier.vala 
b/src/engine/imap/message/imap-mailbox-specifier.vala
index fa0c7c0..7df741e 100644
--- a/src/engine/imap/message/imap-mailbox-specifier.vala
+++ b/src/engine/imap/message/imap-mailbox-specifier.vala
@@ -91,10 +91,25 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
     /**
      * Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
      */
-    public MailboxSpecifier.from_folder_path(FolderPath path, string delim) throws ImapError {
-        init(path.get_fullpath(delim));
+    public MailboxSpecifier.from_folder_path(FolderPath path, MailboxSpecifier inbox, string? delim)
+    throws ImapError {
+        Gee.List<string> parts = path.as_list();
+        if (parts.size > 1 && delim == null) {
+            // XXX not quite right
+            throw new ImapError.INVALID("Path has more than one part but no delimiter given");
+        }
+
+        StringBuilder builder = new StringBuilder(
+            is_inbox_name(parts[0]) ? inbox.name : parts[0]);
+
+        for (int i = 1; i < parts.size; i++) {
+            builder.append(delim);
+            builder.append(parts[i]);
+        }
+
+        init(builder.str);
     }
-    
+
     private void init(string decoded) {
         name = decoded;
         is_inbox = is_inbox_name(decoded);
diff --git a/src/engine/imap/transport/imap-client-session.vala 
b/src/engine/imap/transport/imap-client-session.vala
index 22e8d61..c3a7110 100644
--- a/src/engine/imap/transport/imap-client-session.vala
+++ b/src/engine/imap/transport/imap-client-session.vala
@@ -447,7 +447,73 @@ public class Geary.Imap.ClientSession : BaseObject {
     public bool is_current_mailbox_readonly() {
         return current_mailbox_readonly;
     }
-    
+
+    /**
+     * Determines the SELECT-able mailbox name for a specific folder path.
+     */
+    public MailboxSpecifier get_mailbox_for_path(FolderPath path)
+    throws ImapError {
+        string? delim = get_delimiter_for_path(path);
+        return new MailboxSpecifier.from_folder_path(path, this.inbox.mailbox, delim);
+    }
+
+    /**
+     * Determines the folder path for a mailbox name.
+     */
+    public FolderPath get_path_for_mailbox(MailboxSpecifier mailbox)
+    throws ImapError {
+        string? delim = get_delimiter_for_mailbox(mailbox);
+        return mailbox.to_folder_path(delim, this.inbox.mailbox);
+    }
+
+    /**
+     * Determines the mailbox hierarchy delimiter for a given folder path.
+     *
+     * The returned delimiter be null if a namespace (INBOX, personal,
+     * etc) for the path does not exist, or if the namespace is flat.
+     */
+    public string? get_delimiter_for_path(FolderPath path)
+    throws ImapError {
+        string? delim = null;
+        Geary.FolderRoot root = path.get_root();
+        if (MailboxSpecifier.folder_path_is_inbox(root)) {
+            delim = this.inbox.delim;
+        } else {
+            Namespace? ns = this.namespaces.get(root.basename);
+            if (ns != null) {
+                delim = ns.delim;
+            }
+        }
+        return delim;
+    }
+
+    /**
+     * Determines the mailbox hierarchy delimiter for a given mailbox name.
+     *
+     * The returned delimiter be null if a namespace (INBOX, personal,
+     * etc) for the mailbox does not exist, or if the namespace is flat.
+     */
+    public string? get_delimiter_for_mailbox(MailboxSpecifier mailbox)
+    throws ImapError {
+        string name = mailbox.name;
+        string? delim = null;
+
+        string inbox_name = this.inbox.mailbox.name;
+        string? inbox_delim = this.inbox.delim;
+        if (inbox_name == name ||
+            (inbox_delim != null && inbox_name.has_prefix(name + inbox_delim))) {
+            delim = this.inbox.delim;
+        } else {
+            foreach (Namespace ns in this.namespaces.values) {
+                if (name.has_prefix(ns.prefix)) {
+                    delim = ns.delim;
+                    break;
+                }
+            }
+        }
+        return delim;
+    }
+
     /**
      * Returns the current {@link ProtocolState} of the {@link ClientSession} and, if selected,
      * the current mailbox.


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