Merge branch 'wip/181-special-folder-dupes' into 'master'

Fix special folder duplicates

Closes #181

See merge request GNOME/geary!82
This commit is contained in:
Michael Gratton 2019-01-14 14:08:49 +00:00
commit 5152581a0c
37 changed files with 1582 additions and 749 deletions

View file

@ -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

View file

@ -599,9 +599,6 @@ public class Accounts.Manager : GLib.Object {
try {
services.load(config, account, account.incoming);
services.load(config, account, account.outgoing);
debug("IMAP host name: %s", account.incoming.host);
} catch (GLib.KeyFileError err) {
throw new ConfigError.SYNTAX(err.message);
}
@ -1155,7 +1152,9 @@ public class Accounts.AccountConfigV1 : AccountConfig, GLib.Object {
string key,
Geary.FolderPath? path) {
if (path != null) {
config.set_string_list(key, path.as_list());
config.set_string_list(
key, new Gee.ArrayList<string>.wrap(path.as_array())
);
}
}
@ -1314,17 +1313,37 @@ public class Accounts.AccountConfigLegacy : AccountConfig, GLib.Object {
);
}
Gee.LinkedList<string> empty = new Gee.LinkedList<string>();
config.set_string_list(DRAFTS_FOLDER_KEY, (info.drafts_folder_path != null
? info.drafts_folder_path.as_list() : empty));
config.set_string_list(SENT_MAIL_FOLDER_KEY, (info.sent_folder_path != null
? info.sent_folder_path.as_list() : empty));
config.set_string_list(SPAM_FOLDER_KEY, (info.spam_folder_path != null
? info.spam_folder_path.as_list() : empty));
config.set_string_list(TRASH_FOLDER_KEY, (info.trash_folder_path != null
? info.trash_folder_path.as_list() : empty));
config.set_string_list(ARCHIVE_FOLDER_KEY, (info.archive_folder_path != null
? info.archive_folder_path.as_list() : empty));
Gee.ArrayList<string> empty = new Gee.ArrayList<string>();
config.set_string_list(
DRAFTS_FOLDER_KEY,
(info.drafts_folder_path != null
? new Gee.ArrayList<string>.wrap(info.drafts_folder_path.as_array())
: empty)
);
config.set_string_list(
SENT_MAIL_FOLDER_KEY,
(info.sent_folder_path != null
? new Gee.ArrayList<string>.wrap(info.sent_folder_path.as_array())
: empty)
);
config.set_string_list(
SPAM_FOLDER_KEY,
(info.spam_folder_path != null
? new Gee.ArrayList<string>.wrap(info.spam_folder_path.as_array())
: empty)
);
config.set_string_list(
TRASH_FOLDER_KEY,
(info.trash_folder_path != null
? new Gee.ArrayList<string>.wrap(info.trash_folder_path.as_array())
: empty)
);
config.set_string_list(
ARCHIVE_FOLDER_KEY,
(info.archive_folder_path != null
? new Gee.ArrayList<string>.wrap(info.archive_folder_path.as_array())
: empty)
);
config.set_bool(SAVE_DRAFTS_KEY, info.save_drafts);
}
@ -1456,8 +1475,6 @@ public class Accounts.ServiceConfigLegacy : ServiceConfig, GLib.Object {
Geary.ConfigFile.Group service_config =
config.get_group(AccountConfigLegacy.GROUP);
debug("Loading...");
string prefix = service.protocol == Geary.Protocol.IMAP
? "imap_" : "smtp_";
@ -1479,8 +1496,6 @@ public class Accounts.ServiceConfigLegacy : ServiceConfig, GLib.Object {
prefix + PORT, service.port
);
debug("Host name: %s", service.host);
bool use_tls = service_config.get_bool(
prefix + SSL, service.protocol == Geary.Protocol.IMAP
);

View file

@ -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;
@ -98,11 +98,11 @@ public class FolderList.AccountBranch : Sidebar.Branch {
graft(get_root(), user_folder_group);
}
} else {
Sidebar.Entry? entry = folder_entries.get(folder.path.get_parent());
Sidebar.Entry? entry = folder_entries.get(folder.path.parent);
if (entry != null)
graft_point = entry;
}
// Due to how we enumerate folders on the server, it's unfortunately
// possible now to have two folders that we'd put in the same place in
// our tree. In that case, we just ignore the second folder for now.

View file

@ -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;
}
@ -436,8 +437,9 @@ public class Geary.AccountInformation : BaseObject {
break;
}
if (old_path == null && new_path != null ||
old_path != null && !old_path.equal_to(new_path)) {
if ((old_path == null && new_path != null) ||
(old_path != null && new_path == null) ||
(old_path != null && !old_path.equal_to(new_path))) {
changed();
}
}

View file

@ -13,13 +13,29 @@
* @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; }
public class Geary.FolderPath :
BaseObject, Gee.Hashable<FolderPath>, Gee.Comparable<FolderPath> {
// Workaround for Vala issue #659. See children below.
private class FolderPathWeakRef {
GLib.WeakRef weak_ref;
public FolderPathWeakRef(FolderPath path) {
this.weak_ref = GLib.WeakRef(path);
}
public FolderPath? get() {
return this.weak_ref.get() as FolderPath;
}
}
/** The base name of this folder, excluding parents. */
public string name { get; private set; }
/**
* Whether this path is lexiographically case-sensitive.
*
@ -27,141 +43,97 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
*/
public bool case_sensitive { get; private set; }
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;
/** Determines if this path is a root folder path. */
public bool is_root {
get { return this.parent == null; }
}
/** Determines if this path is a child of the root folder. */
public bool is_top_level {
get {
FolderPath? parent = parent;
return parent != null && parent.is_root;
}
}
/** Returns the parent of this path. */
public FolderPath? parent { get; private set; }
private string[] path;
// Would use a `weak FolderPath` value type for this map instead of
// the custom class, but we can't currently reassign built-in
// weak refs back to a strong ref at the moment, nor use a
// GLib.WeakRef as a generics param. See Vala issue #659.
private Gee.Map<string,FolderPathWeakRef?> children =
new Gee.HashMap<string,FolderPathWeakRef?>();
private uint? stored_hash = null;
/** Constructor only for use by {@link FolderRoot}. */
internal FolderPath() {
this.name = "";
this.parent = null;
this.case_sensitive = false;
this.path = new string[0];
}
private FolderPath.child(FolderPath parent,
string name,
bool case_sensitive) {
this.parent = parent;
this.name = name;
this.case_sensitive = case_sensitive;
this.path = parent.path.copy();
this.path += name;
}
private FolderPath.child(Gee.List<Geary.FolderPath> path, string basename, bool case_sensitive) {
assert(path[0] is FolderRoot);
this.path = path;
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.
*/
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, 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
// look like "this" is stored at the end of the path list
if (path == null)
return (index == 0) ? this : null;
int length = path.size;
if (index < length)
return path[index];
if (index == length)
return this;
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>();
if (path != null) {
foreach (Geary.FolderPath folder in path)
list.add(folder.basename);
FolderPath? path = this;
while (path.parent != null) {
path = path.parent;
}
list.add(basename);
return list;
return (FolderRoot) path;
}
/**
* Creates a {@link FolderPath} object 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}.
* Returns an array of the names of non-root elements in the path.
*/
public Geary.FolderPath get_child(string basename, Trillian child_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));
public string[] as_array() {
return this.path;
}
/**
* Returns true if the other {@link FolderPath} has the same parent as this one.
* Creates a path that is a child of this folder.
*
* Like {@link equal_to} and {@link compare_to}, this comparison the comparison is
* lexiographic, not by reference.
* 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 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;
public virtual FolderPath
get_child(string name,
Trillian is_case_sensitive = Trillian.UNKNOWN) {
FolderPath? child = null;
FolderPathWeakRef? child_ref = this.children.get(name);
if (child_ref != null) {
child = child_ref.get();
}
if (child == null) {
child = new FolderPath.child(
this,
name,
is_case_sensitive.to_boolean(
get_root().default_case_sensitivity
)
);
this.children.set(name, new FolderPathWeakRef(child));
}
return child;
}
/**
@ -169,124 +141,96 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
*/
public bool is_descendant(FolderPath target) {
bool is_descendent = false;
Geary.FolderPath? path = target.get_parent();
FolderPath? path = target.parent;
while (path != null) {
if (path.equal_to(this)) {
is_descendent = true;
break;
}
path = path.get_parent();
path = path.parent;
}
return is_descendent;
}
private uint get_basename_hash() {
return case_sensitive ? str_hash(basename) : str_hash(basename.down());
}
private int compare_internal(Geary.FolderPath other, bool allow_case_sensitive, bool normalize) {
if (this == other)
return 0;
// walk elements using as_list() as that includes the basename (whereas path does not),
// avoids the null problem, and makes comparisons straightforward
Gee.List<string> this_list = as_list();
Gee.List<string> other_list = other.as_list();
// if paths exist, do comparison of each parent in order
int min = int.min(this_list.size, other_list.size);
for (int ctr = 0; ctr < min; ctr++) {
string this_element = this_list[ctr];
string other_element = other_list[ctr];
if (normalize) {
this_element = this_element.normalize();
other_element = other_element.normalize();
}
if (!allow_case_sensitive
// if either case-sensitive, then comparison is CS
|| (!get_folder_at(ctr).case_sensitive && !other.get_folder_at(ctr).case_sensitive)) {
this_element = this_element.casefold();
other_element = other_element.casefold();
}
int result = this_element.collate(other_element);
if (result != 0)
return result;
}
// paths up to the min element count are equal, shortest path is less-than, otherwise
// equal paths
return this_list.size - other_list.size;
}
/**
* Does a Unicode-normalized, case insensitive match. Useful for getting a rough idea if
* a folder matches a name, but shouldn't be used to determine strict equality.
* Does a Unicode-normalized, case insensitive match. Useful for
* getting a rough idea if a folder matches a name, but shouldn't
* be used to determine strict equality.
*/
public int compare_normalized_ci(Geary.FolderPath other) {
public int compare_normalized_ci(FolderPath other) {
return compare_internal(other, false, true);
}
/**
* {@inheritDoc}
*
* Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
* and (b) each element is compared to the corresponding path element of the other FolderPath
* following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
* are less-than longer paths, assuming the path elements are equal up to the shorter path's
* Comparisons for 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 {@link FolderPath.case_sensitive} affects comparisons.
*
* Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
* are equal.
* 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) {
public int compare_to(FolderPath other) {
return compare_internal(other, true, false);
}
/**
* {@inheritDoc}
*
* Note that {@link FolderPath.case_sensitive} affects comparisons.
*/
public uint hash() {
if (stored_hash != uint.MAX)
return stored_hash;
// always one element in path
stored_hash = get_folder_at(0).get_basename_hash();
int path_length = get_path_length();
for (int ctr = 1; ctr < path_length; ctr++)
stored_hash ^= get_folder_at(ctr).get_basename_hash();
return stored_hash;
}
private bool is_basename_equal(string cmp, bool other_cs) {
// case-sensitive comparison if either is sensitive
return (other_cs || case_sensitive) ? (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)
return false;
for (int ctr = 0; ctr < path_length; ctr++) {
// this should never return null as length is already checked
FolderPath? other_folder = other.get_folder_at(ctr);
assert(other_folder != null);
if (!get_folder_at(ctr).is_basename_equal(other_folder.basename, other_folder.case_sensitive))
return false;
if (this.stored_hash == null) {
this.stored_hash = 0;
FolderPath? path = this;
while (path != null) {
this.stored_hash ^= (case_sensitive)
? str_hash(path.name) : str_hash(path.name.down());
path = path.parent;
}
}
return this.stored_hash;
}
/** {@inheritDoc} */
public bool equal_to(FolderPath other) {
if (this == other) {
return true;
}
FolderPath? a = this;
FolderPath? b = other;
while (a != null || b != null) {
if (a == b) {
return true;
}
if ((a != null && b == null) ||
(a == null && b != null)) {
return false;
}
if (a.case_sensitive || b.case_sensitive) {
if (a.name != b.name) {
return false;
}
} else {
if (a.name.down() != b.name.down()) {
return false;
}
}
a = a.parent;
b = b.parent;
}
return true;
}
@ -299,41 +243,96 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
* instead. This method is useful for debugging and logging only.
*/
public string to_string() {
const char SEP = '>';
StringBuilder builder = new StringBuilder();
if (this.path != null) {
foreach (Geary.FolderPath folder in this.path) {
builder.append(folder.basename);
builder.append_c('>');
if (this.is_root) {
builder.append_c(SEP);
} else {
foreach (string name in this.path) {
builder.append_c(SEP);
builder.append(name);
}
}
builder.append(basename);
return builder.str;
}
private int compare_internal(FolderPath other,
bool allow_case_sensitive,
bool normalize) {
if (this == other)
return 0;
FolderPath a = this;
FolderPath b = other;
// Get the common-length prefix of both
while (a.path.length != b.path.length) {
if (a.path.length > b.path.length) {
a = a.parent;
} else if (b.path.length > a.path.length) {
b = b.parent;
}
}
// Compare the common-length prefixes of both
while (a != null && b != null) {
string a_name = a.name;
string b_name = b.name;
if (normalize) {
a_name = a_name.normalize();
b_name = b_name.normalize();
}
if (!allow_case_sensitive
// if either case-sensitive, then comparison is CS
|| (!a.case_sensitive && !b.case_sensitive)) {
a_name = a_name.casefold();
b_name = b_name.casefold();
}
int result = a_name.collate(b_name);
if (result != 0) {
return result;
}
a = a.parent;
b = b.parent;
}
// paths up to the min element count are equal, shortest path
// is less-than, otherwise equal paths
return this.path.length - other.path.length;
}
}
/**
* The root of a folder heirarchy.
* The root of a folder hierarchy.
*
* 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}.
*
* 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 : FolderPath {
/**
* 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;
}
}
}

View file

@ -451,16 +451,16 @@ public abstract class Geary.Folder : BaseObject {
protected virtual void notify_display_name_changed() {
display_name_changed();
}
/**
* Returns a name suitable for displaying to the user.
*
* Default is to display the basename of the Folder's path, unless it's a special folder,
* Default is to display the name of the Folder's path, unless it's a special folder,
* in which case {@link SpecialFolderType.get_display_name} is returned.
*/
public virtual string get_display_name() {
return (special_folder_type == Geary.SpecialFolderType.NONE)
? path.basename : special_folder_type.get_display_name();
? path.name : special_folder_type.get_display_name();
}
/** Determines if a folder has been opened, and if so in which way. */

View file

@ -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); }
@ -326,18 +341,7 @@ private class Geary.ImapDB.Account : BaseObject {
throw err;
}
Geary.Account account;
try {
account = Geary.Engine.instance.get_account_instance(account_information);
} catch (Error e) {
// If they're opening an account, the engine should already be
// open, and there should be no reason for this to fail. Thus, if
// we get here, it's a programmer error.
error("Error finding account from its information: %s", e.message);
}
background_cancellable = new Cancellable();
// Kick off a background update of the search table, but since the database is getting
@ -380,10 +384,19 @@ private class Geary.ImapDB.Account : BaseObject {
// XXX this should really be a db table constraint
Geary.ImapDB.Folder? folder = get_local_folder(path);
if (folder != null)
if (folder != null) {
throw new EngineError.ALREADY_EXISTS(
"Folder with path already exists: %s", path.to_string()
);
}
if (Imap.MailboxSpecifier.folder_path_is_inbox(path) &&
!Imap.MailboxSpecifier.is_canonical_inbox_name(path.name)) {
// Don't add faux inboxes
throw new ImapError.NOT_SUPPORTED(
"Inbox has : %s", path.to_string()
);
}
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
// get the parent of this folder, creating parents if necessary ... ok if this fails,
@ -399,7 +412,7 @@ private class Geary.ImapDB.Account : BaseObject {
Db.Statement stmt = cx.prepare(
"INSERT INTO FolderTable (name, parent_id, last_seen_total, last_seen_status_total, "
+ "uid_validity, uid_next, attributes, unread_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
stmt.bind_string(0, path.basename);
stmt.bind_string(0, path.name);
stmt.bind_rowid(1, parent_id);
stmt.bind_int(2, Numeric.int_floor(properties.select_examine_messages, 0));
stmt.bind_int(3, Numeric.int_floor(properties.status_messages, 0));
@ -419,21 +432,23 @@ private class Geary.ImapDB.Account : BaseObject {
return yield fetch_folder_async(path, cancellable);
}
public async void delete_folder_async(Geary.Folder folder, Cancellable? cancellable)
throws Error {
public async void delete_folder_async(Geary.FolderPath path,
GLib.Cancellable? cancellable)
throws GLib.Error {
check_open();
Geary.FolderPath path = folder.path;
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
int64 folder_id;
do_fetch_folder_id(cx, path, false, out folder_id, cancellable);
if (folder_id == Db.INVALID_ROWID)
return Db.TransactionOutcome.ROLLBACK;
if (folder_id == Db.INVALID_ROWID) {
throw new EngineError.NOT_FOUND(
"Folder not found: %s", path.to_string()
);
}
if (do_has_children(cx, folder_id, cancellable)) {
debug("Can't delete folder %s because it has children", folder.to_string());
return Db.TransactionOutcome.ROLLBACK;
throw new ImapError.NOT_SUPPORTED(
"Folder has children: %s", path.to_string()
);
}
do_delete_folder(cx, folder_id, cancellable);
@ -441,7 +456,6 @@ private class Geary.ImapDB.Account : BaseObject {
return Db.TransactionOutcome.COMMIT;
}, cancellable);
}
private void initialize_contacts(Cancellable? cancellable = null) throws Error {
@ -477,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<
@ -490,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(
@ -512,24 +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");
// ignore anything that's not canonical Inbox
if (parent == null
&& Imap.MailboxSpecifier.is_inbox_name(basename)
&& !Imap.MailboxSpecifier.is_canonical_inbox_name(basename)) {
result.next(cancellable);
continue;
}
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"),
@ -554,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);
@ -1565,23 +1572,28 @@ 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());
folder_id = Db.INVALID_ROWID;
// 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"
);
}
string[] parts = path.as_array();
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;
folder_id = Db.INVALID_ROWID;
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=?");
@ -1626,19 +1638,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.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);
@ -1710,8 +1731,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";
@ -1734,16 +1759,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;
@ -1756,12 +1785,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>

View file

@ -1,14 +0,0 @@
/* 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.
*/
private class Geary.ImapDB.SearchFolderRoot : Geary.FolderRoot {
public const string MAGIC_BASENAME = "$GearySearchFolder$";
public SearchFolderRoot() {
base(MAGIC_BASENAME, false, false);
}
}

View file

@ -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);

View file

@ -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.GmailAccount : Geary.ImapEngine.GenericAccount {
@ -44,30 +46,36 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
}
protected override MinimalFolder new_folder(ImapDB.Folder local_folder) {
Geary.FolderPath path = local_folder.get_path();
SpecialFolderType special_folder_type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
special_folder_type = SpecialFolderType.INBOX;
else
special_folder_type = local_folder.get_properties().attrs.get_special_folder_type();
FolderPath path = local_folder.get_path();
SpecialFolderType type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path)) {
type = SpecialFolderType.INBOX;
} else {
type = local_folder.get_properties().attrs.get_special_folder_type();
// There can be only one Inbox
if (type == SpecialFolderType.INBOX) {
type = SpecialFolderType.NONE;
}
}
switch (special_folder_type) {
switch (type) {
case SpecialFolderType.ALL_MAIL:
return new GmailAllMailFolder(this, local_folder, special_folder_type);
return new GmailAllMailFolder(this, local_folder, type);
case SpecialFolderType.DRAFTS:
return new GmailDraftsFolder(this, local_folder, special_folder_type);
return new GmailDraftsFolder(this, local_folder, type);
case SpecialFolderType.SPAM:
case SpecialFolderType.TRASH:
return new GmailSpamTrashFolder(this, local_folder, special_folder_type);
return new GmailSpamTrashFolder(this, local_folder, type);
default:
return new GmailFolder(this, local_folder, special_folder_type);
return new GmailFolder(this, local_folder, type);
}
}
protected override SearchFolder new_search_folder() {
return new GmailSearchFolder(this);
return new GmailSearchFolder(this, this.local_folder_root);
}
}

View file

@ -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);
}

View file

@ -42,7 +42,6 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
// we do require that for syncing at the moment anyway,
// but keep the tests in for that one glorious day where
// we can just use a generic folder.
debug("Is folder \"%s\" openable: %s", folder.path.to_string(), folder.properties.is_openable.to_string());
MinimalFolder? imap_folder = folder as MinimalFolder;
if (imap_folder != null &&
folder.properties.is_openable.is_possible() &&

View file

@ -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;
@ -411,7 +421,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return Geary.traverse<FolderPath>(folder_map.keys)
.filter(p => {
FolderPath? path_parent = p.get_parent();
FolderPath? path_parent = p.parent;
return ((parent == null && path_parent == null) ||
(parent != null && path_parent != null && path_parent.equal_to(parent)));
})
@ -628,6 +638,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
foreach (Geary.SpecialFolderType special in specials.keys) {
MinimalFolder? minimal = specials.get(special) as MinimalFolder;
if (minimal.special_folder_type != special) {
debug("%s: Promoting %s to %s",
to_string(), minimal.to_string(), special.to_string());
minimal.set_special_folder_type(special);
changed.add(minimal);
@ -683,80 +695,94 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/**
* Locates a special folder, creating it if needed.
*/
internal async Geary.Folder ensure_special_folder_async(Imap.AccountSession remote,
Geary.SpecialFolderType special,
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.
Geary.FolderPath root =
yield remote.get_default_personal_namespace(cancellable);
Gee.List<string> search_names = special_search_names.get(special);
foreach (string search_name in search_names) {
Geary.FolderPath search_path = root.get_child(search_name);
foreach (Geary.FolderPath test_path in folder_map.keys) {
if (test_path.compare_normalized_ci(search_path) == 0) {
path = search_path;
internal async Folder
ensure_special_folder_async(Imap.AccountSession remote,
SpecialFolderType type,
GLib.Cancellable? cancellable)
throws GLib.Error {
Folder? special = get_special_folder(type);
if (special == null) {
FolderPath? path = information.get_special_folder_path(type);
if (path != null && !remote.is_folder_path_valid(path)) {
debug("%s: Ignoring bad special folder path '%s' for type %s",
to_string(),
path.to_string(),
type.to_string());
path = null;
}
if (path == null) {
FolderPath root =
yield remote.get_default_personal_namespace(cancellable);
Gee.List<string> search_names = special_search_names.get(type);
foreach (string search_name in search_names) {
FolderPath search_path = root.get_child(search_name);
foreach (FolderPath test_path in folder_map.keys) {
if (test_path.compare_normalized_ci(search_path) == 0) {
path = search_path;
break;
}
}
if (path != null)
break;
}
if (path == null) {
path = root.get_child(search_names[0]);
}
debug("%s: Guessed folder \'%s\' for special_path %s",
to_string(), path.to_string(), type.to_string()
);
information.set_special_folder_path(type, path);
}
if (!this.folder_map.has_key(path)) {
debug("%s: Creating \"%s\" to use as special folder %s",
to_string(), path.to_string(), type.to_string());
GLib.Error? created_err = null;
try {
yield remote.create_folder_async(path, type, cancellable);
} catch (GLib.Error err) {
// Hang on to the error since the folder might exist
// on the remote, so try fetching it anyway.
created_err = err;
}
Imap.Folder? remote_folder = null;
try {
remote_folder = yield remote.fetch_folder_async(
path, cancellable
);
} catch (GLib.Error err) {
// If we couldn't fetch it after also failing to
// create it, it's probably due to the problem
// creating it, so throw that error instead.
if (created_err != null) {
throw created_err;
} else {
throw err;
}
}
if (path != null)
break;
}
if (path == null)
path = root.get_child(search_names[0]);
information.set_special_folder_path(special, path);
}
if (!this.folder_map.has_key(path)) {
debug("Creating \"%s\" to use as special folder %s",
path.to_string(), special.to_string());
GLib.Error? created_err = null;
try {
yield remote.create_folder_async(path, special, cancellable);
} catch (GLib.Error err) {
// Hang on to the error since the folder might exist
// on the remote, so try fetching it anyway.
created_err = err;
}
Imap.Folder? remote_folder = null;
try {
remote_folder = yield remote.fetch_folder_async(
path, cancellable
ImapDB.Folder local_folder =
yield this.local.clone_folder_async(
remote_folder, cancellable
);
add_folders(
Collection.single(local_folder), created_err != null
);
} catch (GLib.Error err) {
// If we couldn't fetch it after also failing to
// create it, it's probably due to the problem
// creating it, so throw that error instead.
if (created_err != null) {
throw created_err;
} else {
throw err;
}
}
ImapDB.Folder local_folder = yield this.local.clone_folder_async(
remote_folder, cancellable
special= this.folder_map.get(path);
promote_folders(
Collection.single_map<SpecialFolderType,Folder>(
type, special
)
);
add_folders(Collection.single(local_folder), created_err != null);
}
Geary.Folder special_folder = this.folder_map.get(path);
promote_folders(
Collection.single_map<Geary.SpecialFolderType,Geary.Folder>(
special, special_folder
)
);
return special_folder;
return special;
}
/**
@ -779,7 +805,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 +1054,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 +1067,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;
@ -1062,25 +1090,43 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation {
}
}
private async void check_special_folders(Cancellable cancellable)
throws Error {
private async void check_special_folders(GLib.Cancellable cancellable)
throws GLib.Error {
// Local folders loaded that have the SPECIAL-USE flags set
// will have been promoted already via derived account type's
// new_child overrides or some other means. However for those
// that do not have the flag, check here against the local
// config and promote ASAP.
//
// Can't just use ensure_special_folder_async however since
// that will attempt to create the folders if missing, which
// is bad if offline.
GenericAccount generic = (GenericAccount) this.account;
Gee.Map<Geary.SpecialFolderType,Geary.Folder> specials =
Gee.Map<Geary.SpecialFolderType,Geary.Folder> added_specials =
new Gee.HashMap<Geary.SpecialFolderType,Geary.Folder>();
foreach (Geary.SpecialFolderType special in this.specials) {
Geary.FolderPath? path = generic.information.get_special_folder_path(special);
if (path != null) {
try {
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",
generic.information.id, special.to_string(), err.message);
foreach (Geary.SpecialFolderType type in this.specials) {
if (generic.get_special_folder(type) == null) {
Geary.FolderPath? path =
generic.information.get_special_folder_path(type);
if (path != null) {
try {
Geary.Folder target = yield generic.fetch_folder_async(
path, cancellable
);
added_specials.set(type, target);
} catch (Error err) {
debug(
"%s: Previously used special folder %s not loaded: %s",
generic.information.id,
type.to_string(),
err.message
);
}
}
}
}
generic.promote_folders(specials);
generic.promote_folders(added_specials);
}
}
@ -1137,9 +1183,21 @@ internal class Geary.ImapEngine.UpdateRemoteFolders : AccountOperation {
);
try {
bool is_suspect = yield enumerate_remote_folders_async(
remote, remote_folders, null, cancellable
remote,
remote_folders,
account.local.imap_folder_root,
cancellable
);
debug("Existing folders:");
foreach (FolderPath path in existing_folders.keys) {
debug(" - %s (%u)", path.to_string(), path.hash());
}
debug("Remote folders:");
foreach (FolderPath path in remote_folders.keys) {
debug(" - %s (%u)", path.to_string(), path.hash());
}
// pair the local and remote folders and make sure
// everything is up-to-date
yield update_folders_async(
@ -1264,11 +1322,13 @@ internal class Geary.ImapEngine.UpdateRemoteFolders : AccountOperation {
this.generic_account.remove_folders(to_remove);
// Sort by path length descending, so we always remove children first.
removed.sort((a, b) => b.path.get_path_length() - a.path.get_path_length());
removed.sort(
(a, b) => b.path.as_array().length - a.path.as_array().length
);
foreach (Geary.Folder folder in removed) {
try {
debug("Locally deleting removed folder %s", folder.to_string());
yield local.delete_folder_async(folder, cancellable);
yield local.delete_folder_async(folder.path, cancellable);
} catch (Error e) {
debug("Unable to locally delete removed folder %s: %s", folder.to_string(), e.message);
}

View file

@ -960,12 +960,18 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
);
return;
} catch (Error err) {
debug("Other error: %s", err.message);
// Notify that there was a connection error, but don't
// force the folder closed, since it might come good again
// if the user fixes an auth problem or the network comes
// back or whatever.
notify_open_failed(Folder.OpenFailed.REMOTE_ERROR, err);
ErrorContext context = new ErrorContext(err);
if (is_unrecoverable_failure(err)) {
debug("Unrecoverable failure opening remote, forcing closed: %s",
context.format_full_error());
yield force_close(
CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR
);
} else {
debug("Recoverable error opening remote: %s",
context.format_full_error());
notify_open_failed(Folder.OpenFailed.REMOTE_ERROR, err);
}
return;
}

View file

@ -519,11 +519,11 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
} catch (Error replay_err) {
debug("Replay remote error for %s on %s: %s (%s)", op.to_string(), to_string(),
replay_err.message, op.on_remote_error.to_string());
// If a hard failure and operation allows remote replay and not closing,
// re-schedule now
// If a recoverable failure and operation allows
// remote replay and not closing, re-schedule now
if ((op.on_remote_error == ReplayOperation.OnError.RETRY)
&& is_hard_failure(replay_err)
&& !is_unrecoverable_failure(replay_err)
&& state == State.OPEN) {
debug("Schedule op retry %s on %s", op.to_string(), to_string());

View file

@ -6,40 +6,51 @@
namespace Geary.ImapEngine {
/**
* A hard failure is defined as one due to hardware or connectivity issues, where a soft failure
* is due to software reasons, like credential failure or protocol violation.
*/
private static bool is_hard_failure(Error err) {
// CANCELLED is not a hard error
if (err is IOError.CANCELLED)
return false;
/**
* Determines if retrying an operation might succeed or not.
*
* A recoverable failure is defined as one that may not occur
* again if the operation that caused it is retried, without
* needing to make some change in the mean time. For example,
* recoverable failures may occur due to transient network
* connectivity issues or server rate limiting. On the other hand,
* an unrecoverable failure is due to some problem that will not
* succeed if tried again unless some action is taken, such as
* authentication failures, protocol parsing errors, and so on.
*/
private static bool is_unrecoverable_failure(GLib.Error err) {
return !(
err is EngineError.SERVER_UNAVAILABLE ||
err is IOError.BROKEN_PIPE ||
err is IOError.BUSY ||
err is IOError.CONNECTION_CLOSED ||
err is IOError.NOT_CONNECTED ||
err is IOError.TIMED_OUT ||
err is ImapError.NOT_CONNECTED ||
err is ImapError.TIMED_OUT ||
err is ImapError.UNAVAILABLE
);
}
// Treat other errors -- most likely IOErrors -- as hard failures
if (!(err is ImapError) && !(err is EngineError))
return true;
return err is ImapError.NOT_CONNECTED
|| err is ImapError.TIMED_OUT
|| err is ImapError.SERVER_ERROR
|| err is EngineError.SERVER_UNAVAILABLE;
}
/**
* Determines if this IOError related to a remote host or not.
*/
private static bool is_remote_error(GLib.Error err) {
return err is ImapError
|| err is IOError.CONNECTION_CLOSED
|| err is IOError.CONNECTION_REFUSED
|| err is IOError.HOST_UNREACHABLE
|| err is IOError.MESSAGE_TOO_LARGE
|| err is IOError.NETWORK_UNREACHABLE
|| err is IOError.NOT_CONNECTED
|| err is IOError.PROXY_AUTH_FAILED
|| err is IOError.PROXY_FAILED
|| err is IOError.PROXY_NEED_AUTH
|| err is IOError.PROXY_NOT_ALLOWED;
}
/**
* Determines if an error was caused by the remote host or not.
*/
private static bool is_remote_error(GLib.Error err) {
return (
err is EngineError.NOT_FOUND ||
err is EngineError.SERVER_UNAVAILABLE ||
err is IOError.CONNECTION_CLOSED ||
err is IOError.CONNECTION_REFUSED ||
err is IOError.HOST_UNREACHABLE ||
err is IOError.MESSAGE_TOO_LARGE ||
err is IOError.NETWORK_UNREACHABLE ||
err is IOError.NOT_CONNECTED ||
err is IOError.PROXY_AUTH_FAILED ||
err is IOError.PROXY_FAILED ||
err is IOError.PROXY_NEED_AUTH ||
err is IOError.PROXY_NOT_ALLOWED ||
err is ImapError
);
}
}

View file

@ -1,8 +1,9 @@
/*
* 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.OtherAccount : Geary.ImapEngine.GenericAccount {
@ -15,12 +16,17 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
}
protected override MinimalFolder new_folder(ImapDB.Folder local_folder) {
Geary.FolderPath path = local_folder.get_path();
FolderPath path = local_folder.get_path();
SpecialFolderType type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
if (Imap.MailboxSpecifier.folder_path_is_inbox(path)) {
type = SpecialFolderType.INBOX;
else
} else {
type = local_folder.get_properties().attrs.get_special_folder_type();
// There can be only one Inbox
if (type == SpecialFolderType.INBOX) {
type = SpecialFolderType.NONE;
}
}
return new OtherFolder(this, local_folder, type);
}

View file

@ -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.OutlookAccount : Geary.ImapEngine.GenericAccount {
@ -32,19 +34,22 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
}
protected override MinimalFolder new_folder(ImapDB.Folder local_folder) {
// use the Folder's attributes to determine if it's a special folder type, unless it's
// INBOX; that's determined by name
Geary.FolderPath path = local_folder.get_path();
SpecialFolderType special_folder_type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
special_folder_type = SpecialFolderType.INBOX;
else
special_folder_type = local_folder.get_properties().attrs.get_special_folder_type();
FolderPath path = local_folder.get_path();
SpecialFolderType type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path)) {
type = SpecialFolderType.INBOX;
} else {
type = local_folder.get_properties().attrs.get_special_folder_type();
// There can be only one Inbox
if (type == SpecialFolderType.INBOX) {
type = SpecialFolderType.NONE;
}
}
if (special_folder_type == Geary.SpecialFolderType.DRAFTS)
return new OutlookDraftsFolder(this, local_folder, special_folder_type);
if (type == Geary.SpecialFolderType.DRAFTS)
return new OutlookDraftsFolder(this, local_folder, type);
return new OutlookFolder(this, local_folder, special_folder_type);
return new OutlookFolder(this, local_folder, type);
}
}

View file

@ -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
);
}
}

View file

@ -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;
}
/**
@ -139,17 +161,18 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
/**
* 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> fetch_child_folders_async(FolderPath? parent, Cancellable? cancellable)
throws Error {
public async Gee.List<Folder>
fetch_child_folders_async(FolderPath parent,
GLib.Cancellable? cancellable)
throws GLib.Error {
ClientSession session = claim_session();
Gee.List<Imap.Folder> children = new Gee.ArrayList<Imap.Folder>();
Gee.List<MailboxInformation> mailboxes = yield send_list_async(session, parent, true, cancellable);
Gee.List<MailboxInformation> mailboxes = yield send_list_async(
session, parent, true, cancellable
);
if (mailboxes.size == 0) {
return children;
}
@ -172,7 +195,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 +248,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) {
@ -269,7 +296,7 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
// Performs a LIST against the server, returning the results
private async Gee.List<MailboxInformation> send_list_async(ClientSession session,
FolderPath? folder,
FolderPath folder,
bool list_children,
Cancellable? cancellable)
throws Error {
@ -283,7 +310,7 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
}
ListCommand cmd;
if (folder == null) {
if (folder.is_root) {
// List the server root
cmd = new ListCommand.wildcarded(
"", new MailboxSpecifier("%"), can_xlist, return_param
@ -314,7 +341,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();

View file

@ -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);
}
}

View file

@ -84,14 +84,14 @@ 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.name);
}
/**
* Returns true if the string is the name of the IMAP Inbox.
*
* This accounts for IMAP's Inbox name being case-insensitive. This is only for comparing
* folder basenames; this does not account for path delimiters.
* folder names; this does not account for path delimiters.
*
* See [[http://tools.ietf.org/html/rfc3501#section-5.1]]
*
@ -115,30 +115,45 @@ 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 {
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");
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"
);
}
// 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);
string[] parts = path.as_array();
if (parts.length > 1 && delim == null) {
throw new ImapError.NOT_SUPPORTED(
"Path has more than one part but no delimiter given"
);
}
if (String.is_empty_or_whitespace(parts[0])) {
throw new ImapError.NOT_SUPPORTED(
"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++) {
foreach (string name in parts[1:parts.length]) {
if (String.is_empty_or_whitespace(name)) {
throw new ImapError.NOT_SUPPORTED(
"Path contains empty part: '%s'", path.to_string()
);
}
builder.append(delim);
builder.append(parts[i]);
builder.append(name);
}
init(builder.str);
@ -156,7 +171,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 +186,36 @@ 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])
);
list.remove_at(0);
foreach (string name in list) {
path = path.get_child(name);
}
return path;
}
/**
* The mailbox's name without parent folders.
*

View file

@ -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());
}
}
}

View file

@ -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);
}
/**
@ -532,21 +533,23 @@ public class Geary.Imap.ClientSession : BaseObject {
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)) {
FolderRoot root = (FolderRoot) path.get_root();
if (root.inbox.equal_to(path) ||
root.inbox.is_descendant(path)) {
delim = this.inbox.delim;
} else {
Namespace? ns = this.namespaces.get(root.basename);
if (ns == null) {
// Folder's root doesn't exist as a namespace, so try
// the empty namespace.
ns = this.namespaces.get("");
if (ns == null) {
// If that doesn't exist, fall back to the default
// personal namespace
ns = this.personal_namespaces[0];
}
Namespace? ns = null;
FolderPath? search = path;
while (ns == null && search != null) {
ns = this.namespaces.get(search.name);
search = search.parent;
}
if (ns == null) {
// fall back to the default personal namespace
ns = this.personal_namespaces[0];
}
delim = ns.delim;
}
return delim;

View file

@ -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',

View file

@ -1,18 +0,0 @@
/*
* 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.
*/
private class Geary.Outbox.FolderRoot : Geary.FolderRoot {
public const string MAGIC_BASENAME = "$GearyOutbox$";
public FolderRoot() {
base(MAGIC_BASENAME, false, false);
}
}

View file

@ -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;
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2017 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.MockFolderRoot : FolderRoot {
public MockFolderRoot(string name) {
base(name, false, false);
}
}

View file

@ -0,0 +1,249 @@
/*
* 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 {
private FolderRoot? root = null;
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);
add_test("as_array", as_array);
add_test("is_top_level", is_top_level);
add_test("path_to_string", path_to_string);
add_test("path_parent", path_parent);
add_test("path_equal", path_equal);
add_test("path_hash", path_hash);
add_test("path_compare", path_compare);
add_test("path_compare_normalised", path_compare_normalised);
}
public override void set_up() {
this.root = new FolderRoot(false);
}
public override void tear_down() {
this.root = null;
}
public void get_child_from_root() throws GLib.Error {
assert_string(
"test",
this.root.get_child("test").name
);
}
public void get_child_from_child() throws GLib.Error {
assert_string(
"test2",
this.root.get_child("test1").get_child("test2").name
);
}
public void root_is_root() throws GLib.Error {
assert_true(this.root.is_root);
}
public void child_root_is_not_root() throws GLib.Error {
assert_false(this.root.get_child("test").is_root);
}
public void as_array() throws GLib.Error {
assert_true(this.root.as_array().length == 0, "Root list");
assert_int(
1,
this.root.get_child("test").as_array().length,
"Child array length"
);
assert_string(
"test",
this.root.get_child("test").as_array()[0],
"Child array contents"
);
assert_int(
2,
this.root.get_child("test1").get_child("test2").as_array().length,
"Descendent array length"
);
assert_string(
"test1",
this.root.get_child("test1").get_child("test2").as_array()[0],
"Descendent first child"
);
assert_string(
"test2",
this.root.get_child("test1").get_child("test2").as_array()[1],
"Descendent second child"
);
}
public void is_top_level() throws GLib.Error {
assert_false(this.root.is_top_level, "Root is top_level");
assert_true(
this.root.get_child("test").is_top_level,
"Top level is top_level"
);
assert_false(
this.root.get_child("test").get_child("test").is_top_level,
"Descendent is top_level"
);
}
public void path_to_string() throws GLib.Error {
assert_string(">", this.root.to_string());
assert_string(">test", this.root.get_child("test").to_string());
assert_string(
">test1>test2",
this.root.get_child("test1").get_child("test2").to_string()
);
}
public void path_parent() throws GLib.Error {
assert_null(this.root.parent, "Root parent");
assert_string(
"",
this.root.get_child("test").parent.name,
"Root child parent");
assert_string(
"test1",
this.root.get_child("test1").get_child("test2").parent.name,
"Child parent");
}
public void path_equal() throws GLib.Error {
assert_true(this.root.equal_to(this.root), "Root equality");
assert_true(
this.root.get_child("test").equal_to(this.root.get_child("test")),
"Child equality"
);
assert_false(
this.root.get_child("test1").equal_to(this.root.get_child("test2")),
"Child names"
);
assert_false(
this.root.get_child("test1").get_child("test")
.equal_to(this.root.get_child("test2").get_child("test")),
"Disjoint parents"
);
assert_false(
this.root.get_child("test").equal_to(
this.root.get_child("").get_child("test")),
"Pathological case"
);
}
public void path_hash() throws GLib.Error {
assert_true(
this.root.hash() !=
this.root.get_child("test").hash()
);
assert_true(
this.root.get_child("test1").hash() !=
this.root.get_child("test2").hash()
);
}
public void path_compare() throws GLib.Error {
assert_int(0, this.root.compare_to(this.root), "Root equality");
assert_int(0,
this.root.get_child("test").compare_to(this.root.get_child("test")),
"Equal child comparison"
);
assert_int(
-1,
this.root.get_child("test1").compare_to(this.root.get_child("test2")),
"Greater than child comparison"
);
assert_int(
1,
this.root.get_child("test2").compare_to(this.root.get_child("test1")),
"Less than child comparison"
);
assert_int(
-1,
this.root.get_child("test1").get_child("test")
.compare_to(this.root.get_child("test2").get_child("test")),
"Greater than disjoint parents"
);
assert_int(
1,
this.root.get_child("test2").get_child("test")
.compare_to(this.root.get_child("test1").get_child("test")),
"Less than disjoint parents"
);
assert_int(
1,
this.root.get_child("test1").get_child("test")
.compare_to(this.root.get_child("test1")),
"Greater than descendant"
);
assert_int(
-1,
this.root.get_child("test1")
.compare_to(this.root.get_child("test1").get_child("test")),
"Less than descendant"
);
}
public void path_compare_normalised() throws GLib.Error {
assert_int(0, this.root.compare_normalized_ci(this.root), "Root equality");
assert_int(0,
this.root.get_child("test")
.compare_normalized_ci(this.root.get_child("test")),
"Equal child comparison"
);
assert_int(
-1,
this.root.get_child("test1")
.compare_normalized_ci(this.root.get_child("test2")),
"Greater than child comparison"
);
assert_int(
1,
this.root.get_child("test2")
.compare_normalized_ci(this.root.get_child("test1")),
"Less than child comparison"
);
assert_int(
-1,
this.root.get_child("test1").get_child("test")
.compare_normalized_ci(this.root.get_child("test2").get_child("test")),
"Greater than disjoint parents"
);
assert_int(
1,
this.root.get_child("test2").get_child("test")
.compare_normalized_ci(this.root.get_child("test1").get_child("test")),
"Less than disjoint parents"
);
assert_int(
1,
this.root.get_child("test1").get_child("test")
.compare_normalized_ci(this.root.get_child("test1")),
"Greater than descendant"
);
assert_int(
-1,
this.root.get_child("test1")
.compare_normalized_ci(this.root.get_child("test1").get_child("test")),
"Less than descendant"
);
}
}

View file

@ -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

View file

@ -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);

View file

@ -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));

View file

@ -0,0 +1,309 @@
/*
* 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.
*/
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() {
base("Geary.ImapDB.AccountTest");
add_test("create_base_folder", create_base_folder);
add_test("create_child_folder", create_child_folder);
add_test("list_folders", list_folders);
add_test("delete_folder", delete_folder);
add_test("delete_folder_with_child", delete_folder_with_child);
add_test("delete_nonexistent_folder", delete_nonexistent_folder);
add_test("fetch_base_folder", fetch_base_folder);
add_test("fetch_child_folder", fetch_child_folder);
add_test("fetch_nonexistent_folder", fetch_nonexistent_folder);
}
public override void set_up() throws GLib.Error {
this.tmp_dir = GLib.File.new_for_path(
GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
);
this.config = new Geary.AccountInformation(
"test",
ServiceProvider.OTHER,
new MockCredentialsMediator(),
new Geary.RFC822.MailboxAddress(null, "test@example.com")
);
this.account = new Account(config);
this.account.open_async.begin(
this.tmp_dir,
GLib.File.new_for_path(_SOURCE_ROOT_DIR).get_child("sql"),
null,
(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); }
);
this.account.close_async.end(async_result());
delete_file(this.tmp_dir);
this.tmp_dir = null;
}
public void create_base_folder() throws GLib.Error {
Imap.Folder folder = new Imap.Folder(
this.root.get_child("test"),
new Imap.FolderProperties.selectable(
new Imap.MailboxAttributes(
Gee.Collection.empty<Geary.Imap.MailboxAttribute>()
),
new Imap.StatusData(
new Imap.MailboxSpecifier("test"),
10, // total
9, // recent
new Imap.UID(8),
new Imap.UIDValidity(7),
6 //unseen
),
new Imap.Capabilities(1)
)
);
this.account.clone_folder_async.begin(
folder,
null,
(obj, ret) => { async_complete(ret); }
);
this.account.clone_folder_async.end(async_result());
Geary.Db.Result result = this.account.db.query(
"SELECT * FROM FolderTable;"
);
assert_false(result.finished, "Folder not created");
assert_string("test", result.string_for("name"), "Folder name");
assert_true(result.is_null_for("parent_id"), "Folder parent");
assert_false(result.next(), "Multiple rows inserted");
}
public void create_child_folder() throws GLib.Error {
this.account.db.exec(
"INSERT INTO FolderTable (id, name) VALUES (1, 'test');"
);
Imap.Folder folder = new Imap.Folder(
this.root.get_child("test").get_child("child"),
new Imap.FolderProperties.selectable(
new Imap.MailboxAttributes(
Gee.Collection.empty<Geary.Imap.MailboxAttribute>()
),
new Imap.StatusData(
new Imap.MailboxSpecifier("test>child"),
10, // total
9, // recent
new Imap.UID(8),
new Imap.UIDValidity(7),
6 //unseen
),
new Imap.Capabilities(1)
)
);
this.account.clone_folder_async.begin(
folder,
null,
(obj, ret) => { async_complete(ret); }
);
this.account.clone_folder_async.end(async_result());
Geary.Db.Result result = this.account.db.query(
"SELECT * FROM FolderTable WHERE id != 1;"
);
assert_false(result.finished, "Folder not created");
assert_string("child", result.string_for("name"), "Folder name");
assert_int(1, result.int_for("parent_id"), "Folder parent");
assert_false(result.next(), "Multiple rows inserted");
}
public void list_folders() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1),
(3, 'test3', 2);
""");
this.account.list_folders_async.begin(
this.account.imap_folder_root,
null,
(obj, ret) => { async_complete(ret); }
);
Gee.Collection<Geary.ImapDB.Folder> result =
this.account.list_folders_async.end(async_result());
Folder test1 = traverse(result).first();
assert_int(1, result.size, "Base folder not listed");
assert_string("test1", test1.get_path().name, "Base folder name");
this.account.list_folders_async.begin(
test1.get_path(),
null,
(obj, ret) => { async_complete(ret); }
);
result = this.account.list_folders_async.end(async_result());
Folder test2 = traverse(result).first();
assert_int(1, result.size, "Child folder not listed");
assert_string("test2", test2.get_path().name, "Child folder name");
this.account.list_folders_async.begin(
test2.get_path(),
null,
(obj, ret) => { async_complete(ret); }
);
result = this.account.list_folders_async.end(async_result());
Folder test3 = traverse(result).first();
assert_int(1, result.size, "Grandchild folder not listed");
assert_string("test3", test3.get_path().name, "Grandchild folder name");
}
public void delete_folder() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.delete_folder_async.begin(
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(
this.root.get_child("test1"),
null,
(obj, ret) => { async_complete(ret); }
);
this.account.delete_folder_async.end(async_result());
}
public void delete_folder_with_child() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.delete_folder_async.begin(
this.root.get_child("test1"),
null,
(obj, ret) => { async_complete(ret); }
);
try {
this.account.delete_folder_async.end(async_result());
assert_not_reached();
} catch (GLib.Error err) {
assert_error(new ImapError.NOT_SUPPORTED(""), err);
}
}
public void delete_nonexistent_folder() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.delete_folder_async.begin(
this.root.get_child("test3"),
null,
(obj, ret) => { async_complete(ret); }
);
try {
this.account.delete_folder_async.end(async_result());
assert_not_reached();
} catch (GLib.Error err) {
assert_error(new EngineError.NOT_FOUND(""), err);
}
}
public void fetch_base_folder() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.fetch_folder_async.begin(
this.root.get_child("test1"),
null,
(obj, ret) => { async_complete(ret); }
);
Folder? result = this.account.fetch_folder_async.end(async_result());
assert_non_null(result);
assert_string("test1", result.get_path().name);
}
public void fetch_child_folder() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.fetch_folder_async.begin(
this.root.get_child("test1").get_child("test2"),
null,
(obj, ret) => { async_complete(ret); }
);
Folder? result = this.account.fetch_folder_async.end(async_result());
assert_non_null(result);
assert_string("test2", result.get_path().name);
}
public void fetch_nonexistent_folder() throws GLib.Error {
this.account.db.exec("""
INSERT INTO FolderTable (id, name, parent_id)
VALUES
(1, 'test1', null),
(2, 'test2', 1);
""");
this.account.fetch_folder_async.begin(
this.root.get_child("test3"),
null,
(obj, ret) => { async_complete(ret); }
);
try {
this.account.fetch_folder_async.end(async_result());
assert_not_reached();
} catch (GLib.Error err) {
assert_error(new EngineError.NOT_FOUND(""), err);
}
}
}

View file

@ -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
root.get_child(""), inbox, "$"
);
assert_not_reached();
} catch (GLib.Error err) {
// all good
}
try {
new MailboxSpecifier.from_folder_path(
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$Foo$Bar",
new MailboxSpecifier.from_folder_path(
non_empty_root.get_child("Foo").get_child("Bar"),
non_empty_inbox,
"$"
).name
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_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")
)
);
}

View file

@ -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',
@ -36,6 +36,7 @@ geary_test_engine_sources = [
'engine/imap/parameter/imap-list-parameter-test.vala',
'engine/imap/response/imap-namespace-response-test.vala',
'engine/imap/transport/imap-deserializer-test.vala',
'engine/imap-db/imap-db-account-test.vala',
'engine/imap-db/imap-db-attachment-test.vala',
'engine/imap-db/imap-db-database-test.vala',
'engine/imap-engine/account-processor-test.vala',

View file

@ -152,6 +152,27 @@ private inline void print_assert(string message, string? context) {
GLib.stderr.putc('\n');
}
public void delete_file(File parent) throws GLib.Error {
FileInfo info = parent.query_info(
"standard::*",
FileQueryInfoFlags.NOFOLLOW_SYMLINKS
);
if (info.get_file_type () == FileType.DIRECTORY) {
FileEnumerator enumerator = parent.enumerate_children(
"standard::*",
FileQueryInfoFlags.NOFOLLOW_SYMLINKS
);
info = null;
while (((info = enumerator.next_file()) != null)) {
delete_file(parent.get_child(info.get_name()));
}
}
parent.delete();
}
public abstract class TestCase : Object {
@ -304,5 +325,7 @@ public abstract class TestCase : Object {
assert_no_error(err);
}
}
}
}

View file

@ -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());
@ -45,6 +46,7 @@ int main(string[] args) {
engine.add_suite(new Geary.Imap.ListParameterTest().get_suite());
engine.add_suite(new Geary.Imap.MailboxSpecifierTest().get_suite());
engine.add_suite(new Geary.Imap.NamespaceResponseTest().get_suite());
engine.add_suite(new Geary.ImapDB.AccountTest().get_suite());
engine.add_suite(new Geary.ImapDB.AttachmentTest().get_suite());
engine.add_suite(new Geary.ImapDB.AttachmentIoTest().get_suite());
engine.add_suite(new Geary.ImapDB.DatabaseTest().get_suite());