Improved FolderPath and separator handling

This commit fixes a number of issues with traversing, showing, and
opening subfolders, especially on systems that don't use the
forward slash as a separator.
This commit is contained in:
Jim Nelson 2013-06-10 20:07:12 -07:00
parent f246ff4919
commit b254ad54e4
9 changed files with 220 additions and 80 deletions

View file

@ -4,8 +4,22 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A generic structure for representing and maintaining folder paths.
*
* A FolderPath may have one parent and one child. A FolderPath without a parent is called a
* root folder can be be created with {@link FolderRoot}, which is a FolderPath.
*
* A FolderPath has a delimiter. This delimiter is specified in the FolderRoot.
*
* @see FolderRoot
*/
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; }
private Gee.List<Geary.FolderPath>? path = null;
@ -26,26 +40,53 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
this.basename = basename;
}
/**
* Returns true if this {@link FolderPath} is the 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.
*/
public Geary.FolderRoot get_root() {
return (FolderRoot) ((path != null && path.size > 0) ? path[0] : this);
}
/**
* Returns the parent {@link FolderPath} of this folder or null if this is the root.
*
* @see is_root
*/
public Geary.FolderPath? get_parent() {
return (path != null && path.size > 0) ? path.last() : null;
}
/**
* Returns the number of folders in this path, not including any children of this object.
*/
public int get_path_length() {
// include self, which is not stored in the path list
return (path != null) ? path.size + 1 : 1;
}
/**
* Returns the {@link FolderPath} object at the index, with this FolderPath object being
* the farthest child.
*
* Root is at index 0 (zero).
*
* Returns null if index is out of bounds. There is always at least one element in the path,
* namely this one.
* namely this one, meaning zero is always acceptable and that index[length - 1] will always
* return this object.
*
* @see get_path_length
*/
public Geary.FolderPath? get_folder_at(int index) {
// include self, which is not stored in the path list ... essentially, this logic makes it
@ -63,6 +104,12 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return null;
}
/**
* Returns the {@link FolderPath} as a List of {@link basename} strings, this FolderPath's
* being the last in the list.
*
* Thus, the list should have at least one element.
*/
public Gee.List<string> as_list() {
Gee.List<string> list = new Gee.ArrayList<string>();
@ -76,6 +123,9 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return list;
}
/**
* Creates a {@link FolderPath} object that is a child of this folder.
*/
public Geary.FolderPath get_child(string basename) {
// Build the child's path, which is this node's path plus this node
Gee.List<FolderPath> child_path = new Gee.ArrayList<FolderPath>();
@ -86,13 +136,54 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return new FolderPath.child(child_path, basename);
}
public string get_fullpath(string? use_separator = null) {
/**
* Returns true if this {@link FolderPath} has a default separator.
*
* It determines this by returning true if its {@link FolderRoot.default_separator} is
* non-null and non-empty.
*/
public bool has_default_separator() {
return get_root().default_separator != null;
}
/**
* Returns true if the other {@link FolderPath} has the same parent as this one.
*
* Like {@link equal_to} and {@link compare_to}, this comparison does not account for the
* {@link FolderRoot.default_separator}. The comparison is lexiographic, not by reference.
*/
public bool has_same_parent(FolderPath other) {
FolderPath? parent = get_parent();
FolderPath? other_parent = other.get_parent();
if (parent == other_parent)
return true;
if (parent != null && other_parent != null)
return parent.equal_to(other_parent);
return false;
}
/**
* Returns the {@link FolderPath} as a single string with the supplied separator used as a
* delimiter.
*
* If null is passed in, {@link FolderRoot.default_separator} is used. If the default
* separator is null, no fullpath can be produced and this method will return null.
*
* The separator is not appended to the fullpath.
*
* @see has_default_separator
*/
public string? get_fullpath(string? use_separator) {
string? separator = use_separator ?? get_root().default_separator;
// no separator, no hierarchy
// no separator, no fullpath
if (separator == null)
return basename;
return null;
// use cached copy if the stars align
if (fullpath != null && fullpath_separator == separator)
return fullpath;
@ -118,11 +209,19 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
}
/**
* {@inheritDoc}
*
* Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
* and (b) each element is compared to the corresponding path element of the other FolderPath
* following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
* are less-than longer paths, assuming the path elements are equal up to the shorter path's
* length.
*
* Note that the {@ link FolderRoot.default_separator} has no bearing on comparisons, although
* {@link FolderRoot.case_sensitive} does.
*
* Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
* are equal.
*/
public int compare_to(Geary.FolderPath other) {
if (this == other)
@ -146,6 +245,12 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return this_list.size - other_list.size;
}
/**
* {@inheritDoc}
*
* As with {@link compare_to}, the {@link FolderRoot.default_separator} has no bearing on the
* hash, although {@link FolderRoot.case_sensitive} does.
*/
public uint hash() {
if (stored_hash != uint.MAX)
return stored_hash;
@ -168,6 +273,9 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return cs ? (basename == cmp) : (basename.down() == cmp.down());
}
/**
* {@inheritDoc}
*/
public bool equal_to(Geary.FolderPath other) {
int path_length = get_path_length();
if (other.get_path_length() != path_length)
@ -188,21 +296,44 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
}
/**
* Returns the fullpath using the default separator. Using only for debugging and logging.
* Returns the fullpath using the default separator.
*
* Use only for debugging and logging.
*/
public string to_string() {
return get_fullpath();
// use slash if no default separator available
return get_fullpath(has_default_separator() ? null : "/");
}
}
/**
* 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}.
*/
public class Geary.FolderRoot : Geary.FolderPath {
/**
* The default separator (delimiter) for this path.
*
* If null, the separator can be supplied later to {@link FolderPath.get_fullpath}.
*
* This value will never be empty (i.e. zero-length). A zero-length separator passed to the
* constructor will result in this property being null.
*/
public string? default_separator { get; private set; }
/**
* Whether this path is lexiographically case-sensitive.
*
* This has implications, as {@link FolderPath} is Comparable and Hashable.
*/
public bool case_sensitive { get; private set; }
public FolderRoot(string basename, string? default_separator, bool case_sensitive) {
base (basename);
this.default_separator = default_separator;
this.default_separator = !String.is_empty(default_separator) ? default_separator : null;
this.case_sensitive;
}
}

View file

@ -345,7 +345,7 @@ private class Geary.ImapDB.Account : BaseObject {
if (id_map.size == 0) {
throw new EngineError.NOT_FOUND("No local folders in %s",
(parent != null) ? parent.get_fullpath() : "root");
(parent != null) ? parent.to_string() : "root");
}
Gee.Collection<Geary.ImapDB.Folder> folders = new Gee.ArrayList<Geary.ImapDB.Folder>();
@ -431,8 +431,19 @@ private class Geary.ImapDB.Account : BaseObject {
private Geary.ImapDB.Folder? get_local_folder(Geary.FolderPath path) {
FolderReference? folder_ref = folder_refs.get(path);
if (folder_ref == null)
return null;
return (folder_ref != null) ? (Geary.ImapDB.Folder) folder_ref.get_reference() : null;
ImapDB.Folder? folder = (Geary.ImapDB.Folder?) folder_ref.get_reference();
if (folder == null)
return null;
// use supplied FolderPath rather than one here; if it came from the server, it has
// a usable separator
if (path.get_root().default_separator != null)
folder.set_path(path);
return folder;
}
private Geary.ImapDB.Folder create_local_folder(Geary.FolderPath path, int64 folder_id,
@ -684,10 +695,8 @@ private class Geary.ImapDB.Account : BaseObject {
return null;
}
if (parent_id <= 0) {
return new Geary.FolderRoot(name,
Geary.Imap.Account.ASSUMED_SEPARATOR, Geary.Imap.Folder.CASE_SENSITIVE);
}
if (parent_id <= 0)
return new Geary.FolderRoot(name, null, Geary.Imap.Folder.CASE_SENSITIVE);
Geary.FolderPath? parent_path = do_find_folder_path(cx, parent_id, cancellable);
return (parent_path == null ? null : parent_path.get_child(name));

View file

@ -85,6 +85,12 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return path;
}
// Use with caution; ImapDB.Account uses this to "improve" the path with one from the server,
// which has a usable path delimiter.
internal void set_path(FolderPath path) {
this.path = path;
}
public Geary.Imap.FolderProperties get_properties() {
return properties;
}

View file

@ -43,13 +43,13 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
if (path_type_map == null) {
path_type_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
path_type_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR,
path_type_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, null,
Imap.Folder.CASE_SENSITIVE), SpecialFolderType.INBOX);
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER,
Imap.Account.ASSUMED_SEPARATOR, Imap.Folder.CASE_SENSITIVE);
Geary.FolderPath googlemail_root = new Geary.FolderRoot(GOOGLEMAIL_FOLDER,
Imap.Account.ASSUMED_SEPARATOR, Imap.Folder.CASE_SENSITIVE);
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER, null,
Imap.Folder.CASE_SENSITIVE);
Geary.FolderPath googlemail_root = new Geary.FolderRoot(GOOGLEMAIL_FOLDER, null,
Imap.Folder.CASE_SENSITIVE);
path_type_map.set(gmail_root.get_child("Drafts"), SpecialFolderType.DRAFTS);
path_type_map.set(googlemail_root.get_child("Drafts"), SpecialFolderType.DRAFTS);

View file

@ -7,7 +7,6 @@
private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
private const int REFRESH_FOLDER_LIST_SEC = 10 * 60;
private static Geary.FolderPath? inbox_path = null;
private static Geary.FolderPath? outbox_path = null;
private Imap.Account remote;
@ -30,14 +29,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
this.remote.login_failed.connect(on_login_failed);
this.remote.email_sent.connect(on_email_sent);
if (inbox_path == null) {
inbox_path = new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR,
Imap.Folder.CASE_SENSITIVE);
}
if (outbox_path == null) {
if (outbox_path == null)
outbox_path = new SmtpOutboxFolderRoot();
}
}
private void check_open() throws EngineError {
@ -162,12 +155,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
return return_folders;
}
public override Gee.Collection<Geary.Folder> list_matching_folders(
Geary.FolderPath? parent) throws Error {
public override Gee.Collection<Geary.Folder> list_matching_folders(Geary.FolderPath? parent)
throws Error {
check_open();
Gee.ArrayList<Geary.Folder> matches = new Gee.ArrayList<Geary.Folder>();
foreach(FolderPath path in folder_map.keys) {
FolderPath? path_parent = path.get_parent();
if ((parent == null && path_parent == null) ||

View file

@ -40,16 +40,12 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
if (special_map == null) {
special_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
special_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false),
special_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, null, false),
Geary.SpecialFolderType.INBOX);
special_map.set(new Geary.FolderRoot("Sent", Imap.Account.ASSUMED_SEPARATOR, false),
Geary.SpecialFolderType.SENT);
special_map.set(new Geary.FolderRoot("Draft", Imap.Account.ASSUMED_SEPARATOR, false),
Geary.SpecialFolderType.DRAFTS);
special_map.set(new Geary.FolderRoot("Bulk Mail", Imap.Account.ASSUMED_SEPARATOR, false),
Geary.SpecialFolderType.SPAM);
special_map.set(new Geary.FolderRoot("Trash", Imap.Account.ASSUMED_SEPARATOR, false),
Geary.SpecialFolderType.TRASH);
special_map.set(new Geary.FolderRoot("Sent", null, false), Geary.SpecialFolderType.SENT);
special_map.set(new Geary.FolderRoot("Draft", null, false), Geary.SpecialFolderType.DRAFTS);
special_map.set(new Geary.FolderRoot("Bulk Mail", null, false), Geary.SpecialFolderType.SPAM);
special_map.set(new Geary.FolderRoot("Trash", null, false), Geary.SpecialFolderType.TRASH);
}
}

View file

@ -21,7 +21,6 @@ private class Geary.Imap.Account : BaseObject {
// all references to Inbox are converted to this string, purely for sanity sake when dealing
// with Inbox's case issues
public const string INBOX_NAME = "INBOX";
public const string ASSUMED_SEPARATOR = "/";
public bool is_open { get; private set; default = false; }
@ -177,7 +176,7 @@ private class Geary.Imap.Account : BaseObject {
private async MailboxInformation fetch_mailbox_async(FolderPath path, Cancellable? cancellable)
throws Error {
Geary.FolderPath? processed = process_path(path, null, path.get_root().default_separator);
Geary.FolderPath? processed = normalize_inbox(path);
if (processed == null)
throw new ImapError.INVALID("Invalid path %s", path.to_string());
@ -186,7 +185,7 @@ private class Geary.Imap.Account : BaseObject {
Gee.List<MailboxInformation> list_results = new Gee.ArrayList<MailboxInformation>();
StatusResponse response = yield send_command_async(
new ListCommand(new MailboxSpecifier.from_folder_path(processed), can_xlist),
new ListCommand(new MailboxSpecifier.from_folder_path(processed, null), can_xlist),
list_results, null, cancellable);
throw_fetch_error(response, processed, list_results.size);
@ -198,13 +197,13 @@ private class Geary.Imap.Account : BaseObject {
throws Error {
check_open();
Geary.FolderPath? processed = process_path(path, null, path.get_root().default_separator);
Geary.FolderPath? processed = normalize_inbox(path);
if (processed == null)
throw new ImapError.INVALID("Invalid path %s", path.to_string());
Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
StatusResponse response = yield send_command_async(
new StatusCommand(new MailboxSpecifier.from_folder_path(processed), StatusDataType.all()),
new StatusCommand(new MailboxSpecifier.from_folder_path(processed, null), StatusDataType.all()),
null, status_results, cancellable);
throw_fetch_error(response, processed, status_results.size);
@ -231,8 +230,7 @@ private class Geary.Imap.Account : BaseObject {
throws Error {
check_open();
Geary.FolderPath? processed = process_path(parent, null,
(parent != null) ? parent.get_root().default_separator : null);
Geary.FolderPath? processed = normalize_inbox(parent);
Gee.List<MailboxInformation>? child_info = yield list_children_async(processed, cancellable);
if (child_info == null || child_info.size == 0)
@ -300,8 +298,7 @@ private class Geary.Imap.Account : BaseObject {
private async Gee.List<MailboxInformation>? list_children_async(FolderPath? parent, Cancellable? cancellable)
throws Error {
Geary.FolderPath? processed = process_path(parent, null,
(parent != null) ? parent.get_root().default_separator : ASSUMED_SEPARATOR);
Geary.FolderPath? processed = normalize_inbox(parent);
ClientSession session = yield claim_session_async(cancellable);
bool can_xlist = session.capabilities.has_capability(Capabilities.XLIST);
@ -310,8 +307,12 @@ private class Geary.Imap.Account : BaseObject {
if (processed == null) {
cmd = new ListCommand.wildcarded("", new MailboxSpecifier("%"), can_xlist);
} else {
string specifier = processed.get_fullpath();
string delim = processed.get_root().default_separator;
string? specifier = processed.get_fullpath(null);
string? delim = processed.get_root().default_separator;
if (specifier == null || delim == null) {
throw new ImapError.INVALID("Unable to list children of %s: no delimiter specified",
processed.to_string());
}
specifier += specifier.has_suffix(delim) ? "%" : (delim + "%");
@ -377,38 +378,28 @@ private class Geary.Imap.Account : BaseObject {
(path != null) ? path.to_string() : "root", session_mgr.to_string());
}
// This method ensures that Inbox is dealt with in a consistent fashion throughout the
// application.
private static Geary.FolderPath? process_path(Geary.FolderPath? parent, string? basename,
string? delim) throws ImapError {
bool empty_basename = String.is_empty(basename);
// 1. Both null, done
if (parent == null && empty_basename)
// This method ensures that INBOX is dealt with in a consistent fashion throughout the
// application. In IMAP, INBOX is case-insensitive (although there is no specification on
// sensitivity of other folders) and must always be recognized as such. Thus, this method
// converts all mention of INBOX (or Inbox, or inbox, or InBoX) into a standard string.
private static Geary.FolderPath? normalize_inbox(Geary.FolderPath? path) {
if (path == null)
return null;
// 2. Parent null but basename not, create FolderRoot for Inbox
if (parent == null && !empty_basename && basename.up() == INBOX_NAME)
return new Geary.FolderRoot(INBOX_NAME, delim, false);
FolderRoot root = path.get_root();
if (root.basename.up() != INBOX_NAME)
return path;
// 3. Parent and basename supplied, verify parent is not Inbox, as IMAP does not allow it
// to have children
if (parent != null && !empty_basename && parent.get_root().basename.up() == INBOX_NAME)
throw new ImapError.INVALID("Inbox may not have children");
// create new FolderPath with normalized INBOX at its root
FolderPath new_path = new Geary.FolderRoot(INBOX_NAME, root.default_separator,
root.case_sensitive);
// 4. Parent supplied but basename is not; if parent points to Inbox, normalize it
if (parent != null && empty_basename && parent.basename.up() == INBOX_NAME)
return new Geary.FolderRoot(INBOX_NAME, delim, false);
// copy in children starting at 1 (zero is INBOX)
Gee.List<string> basenames = path.as_list();
for (int ctr = 1; ctr < basenames.size; ctr++)
new_path = new_path.get_child(basenames[ctr]);
// 5. Default behavior: create child of basename or basename as root, otherwise return parent
// unmodified
if (parent != null && !empty_basename)
return parent.get_child(basename);
if (!empty_basename)
return new Geary.FolderRoot(basename, delim, Folder.CASE_SENSITIVE);
return parent;
return new_path;
}
private void on_login_failed() {

View file

@ -446,7 +446,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));
CopyCommand cmd = new CopyCommand(msg_set,
new MailboxSpecifier.from_folder_path(destination, null));
Gee.Collection<Command> cmds = new Collection.SingleItem<Command>(cmd);
yield exec_commands_async(cmds, cancellable);
@ -462,7 +463,7 @@ private class Geary.Imap.Folder : BaseObject {
// Don't use copy_email_async followed by remove_email_async; this needs to be one
// set of commands executed in order without releasing the cmd_mutex; this is especially
// vital if positional addressing is used
cmds.add(new CopyCommand(msg_set, new MailboxSpecifier.from_folder_path(destination)));
cmds.add(new CopyCommand(msg_set, new MailboxSpecifier.from_folder_path(destination, null)));
Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
flags.add(MessageFlag.DELETED);

View file

@ -48,8 +48,21 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
init(param.decode());
}
public MailboxSpecifier.from_folder_path(FolderPath path, string? delim = null) {
init(path.get_fullpath(delim));
/**
* Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
*
* If the delimiter was supplied from a {@link ListCommand} response, it can be supplied here.
* Otherwise, the path's {@link FolderRoot.default_separator} will be used. If neither have a
* separator, {@link ImapError.INVALID} is thrown.
*/
public MailboxSpecifier.from_folder_path(FolderPath path, string? delim) throws ImapError {
string? fullpath = path.get_fullpath(delim);
if (fullpath == null) {
throw new ImapError.INVALID("Unable to convert FolderPath to MailboxSpecifier: no delimiter for %s",
path.to_string());
}
init(fullpath);
}
private void init(string decoded) {