From 0273b78005be7dfd81b25562ce8cd9fbe3e52531 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Fri, 1 Jul 2011 15:40:20 -0700 Subject: [PATCH] Folder heirarchies: #3788 Now supporting folder heirarchies. The client will now descend looking for subfolders. This task now opens up multiple outstanding requests to the Engine as well as exercises the database schema. Closing this ticket opens the door to finishing #3692. --- src/client/geary-application.vala | 2 +- src/client/ui/folder-list-store.vala | 43 +++- src/client/ui/main-window.vala | 30 ++- src/client/wscript_build | 4 +- .../intl.vala => common/common-intl.vala} | 0 src/console/main.vala | 34 ++-- src/engine/api/geary-abstract-account.vala | 31 +++ src/engine/api/geary-abstract-folder.vala | 4 +- src/engine/api/geary-account.vala | 32 ++- src/engine/api/geary-credentials.vala | 5 +- src/engine/api/geary-engine-account.vala | 12 ++ src/engine/api/geary-engine-folder.vala | 25 +-- src/engine/api/geary-engine.vala | 8 +- src/engine/api/geary-folder-path.vala | 187 ++++++++++++++++++ src/engine/api/geary-folder.vala | 2 +- ...e.vala => geary-generic-imap-account.vala} | 67 +++---- src/engine/api/geary-local-interfaces.vala | 6 + src/engine/api/geary-remote-interfaces.vala | 2 + src/engine/common/common-interfaces.vala | 16 +- .../common/common-nonblocking-mailbox.vala | 31 +++ .../common/common-nonblocking-mutex.vala | 39 ++++ .../common/common-nonblocking-semaphore.vala | 69 ++++++- src/engine/imap/api/imap-account.vala | 90 ++++++--- src/engine/imap/api/imap-folder.vala | 17 +- .../imap/decoders/imap-command-results.vala | 4 + .../imap/decoders/imap-list-results.vala | 28 ++- src/engine/imap/message/imap-flag.vala | 6 +- .../imap/message/imap-message-data.vala | 2 +- .../transport/imap-client-connection.vala | 65 +++--- .../imap-client-session-manager.vala | 64 ++++-- .../imap/transport/imap-client-session.vala | 13 +- src/engine/sqlite/api/sqlite-account.vala | 79 +++++--- src/engine/sqlite/api/sqlite-folder.vala | 27 +-- .../sqlite/email/sqlite-folder-table.vala | 41 ++++ src/engine/state/state-machine.vala | 2 +- src/wscript | 8 +- 36 files changed, 846 insertions(+), 249 deletions(-) rename src/{client/util/intl.vala => common/common-intl.vala} (100%) create mode 100644 src/engine/api/geary-abstract-account.vala create mode 100644 src/engine/api/geary-engine-account.vala create mode 100644 src/engine/api/geary-folder-path.vala rename src/engine/api/{geary-imap-engine.vala => geary-generic-imap-account.vala} (63%) create mode 100644 src/engine/common/common-nonblocking-mailbox.vala create mode 100644 src/engine/common/common-nonblocking-mutex.vala diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index 17998dbf..e8e5fb34 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -46,7 +46,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., private static GearyApplication? _instance = null; private MainWindow main_window = new MainWindow(); - private Geary.Account? account = null; + private Geary.EngineAccount? account = null; private GearyApplication() { base (NAME, "geary", "org.yorba.geary"); diff --git a/src/client/ui/folder-list-store.vala b/src/client/ui/folder-list-store.vala index c0c7c20c..960119d4 100644 --- a/src/client/ui/folder-list-store.vala +++ b/src/client/ui/folder-list-store.vala @@ -52,20 +52,19 @@ public class FolderListStore : Gtk.TreeStore { } public void add_folder(Geary.Folder folder) { + Gtk.TreeIter? parent_iter = !folder.get_path().is_root() + ? find_path(folder.get_path().get_parent()) + : null; + Gtk.TreeIter iter; - append(out iter, null); + append(out iter, parent_iter); set(iter, - Column.NAME, folder.get_name(), + Column.NAME, folder.get_path().basename, Column.FOLDER_OBJECT, folder ); } - public void add_folders(Gee.Collection folders) { - foreach (Geary.Folder folder in folders) - add_folder(folder); - } - public Geary.Folder? get_folder_at(Gtk.TreePath path) { Gtk.TreeIter iter; if (!get_iter(out iter, path)) @@ -77,6 +76,36 @@ public class FolderListStore : Gtk.TreeStore { return folder; } + // TODO: This could be replaced with a binary search + private Gtk.TreeIter? find_path(Geary.FolderPath path, Gtk.TreeIter? parent = null) { + Gtk.TreeIter iter; + // no parent, start at the root, otherwise start at the parent's children + if (parent == null) { + if (!get_iter_first(out iter)) + return null; + } else { + if (!iter_children(out iter, parent)) + return null; + } + + do { + Geary.Folder folder; + get(iter, Column.FOLDER_OBJECT, out folder); + + if (folder.get_path().equals(path)) + return iter; + + // recurse + if (iter_has_child(iter)) { + Gtk.TreeIter? found = find_path(path, iter); + if (found != null) + return found; + } + } while (iter_next(ref iter)); + + return null; + } + private int sort_by_name(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) { string aname; model.get(aiter, Column.NAME, out aname); diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index e12ac478..62856066 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -26,7 +26,7 @@ public class MainWindow : Gtk.Window { private MessageViewer message_viewer = new MessageViewer(); private MessageBuffer message_buffer = new MessageBuffer(); private Gtk.UIManager ui = new Gtk.UIManager(); - private Geary.Account? account = null; + private Geary.EngineAccount? account = null; private Geary.Folder? current_folder = null; public MainWindow() { @@ -61,7 +61,7 @@ public class MainWindow : Gtk.Window { account.folders_added_removed.disconnect(on_folders_added_removed); } - public void start(Geary.Account account) { + public void start(Geary.EngineAccount account) { this.account = account; account.folders_added_removed.connect(on_folders_added_removed); @@ -180,7 +180,7 @@ public class MainWindow : Gtk.Window { return; } - debug("Folder %s selected", folder.get_name()); + debug("Folder %s selected", folder.to_string()); do_select_folder.begin(folder, on_select_folder_completed); } @@ -252,9 +252,11 @@ public class MainWindow : Gtk.Window { private void on_folders_added_removed(Gee.Collection? added, Gee.Collection? removed) { - if (added != null) { - folder_list_store.add_folders(added); - debug("%d folders added", added.size); + if (added != null && added.size > 0) { + foreach (Geary.Folder folder in added) + folder_list_store.add_folder(folder); + + search_folders_for_children.begin(added); } } @@ -264,5 +266,21 @@ public class MainWindow : Gtk.Window { message_list_store.append_envelope(email); } } + + private async void search_folders_for_children(Gee.Collection folders) { + Gee.ArrayList accumulator = new Gee.ArrayList(); + foreach (Geary.Folder folder in folders) { + try { + Gee.Collection children = yield account.list_folders_async( + folder.get_path(), null); + accumulator.add_all(children); + } catch (Error err) { + debug("Unable to list children of %s: %s", folder.to_string(), err.message); + } + } + + if (accumulator.size > 0) + on_folders_added_removed(accumulator, null); + } } diff --git a/src/client/wscript_build b/src/client/wscript_build index 1fdbb93c..03782616 100644 --- a/src/client/wscript_build +++ b/src/client/wscript_build @@ -13,9 +13,7 @@ client_src = [ 'ui/message-buffer.vala', 'ui/message-list-store.vala', 'ui/message-list-view.vala', -'ui/message-viewer.vala', - -'util/intl.vala' +'ui/message-viewer.vala' ] client_uselib = 'GLIB GEE GTK' diff --git a/src/client/util/intl.vala b/src/common/common-intl.vala similarity index 100% rename from src/client/util/intl.vala rename to src/common/common-intl.vala diff --git a/src/console/main.vala b/src/console/main.vala index d021ad8e..27061aa7 100644 --- a/src/console/main.vala +++ b/src/console/main.vala @@ -217,7 +217,7 @@ class ImapConsole : Gtk.Window { private void capabilities(string cmd, string[] args) throws Error { check_connected(cmd, args, 0, null); - cx.send_async.begin(new Geary.Imap.CapabilityCommand(cx.generate_tag()), Priority.DEFAULT, null, + cx.send_async.begin(new Geary.Imap.CapabilityCommand(cx.generate_tag()), null, on_capabilities); } @@ -233,8 +233,7 @@ class ImapConsole : Gtk.Window { private void noop(string cmd, string[] args) throws Error { check_connected(cmd, args, 0, null); - cx.send_async.begin(new Geary.Imap.NoopCommand(cx.generate_tag()), Priority.DEFAULT, null, - on_noop); + cx.send_async.begin(new Geary.Imap.NoopCommand(cx.generate_tag()), null, on_noop); } private void on_noop(Object? source, AsyncResult result) { @@ -301,12 +300,13 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 2, "user pass"); status("Logging in..."); - cx.post(new Geary.Imap.LoginCommand(cx.generate_tag(), args[0], args[1]), on_logged_in); + cx.send_async.begin(new Geary.Imap.LoginCommand(cx.generate_tag(), args[0], args[1]), + null, on_logged_in); } private void on_logged_in(Object? source, AsyncResult result) { try { - cx.finish_post(result); + cx.send_async.end(result); status("Login completed"); } catch (Error err) { exception(err); @@ -317,12 +317,12 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 0, null); status("Logging out..."); - cx.post(new Geary.Imap.LogoutCommand(cx.generate_tag()), on_logout); + cx.send_async.begin(new Geary.Imap.LogoutCommand(cx.generate_tag()), null, on_logout); } private void on_logout(Object? source, AsyncResult result) { try { - cx.finish_post(result); + cx.send_async.end(result); status("Logged out"); } catch (Error err) { exception(err); @@ -333,12 +333,13 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 2, " "); status("Listing..."); - cx.post(new Geary.Imap.ListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), on_list); + cx.send_async.begin(new Geary.Imap.ListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), + null, on_list); } private void on_list(Object? source, AsyncResult result) { try { - cx.finish_post(result); + cx.send_async.end(result); status("Listed"); } catch (Error err) { exception(err); @@ -349,20 +350,21 @@ class ImapConsole : Gtk.Window { check_connected(cmd, args, 2, " "); status("Xlisting..."); - cx.post(new Geary.Imap.XListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), - on_list); + cx.send_async.begin(new Geary.Imap.XListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), + null, on_list); } private void examine(string cmd, string[] args) throws Error { check_connected(cmd, args, 1, ""); status("Opening %s read-only".printf(args[0])); - cx.post(new Geary.Imap.ExamineCommand(cx.generate_tag(), args[0]), on_examine); + cx.send_async.begin(new Geary.Imap.ExamineCommand(cx.generate_tag(), args[0]), null, + on_examine); } private void on_examine(Object? source, AsyncResult result) { try { - cx.finish_post(result); + cx.send_async.end(result); status("Opened read-only"); } catch (Error err) { exception(err); @@ -378,13 +380,13 @@ class ImapConsole : Gtk.Window { for (int ctr = 1; ctr < args.length; ctr++) data_items += Geary.Imap.FetchDataType.decode(args[ctr]); - cx.post(new Geary.Imap.FetchCommand(cx.generate_tag(), - new Geary.Imap.MessageSet.custom(args[0]), data_items), on_fetch); + cx.send_async.begin(new Geary.Imap.FetchCommand(cx.generate_tag(), + new Geary.Imap.MessageSet.custom(args[0]), data_items), null, on_fetch); } private void on_fetch(Object? source, AsyncResult result) { try { - cx.finish_post(result); + cx.send_async.end(result); status("Fetched"); } catch (Error err) { exception(err); diff --git a/src/engine/api/geary-abstract-account.vala b/src/engine/api/geary-abstract-account.vala new file mode 100644 index 00000000..120bcdce --- /dev/null +++ b/src/engine/api/geary-abstract-account.vala @@ -0,0 +1,31 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public abstract class Geary.AbstractAccount : Object, Geary.Account { + private string name; + + public AbstractAccount(string name) { + this.name = name; + } + + protected virtual void notify_folders_added_removed(Gee.Collection? added, + Gee.Collection? removed) { + folders_added_removed(added, removed); + } + + public abstract Geary.Email.Field get_required_fields_for_writing(); + + public abstract async Gee.Collection list_folders_async(Geary.FolderPath? parent, + Cancellable? cancellable = null) throws Error; + + public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path, + Cancellable? cancellable = null) throws Error; + + public virtual string to_string() { + return name; + } +} + diff --git a/src/engine/api/geary-abstract-folder.vala b/src/engine/api/geary-abstract-folder.vala index dbb0c1c9..77cbd11c 100644 --- a/src/engine/api/geary-abstract-folder.vala +++ b/src/engine/api/geary-abstract-folder.vala @@ -22,7 +22,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { updated(); } - public abstract string get_name(); + public abstract Geary.FolderPath get_path(); public abstract Geary.FolderProperties? get_properties(); @@ -85,7 +85,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { Cancellable? cancellable = null) throws Error; public virtual string to_string() { - return get_name(); + return get_path().to_string(); } } diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 12b7c997..89f30e5f 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -8,10 +8,8 @@ public interface Geary.Account : Object { public signal void folders_added_removed(Gee.Collection? added, Gee.Collection? removed); - protected virtual void notify_folders_added_removed(Gee.Collection? added, - Gee.Collection? removed) { - folders_added_removed(added, removed); - } + protected abstract void notify_folders_added_removed(Gee.Collection? added, + Gee.Collection? removed); /** * This method returns which Geary.Email.Field fields must be available in a Geary.Email to @@ -25,22 +23,22 @@ public interface Geary.Account : Object { */ public abstract Geary.Email.Field get_required_fields_for_writing(); - public abstract async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + /** + * Lists all the folders found under the parent path unless it's null, in which case it lists + * all the root folders. If the parent path cannot be found, EngineError.NOT_FOUND is thrown. + * If no folders exist in the root, EngineError.NOT_FOUND may be thrown as well. However, + * the caller should be prepared to deal with an empty list being returned instead. + */ + public abstract async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error; - public abstract async void create_many_folders_async(Geary.Folder? parent, - Gee.Collection folders, Cancellable? cancellable = null) throws Error; - - public abstract async Gee.Collection list_folders_async(Geary.Folder? parent, + /** + * Fetches a Folder object corresponding to the supplied path. If the backing medium does + * not have a record of a folder at the path, EngineError.NOT_FOUND will be thrown. + */ + public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error; - public abstract async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, - Cancellable? cancellable = null) throws Error; - - public abstract async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) - throws Error; - - public abstract async void remove_many_folders_async(Gee.Set folders, - Cancellable? cancellable = null) throws Error; + public abstract string to_string(); } diff --git a/src/engine/api/geary-credentials.vala b/src/engine/api/geary-credentials.vala index d865b493..6b7264fe 100644 --- a/src/engine/api/geary-credentials.vala +++ b/src/engine/api/geary-credentials.vala @@ -14,5 +14,8 @@ public class Geary.Credentials { this.user = user; this.pass = pass; } + + public string to_string() { + return "%s/%s".printf(user, server); + } } - diff --git a/src/engine/api/geary-engine-account.vala b/src/engine/api/geary-engine-account.vala new file mode 100644 index 00000000..b60916d8 --- /dev/null +++ b/src/engine/api/geary-engine-account.vala @@ -0,0 +1,12 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public abstract class Geary.EngineAccount : Geary.AbstractAccount { + public EngineAccount(string name) { + base (name); + } +} + diff --git a/src/engine/api/geary-engine-folder.vala b/src/engine/api/geary-engine-folder.vala index 8d09214e..e34f36c7 100644 --- a/src/engine/api/geary-engine-folder.vala +++ b/src/engine/api/geary-engine-folder.vala @@ -12,7 +12,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { private RemoteFolder? remote_folder = null; private LocalFolder local_folder; private bool opened = false; - private Geary.Common.NonblockingSemaphore remote_semaphore = new Geary.Common.NonblockingSemaphore(); + private Geary.Common.NonblockingSemaphore remote_semaphore = + new Geary.Common.NonblockingSemaphore(true); public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) { this.remote = remote; @@ -24,13 +25,13 @@ private class Geary.EngineFolder : Geary.AbstractFolder { ~EngineFolder() { if (opened) - warning("Folder %s destroyed without closing", get_name()); + warning("Folder %s destroyed without closing", to_string()); local_folder.updated.disconnect(on_local_updated); } - public override string get_name() { - return local_folder.get_name(); + public override Geary.FolderPath get_path() { + return local_folder.get_path(); } public override Geary.FolderProperties? get_properties() { @@ -43,7 +44,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { if (opened) - throw new EngineError.ALREADY_OPEN("Folder %s already open", get_name()); + throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string()); yield local_folder.open_async(readonly, cancellable); @@ -55,8 +56,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // wait_for_remote_to_open(), which uses a NonblockingSemaphore to indicate that the remote // is open (or has failed to open). This allows for early calls to list and fetch emails // can work out of the local cache until the remote is ready. - RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(), - cancellable); + RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(), + cancellable); open_remote_async.begin(folder, readonly, cancellable, on_open_remote_completed); opened = true; @@ -100,7 +101,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // this method to complete (much like open_async()) if (remote_folder != null) { yield remote_semaphore.wait_async(); - remote_semaphore = new Geary.Common.NonblockingSemaphore(); + remote_semaphore = new Geary.Common.NonblockingSemaphore(true); remote_folder.updated.disconnect(on_remote_updated); RemoteFolder? folder = remote_folder; @@ -145,7 +146,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { assert(count >= 0); if (!opened) - throw new EngineError.OPEN_REQUIRED("%s is not open", get_name()); + throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); if (count == 0) { // signal finished @@ -246,7 +247,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error { if (!opened) - throw new EngineError.OPEN_REQUIRED("%s is not open", get_name()); + throw new EngineError.OPEN_REQUIRED("%s is not open", to_string()); if (by_position.length == 0) { // signal finished @@ -345,7 +346,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { // possible to call remote multiple times, wait for it to open once and go yield wait_for_remote_to_open(); - debug("Background fetching %d emails for %s", needed_by_position.length, get_name()); + debug("Background fetching %d emails for %s", needed_by_position.length, to_string()); Gee.List full = new Gee.ArrayList(); @@ -419,7 +420,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder { public override async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (!opened) - throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); + throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string()); try { return yield local_folder.fetch_email_async(num, fields, cancellable); diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index 963917c2..a034c860 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -5,11 +5,11 @@ */ public class Geary.Engine { - public static Geary.Account open(Geary.Credentials cred) throws Error { - // Only ImapEngine today - return new ImapEngine( + public static Geary.EngineAccount open(Geary.Credentials cred) throws Error { + // Only Gmail today + return new GenericImapAccount( + "Gmail account %s".printf(cred.to_string()), new Geary.Imap.Account(cred, Imap.ClientConnection.DEFAULT_PORT_TLS), new Geary.Sqlite.Account(cred)); } } - diff --git a/src/engine/api/geary-folder-path.vala b/src/engine/api/geary-folder-path.vala new file mode 100644 index 00000000..c6e0d8a2 --- /dev/null +++ b/src/engine/api/geary-folder-path.vala @@ -0,0 +1,187 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Geary.FolderPath : Object, Hashable, Equalable { + public string basename { get; private set; } + + private Gee.List? path = null; + private string? fullpath = null; + private string? fullpath_separator = null; + private uint hash = uint.MAX; + + protected FolderPath(string basename) { + assert(this is FolderRoot); + + this.basename = basename; + } + + private FolderPath.child(Gee.List path, string basename) { + assert(path[0] is FolderRoot); + + this.path = path; + this.basename = basename; + } + + public bool is_root() { + return (path == null || path.size == 0); + } + + public Geary.FolderRoot get_root() { + return (FolderRoot) ((path != null && path.size > 0) ? path[0] : this); + } + + public Geary.FolderPath? get_parent() { + return (path != null && path.size > 0) ? path.last() : null; + } + + public int get_path_length() { + // include self, which is not stored in the path list + return (path != null) ? path.size + 1 : 1; + } + + /** + * Returns null if index is out of bounds. There is always at least one element in the path, + * namely this one. + */ + 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; + } + + public Gee.List as_list() { + Gee.List list = new Gee.ArrayList(); + + if (path != null) { + foreach (Geary.FolderPath folder in path) + list.add(folder.basename); + } + + list.add(basename); + + return list; + } + + public Geary.FolderPath get_child(string basename) { + // Build the child's path, which is this node's path plus this node + Gee.List child_path = new Gee.ArrayList(); + if (path != null) + child_path.add_all(path); + child_path.add(this); + + return new FolderPath.child(child_path, basename); + } + + public string get_fullpath(string? use_separator = null) { + string? separator = use_separator ?? get_root().default_separator; + + // no separator, no heirarchy + if (separator == null) + return basename; + + if (fullpath != null && fullpath_separator == separator) + return fullpath; + + StringBuilder builder = new StringBuilder(); + + if (path != null) { + foreach (Geary.FolderPath folder in path) { + builder.append(folder.basename); + builder.append(separator); + } + } + + builder.append(basename); + + fullpath = builder.str; + fullpath_separator = separator; + + return fullpath; + } + + private uint get_basename_hash(bool cs) { + return cs ? str_hash(basename) : str_hash(basename.down()); + } + + public uint to_hash() { + if (hash != uint.MAX) + return hash; + + bool cs = get_root().case_sensitive; + + // always one element in path + uint calc = get_folder_at(0).get_basename_hash(cs); + + int path_length = get_path_length(); + for (int ctr = 1; ctr < path_length; ctr++) + calc ^= get_folder_at(ctr).get_basename_hash(cs); + + hash = calc; + + return hash; + } + + private bool is_basename_equal(string cmp, bool cs) { + return cs ? (basename == cmp) : (basename.down() == cmp.down()); + } + + public bool equals(Equalable o) { + FolderPath? other = o as FolderPath; + if (o == null) + return false; + + if (o == this) + return true; + + int path_length = get_path_length(); + if (other.get_path_length() != path_length) + return false; + + + bool cs = get_root().case_sensitive; + if (other.get_root().case_sensitive != cs) { + message("Comparing %s and %s with different case sensitivities", to_string(), + other.to_string()); + } + + for (int ctr = 0; ctr < path_length; ctr++) { + if (!get_folder_at(ctr).is_basename_equal(other.get_folder_at(ctr).basename, cs)) + return false; + } + + return true; + } + + /** + * Returns the fullpath using the default separator. Using only for debugging and logging. + */ + public string to_string() { + return get_fullpath(); + } +} + +public class Geary.FolderRoot : Geary.FolderPath { + public string? default_separator { get; private set; } + public bool case_sensitive { get; private set; } + + public FolderRoot(string basename, string? default_separator, bool case_sensitive) { + base (basename); + + this.default_separator = default_separator; + this.case_sensitive; + } +} + diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 67dd601c..e21cd472 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -82,7 +82,7 @@ public interface Geary.Folder : Object { updated(); } - public abstract string get_name(); + public abstract Geary.FolderPath get_path(); public abstract Geary.FolderProperties? get_properties(); diff --git a/src/engine/api/geary-imap-engine.vala b/src/engine/api/geary-generic-imap-account.vala similarity index 63% rename from src/engine/api/geary-imap-engine.vala rename to src/engine/api/geary-generic-imap-account.vala index e94c233a..796b7322 100644 --- a/src/engine/api/geary-imap-engine.vala +++ b/src/engine/api/geary-generic-imap-account.vala @@ -4,48 +4,50 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private class Geary.ImapEngine : Object, Geary.Account { +private class Geary.GenericImapAccount : Geary.EngineAccount { + public const string INBOX = "Inbox"; + private RemoteAccount remote; private LocalAccount local; - public ImapEngine(RemoteAccount remote, LocalAccount local) { + public GenericImapAccount(string name, RemoteAccount remote, LocalAccount local) { + base (name); + this.remote = remote; this.local = local; } - public Geary.Email.Field get_required_fields_for_writing() { + public override Geary.Email.Field get_required_fields_for_writing() { // Return the more restrictive of the two, which is the NetworkAccount's. // TODO: This could be determined at runtime rather than fixed in stone here. return Geary.Email.Field.HEADER | Geary.Email.Field.BODY; } - public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { - } - - public async void create_many_folders_async(Geary.Folder? parent, - Gee.Collection folders, Cancellable? cancellable = null) throws Error { - } - - public async Gee.Collection list_folders_async(Geary.Folder? parent, - Cancellable? cancellable = null) throws Error { - Gee.Collection local_list = yield local.list_folders_async(parent, cancellable); + Gee.Collection? local_list = null; + try { + local_list = yield local.list_folders_async(parent, cancellable); + } catch (EngineError err) { + // don't pass on NOT_FOUND's, that means we need to go to the server for more info + if (!(err is EngineError.NOT_FOUND)) + throw err; + } Gee.Collection engine_list = new Gee.ArrayList(); - foreach (Geary.Folder local_folder in local_list) - engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder)); + if (local_list != null && local_list.size > 0) { + foreach (Geary.Folder local_folder in local_list) + engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder)); + } background_update_folders.begin(parent, engine_list); - debug("Reporting %d folders", engine_list.size); - return engine_list; } - public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, + public override async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { - LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(parent, folder_name, - cancellable); + LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable); Geary.Folder engine_folder = new EngineFolder(remote, local, local_folder); return engine_folder; @@ -54,7 +56,7 @@ private class Geary.ImapEngine : Object, Geary.Account { private Gee.Set get_folder_names(Gee.Collection folders) { Gee.Set names = new Gee.HashSet(); foreach (Geary.Folder folder in folders) - names.add(folder.get_name()); + names.add(folder.get_path().basename); return names; } @@ -63,14 +65,14 @@ private class Geary.ImapEngine : Object, Geary.Account { Gee.Set names) { Gee.List excluded = new Gee.ArrayList(); foreach (Geary.Folder folder in folders) { - if (!names.contains(folder.get_name())) + if (!names.contains(folder.get_path().basename)) excluded.add(folder); } return excluded; } - private async void background_update_folders(Geary.Folder? parent, + private async void background_update_folders(Geary.FolderPath? parent, Gee.Collection engine_folders) { Gee.Collection remote_folders; try { @@ -82,13 +84,9 @@ private class Geary.ImapEngine : Object, Geary.Account { Gee.Set local_names = get_folder_names(engine_folders); Gee.Set remote_names = get_folder_names(remote_folders); - debug("%d local names, %d remote names", local_names.size, remote_names.size); - Gee.List? to_add = get_excluded_folders(remote_folders, local_names); Gee.List? to_remove = get_excluded_folders(engine_folders, remote_names); - debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); - if (to_add.size == 0) to_add = null; @@ -97,7 +95,7 @@ private class Geary.ImapEngine : Object, Geary.Account { try { if (to_add != null) - yield local.create_many_folders_async(parent, to_add); + yield local.clone_many_folders_async(to_add); } catch (Error err) { error("Unable to add/remove folders: %s", err.message); } @@ -107,8 +105,9 @@ private class Geary.ImapEngine : Object, Geary.Account { engine_added = new Gee.ArrayList(); foreach (Geary.Folder remote_folder in to_add) { try { - engine_added.add(new EngineFolder(remote, local, - (LocalFolder) yield local.fetch_folder_async(parent, remote_folder.get_name()))); + LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async( + remote_folder.get_path()); + engine_added.add(new EngineFolder(remote, local, local_folder)); } catch (Error convert_err) { error("Unable to fetch local folder: %s", convert_err.message); } @@ -118,13 +117,5 @@ private class Geary.ImapEngine : Object, Geary.Account { if (engine_added != null) notify_folders_added_removed(engine_added, null); } - - public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) - throws Error { - } - - public async void remove_many_folders_async(Gee.Set folders, - Cancellable? cancellable = null) throws Error { - } } diff --git a/src/engine/api/geary-local-interfaces.vala b/src/engine/api/geary-local-interfaces.vala index 57ae5309..5a55d95c 100644 --- a/src/engine/api/geary-local-interfaces.vala +++ b/src/engine/api/geary-local-interfaces.vala @@ -5,6 +5,12 @@ */ public interface Geary.LocalAccount : Object, Geary.Account { + public abstract async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null) + throws Error; + + public abstract async void clone_many_folders_async(Gee.Collection folders, + Cancellable? cancellable = null) throws Error; + /** * Returns true if the email (identified by its Message-ID) already exists in the account's * local store, no matter the folder. diff --git a/src/engine/api/geary-remote-interfaces.vala b/src/engine/api/geary-remote-interfaces.vala index 5ea478ba..cd09ed5f 100644 --- a/src/engine/api/geary-remote-interfaces.vala +++ b/src/engine/api/geary-remote-interfaces.vala @@ -5,6 +5,8 @@ */ public interface Geary.RemoteAccount : Object, Geary.Account { + public abstract async string? get_folder_delimiter_async(string toplevel, + Cancellable? cancellable = null) throws Error; } public interface Geary.RemoteFolder : Object, Geary.Folder { diff --git a/src/engine/common/common-interfaces.vala b/src/engine/common/common-interfaces.vala index f07f03ef..17716677 100644 --- a/src/engine/common/common-interfaces.vala +++ b/src/engine/common/common-interfaces.vala @@ -5,18 +5,26 @@ */ public interface Geary.Comparable { - public abstract bool equals(Comparable other); + public abstract int compare(Comparable other); + + public static int compare_func(void *a, void *b) { + return ((Comparable *) a)->compare((Comparable *) b); + } +} + +public interface Geary.Equalable { + public abstract bool equals(Equalable other); public static bool equal_func(void *a, void *b) { - return ((Comparable *) a)->equals((Comparable *) b); + return ((Equalable *) a)->equals((Equalable *) b); } } public interface Geary.Hashable { - public abstract uint get_hash(); + public abstract uint to_hash(); public static uint hash_func(void *ptr) { - return ((Hashable *) ptr)->get_hash(); + return ((Hashable *) ptr)->to_hash(); } } diff --git a/src/engine/common/common-nonblocking-mailbox.vala b/src/engine/common/common-nonblocking-mailbox.vala new file mode 100644 index 00000000..260573b5 --- /dev/null +++ b/src/engine/common/common-nonblocking-mailbox.vala @@ -0,0 +1,31 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Geary.Common.NonblockingMailbox : Object { + public int size { get { return queue.size; } } + + private Gee.List queue; + private NonblockingSemaphore spinlock = new NonblockingSemaphore(false); + + public NonblockingMailbox() { + queue = new Gee.LinkedList(); + } + + public void send(G msg) throws Error { + queue.add(msg); + spinlock.notify(); + } + + public async G recv_async(Cancellable? cancellable = null) throws Error { + for (;;) { + if (queue.size > 0) + return queue.remove_at(0); + + yield spinlock.wait_async(cancellable); + } + } +} + diff --git a/src/engine/common/common-nonblocking-mutex.vala b/src/engine/common/common-nonblocking-mutex.vala new file mode 100644 index 00000000..62b15361 --- /dev/null +++ b/src/engine/common/common-nonblocking-mutex.vala @@ -0,0 +1,39 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Geary.Common.NonblockingMutex { + private NonblockingSemaphore spinlock = new NonblockingSemaphore(false); + private bool locked = false; + private int next_token = 0; + private int locked_token = -1; + + public NonblockingMutex() { + } + + public async int claim_async(Cancellable? cancellable = null) throws Error { + for (;;) { + if (!locked) { + locked = true; + locked_token = next_token++; + + return locked_token; + } + + yield spinlock.wait_async(cancellable); + } + } + + public void release(int token) throws Error { + if (token != locked_token) + throw new IOError.INVALID_ARGUMENT("Token %d is not the lock token", token); + + locked = false; + locked_token = -1; + + spinlock.notify(); + } +} + diff --git a/src/engine/common/common-nonblocking-semaphore.vala b/src/engine/common/common-nonblocking-semaphore.vala index 30f366eb..83b88a24 100644 --- a/src/engine/common/common-nonblocking-semaphore.vala +++ b/src/engine/common/common-nonblocking-semaphore.vala @@ -7,17 +7,35 @@ public class Geary.Common.NonblockingSemaphore { private class Pending { public SourceFunc cb; + public Cancellable? cancellable; - public Pending(SourceFunc cb) { + public signal void cancelled(); + + public Pending(SourceFunc cb, Cancellable? cancellable) { this.cb = cb; + this.cancellable = cancellable; + + if (cancellable != null) + cancellable.cancelled.connect(on_cancelled); + } + + ~Pending() { + if (cancellable != null) + cancellable.cancelled.disconnect(on_cancelled); + } + + private void on_cancelled() { + cancelled(); } } + private bool broadcast; private Cancellable? cancellable; private bool passed = false; private Gee.List pending_queue = new Gee.LinkedList(); - public NonblockingSemaphore(Cancellable? cancellable = null) { + public NonblockingSemaphore(bool broadcast, Cancellable? cancellable = null) { + this.broadcast = broadcast; this.cancellable = cancellable; if (cancellable != null) @@ -29,34 +47,53 @@ public class Geary.Common.NonblockingSemaphore { warning("Nonblocking semaphore destroyed with %d pending callers", pending_queue.size); } - private void trigger_all() { - foreach (Pending pending in pending_queue) - Idle.add(pending.cb); + private void trigger(bool all) { + if (pending_queue.size == 0) + return; - pending_queue.clear(); + if (all) { + foreach (Pending pending in pending_queue) + Idle.add(pending.cb); + + pending_queue.clear(); + } else { + Pending pending = pending_queue.remove_at(0); + Idle.add(pending.cb); + } } public void notify() throws Error { check_cancelled(); passed = true; - trigger_all(); + + trigger(broadcast); } // TODO: Allow the caller to pass their own cancellable in if they want to be able to cancel // this particular wait (and not all waiting threads of execution) - public async void wait_async() throws Error { + public async void wait_async(Cancellable? cancellable = null) throws Error { for (;;) { + check_user_cancelled(cancellable); check_cancelled(); if (passed) return; - pending_queue.add(new Pending(wait_async.callback)); + Pending pending = new Pending(wait_async.callback, cancellable); + pending.cancelled.connect(on_pending_cancelled); + + pending_queue.add(pending); yield; + + pending.cancelled.disconnect(on_pending_cancelled); } } + public void reset() { + passed = false; + } + public bool is_cancelled() { return (cancellable != null) ? cancellable.is_cancelled() : false; } @@ -66,8 +103,20 @@ public class Geary.Common.NonblockingSemaphore { throw new IOError.CANCELLED("Semaphore cancelled"); } + private static void check_user_cancelled(Cancellable? cancellable) throws Error { + if (cancellable != null && cancellable.is_cancelled()) + throw new IOError.CANCELLED("User cancelled operation"); + } + + private void on_pending_cancelled(Pending pending) { + bool removed = pending_queue.remove(pending); + assert(removed); + + Idle.add(pending.cb); + } + private void on_cancelled() { - trigger_all(); + trigger(true); } } diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index 44aba97c..63d568e5 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -4,57 +4,87 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.Account : Object, Geary.Account, Geary.RemoteAccount { +public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount { private ClientSessionManager session_mgr; + private Gee.HashMap delims = new Gee.HashMap(); public Account(Credentials cred, uint default_port) { + base ("IMAP Account for %s".printf(cred.to_string())); + session_mgr = new ClientSessionManager(cred, default_port); } - public Geary.Email.Field get_required_fields_for_writing() { + public override Geary.Email.Field get_required_fields_for_writing() { return Geary.Email.Field.HEADER | Geary.Email.Field.BODY; } - public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, + public async string? get_folder_delimiter_async(string toplevel, Cancellable? cancellable = null) throws Error { - throw new EngineError.READONLY("IMAP readonly"); + if (delims.has_key(toplevel)) + return delims.get(toplevel); + + MailboxInformation? mbox = yield session_mgr.fetch_async(toplevel, cancellable); + if (mbox == null) { + throw new EngineError.NOT_FOUND("Toplevel folder %s not found on %s", toplevel, + session_mgr.to_string()); + } + + delims.set(toplevel, mbox.delim); + + return mbox.delim; } - public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection folders, + public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { - throw new EngineError.READONLY("IMAP readonly"); - } - - public async Gee.Collection list_folders_async(Geary.Folder? parent, - Cancellable? cancellable = null) throws Error { - Gee.Collection mboxes = yield session_mgr.list( - (parent != null) ? parent.get_name() : null, cancellable); + Gee.Collection mboxes; + try { + mboxes = (parent == null) + ? yield session_mgr.list_roots(cancellable) + : yield session_mgr.list(parent.get_fullpath(), parent.get_root().default_separator, + cancellable); + } catch (Error err) { + if (err is ImapError.SERVER_ERROR) + throw_not_found(parent); + else + throw err; + } Gee.Collection folders = new Gee.ArrayList(); - foreach (MailboxInformation mbox in mboxes) - folders.add(new Geary.Imap.Folder(session_mgr, mbox)); + foreach (MailboxInformation mbox in mboxes) { + if (parent == null) + delims.set(mbox.name, mbox.delim); + + string basename = mbox.get_path().last(); + + Geary.FolderPath path = (parent != null) + ? parent.get_child(basename) + : new Geary.FolderRoot(basename, mbox.delim, Folder.CASE_SENSITIVE); + + folders.add(new Geary.Imap.Folder(session_mgr, path, mbox)); + } return folders; } - public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, + public override async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { - MailboxInformation? mbox = yield session_mgr.fetch_async( - (parent != null) ? parent.get_name() : null, folder_name, cancellable); - if (mbox == null) - throw new EngineError.NOT_FOUND("Folder %s not found on server", folder_name); - - return new Geary.Imap.Folder(session_mgr, mbox); + try { + MailboxInformation? mbox = yield session_mgr.fetch_async(path.get_fullpath(), cancellable); + if (mbox == null) + throw_not_found(path); + + return new Geary.Imap.Folder(session_mgr, path, mbox); + } catch (ImapError err) { + if (err is ImapError.SERVER_ERROR) + throw_not_found(path); + else + throw err; + } } - public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) - throws Error { - throw new EngineError.READONLY("IMAP readonly"); - } - - public async void remove_many_folders_async(Gee.Set folders, - Cancellable? cancellable = null) throws Error { - throw new EngineError.READONLY("IMAP readonly"); + [NoReturn] + private void throw_not_found(Geary.FolderPath? path) throws EngineError { + throw new EngineError.NOT_FOUND("Folder %s not found on %s", + (path != null) ? path.to_string() : "root", session_mgr.to_string()); } } - diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 837a47a2..4c733570 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -5,24 +5,26 @@ */ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { + public const bool CASE_SENSITIVE = true; + private ClientSessionManager session_mgr; private MailboxInformation info; - private string name; + private Geary.FolderPath path; private Trillian readonly; private Imap.FolderProperties properties; private Mailbox? mailbox = null; - internal Folder(ClientSessionManager session_mgr, MailboxInformation info) { + internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, MailboxInformation info) { this.session_mgr = session_mgr; this.info = info; + this.path = path; - name = info.name; readonly = Trillian.UNKNOWN; properties = new Imap.FolderProperties(null, info.attrs); } - public override string get_name() { - return name; + public override Geary.FolderPath get_path() { + return path; } public Trillian is_readonly() { @@ -37,8 +39,9 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { if (mailbox != null) throw new EngineError.ALREADY_OPEN("%s already open", to_string()); - mailbox = yield session_mgr.select_examine_mailbox(name, !readonly, cancellable); - // hook up signals + mailbox = yield session_mgr.select_examine_mailbox(path.get_fullpath(info.delim), !readonly, + cancellable); + // TODO: hook up signals this.readonly = Trillian.from_boolean(readonly); properties.uid_validity = mailbox.uid_validity; diff --git a/src/engine/imap/decoders/imap-command-results.vala b/src/engine/imap/decoders/imap-command-results.vala index 5e6d859b..eab56314 100644 --- a/src/engine/imap/decoders/imap-command-results.vala +++ b/src/engine/imap/decoders/imap-command-results.vala @@ -10,5 +10,9 @@ public abstract class Geary.Imap.CommandResults { public CommandResults(StatusResponse status_response) { this.status_response = status_response; } + + public string to_string() { + return status_response.to_string(); + } } diff --git a/src/engine/imap/decoders/imap-list-results.vala b/src/engine/imap/decoders/imap-list-results.vala index 5f8aa93c..e947ee12 100644 --- a/src/engine/imap/decoders/imap-list-results.vala +++ b/src/engine/imap/decoders/imap-list-results.vala @@ -6,14 +6,34 @@ public class Geary.Imap.MailboxInformation { public string name { get; private set; } - public string delim { get; private set; } + public string? delim { get; private set; } public MailboxAttributes attrs { get; private set; } - public MailboxInformation(string name, string delim, MailboxAttributes attrs) { + public MailboxInformation(string name, string? delim, MailboxAttributes attrs) { this.name = name; this.delim = delim; this.attrs = attrs; } + + /** + * Will always return a list with at least one element in it. + */ + public Gee.List get_path() { + Gee.List path = new Gee.ArrayList(); + + if (delim != null) { + string[] split = name.split(delim); + foreach (string str in split) { + if (!String.is_empty(str)) + path.add(str); + } + } + + if (path.size == 0) + path.add(name); + + return path; + } } public class Geary.Imap.ListResults : Geary.Imap.CommandResults { @@ -37,7 +57,7 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults { try { StringParameter cmd = data.get_as_string(1); ListParameter attrs = data.get_as_list(2); - StringParameter delim = data.get_as_string(3); + StringParameter? delim = data.get_as_nullable_string(3); StringParameter mailbox = data.get_as_string(4); if (!cmd.equals_ci(ListCommand.NAME) && !cmd.equals_ci(XListCommand.NAME)) { @@ -60,7 +80,7 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults { attrlist.add(new MailboxAttribute(stringp.value)); } - MailboxInformation info = new MailboxInformation(mailbox.value, delim.value, + MailboxInformation info = new MailboxInformation(mailbox.value, delim.nullable_value, new MailboxAttributes(attrlist)); map.set(mailbox.value, info); diff --git a/src/engine/imap/message/imap-flag.vala b/src/engine/imap/message/imap-flag.vala index 49480658..b11ba078 100644 --- a/src/engine/imap/message/imap-flag.vala +++ b/src/engine/imap/message/imap-flag.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public abstract class Geary.Imap.Flag : Comparable, Hashable { +public abstract class Geary.Imap.Flag : Equalable, Hashable { public string value { get; private set; } public Flag(string value) { @@ -19,7 +19,7 @@ public abstract class Geary.Imap.Flag : Comparable, Hashable { return this.value.down() == value.down(); } - public bool equals(Comparable b) { + public bool equals(Equalable b) { Flag? flag = b as Flag; if (flag == null) return false; @@ -27,7 +27,7 @@ public abstract class Geary.Imap.Flag : Comparable, Hashable { return (flag == this) ? true : flag.equals_string(value); } - public uint get_hash() { + public uint to_hash() { return str_hash(value.down()); } diff --git a/src/engine/imap/message/imap-message-data.vala b/src/engine/imap/message/imap-message-data.vala index 5bdddb06..ba75e8a4 100644 --- a/src/engine/imap/message/imap-message-data.vala +++ b/src/engine/imap/message/imap-message-data.vala @@ -35,7 +35,7 @@ public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.Me private Gee.Set list; public Flags(Gee.Collection flags) { - list = new Gee.HashSet(Hashable.hash_func, Comparable.equal_func); + list = new Gee.HashSet(Hashable.hash_func, Equalable.equal_func); list.add_all(flags); } diff --git a/src/engine/imap/transport/imap-client-connection.vala b/src/engine/imap/transport/imap-client-connection.vala index 7c22400e..6b0e30d8 100644 --- a/src/engine/imap/transport/imap-client-connection.vala +++ b/src/engine/imap/transport/imap-client-connection.vala @@ -8,14 +8,18 @@ public class Geary.Imap.ClientConnection { public const uint16 DEFAULT_PORT = 143; public const uint16 DEFAULT_PORT_TLS = 993; + private const int FLUSH_TIMEOUT_MSEC = 100; + private string host_specifier; private uint16 default_port; private SocketClient socket_client = new SocketClient(); private SocketConnection? cx = null; private Serializer? ser = null; private Deserializer? des = null; + private Geary.Common.NonblockingMutex send_mutex = new Geary.Common.NonblockingMutex(); private int tag_counter = 0; private char tag_prefix = 'a'; + private uint flush_timeout_id = 0; public virtual signal void connected() { } @@ -26,6 +30,9 @@ public class Geary.Imap.ClientConnection { public virtual signal void sent_command(Command cmd) { } + public virtual signal void flush_failure(Error err) { + } + public virtual signal void received_status_response(StatusResponse status_response) { } @@ -41,7 +48,7 @@ public class Geary.Imap.ClientConnection { public virtual signal void receive_failure(Error err) { } - public virtual signal void deserialize_failure() { + public virtual signal void deserialize_failure(Error err) { } public ClientConnection(string host_specifier, uint16 default_port) { @@ -54,6 +61,8 @@ public class Geary.Imap.ClientConnection { ~ClientConnection() { // TODO: Close connection as gracefully as possible + if (flush_timeout_id != 0) + Source.remove(flush_timeout_id); } /** @@ -106,6 +115,8 @@ public class Geary.Imap.ClientConnection { ser = null; des = null; + des.xoff(); + disconnected(); } @@ -128,43 +139,47 @@ public class Geary.Imap.ClientConnection { } private void on_deserialize_failure() { - deserialize_failure(); + deserialize_failure(new ImapError.PARSE_ERROR("Unable to deserialize from %s", to_string())); } private void on_eos() { recv_closed(); } - /** - * Convenience method for send_async.begin(). - */ - public void post(Command cmd, AsyncReadyCallback cb, int priority = Priority.DEFAULT, - Cancellable? cancellable = null) { - send_async.begin(cmd, priority, cancellable, cb); - } - - /** - * Convenience method for sync_async.end(). This is largely provided for symmetry with - * post_send(). - */ - public void finish_post(AsyncResult result) throws Error { - send_async.end(result); - } - - public async void send_async(Command cmd, int priority = Priority.DEFAULT, - Cancellable? cancellable = null) throws Error { + public async void send_async(Command cmd, Cancellable? cancellable = null) throws Error { check_for_connection(); - cmd.serialize(ser); + // need to run this in critical section because OutputStreams can only be written to + // serially + int token = yield send_mutex.claim_async(cancellable); - // TODO: At this point, we flush each command as it's written; at some point we'll have - // a queuing strategy that means serialized data is pushed out to the wire only at certain - // times - yield ser.flush_async(priority, cancellable); + yield cmd.serialize(ser); + + send_mutex.release(token); + + if (flush_timeout_id == 0) + flush_timeout_id = Timeout.add(FLUSH_TIMEOUT_MSEC, on_flush_timeout); sent_command(cmd); } + private bool on_flush_timeout() { + do_flush_async.begin(); + + flush_timeout_id = 0; + + return false; + } + + private async void do_flush_async() { + try { + if (ser != null) + yield ser.flush_async(); + } catch (Error err) { + flush_failure(err); + } + } + private void check_for_connection() throws Error { if (cx == null) throw new ImapError.NOT_CONNECTED("Not connected to %s", to_string()); diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index 6689b898..081b55d9 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -10,6 +10,7 @@ public class Geary.Imap.ClientSessionManager { private Credentials cred; private uint default_port; private Gee.HashSet sessions = new Gee.HashSet(); + private Geary.Common.NonblockingMutex sessions_mutex = new Geary.Common.NonblockingMutex(); private Gee.HashSet examined_contexts = new Gee.HashSet(); private Gee.HashSet selected_contexts = new Gee.HashSet(); private int keepalive_sec = ClientSession.DEFAULT_KEEPALIVE_SEC; @@ -45,31 +46,46 @@ public class Geary.Imap.ClientSessionManager { session.enable_keepalives(keepalive_sec); } - public async Gee.Collection list(string? parent_name, + public async Gee.Collection list_roots( Cancellable? cancellable = null) throws Error { - // build a proper IMAP specifier - string specifier = parent_name ?? "/"; - specifier += (specifier.has_suffix("/")) ? "%" : "/%"; - ClientSession session = yield get_authorized_session(cancellable); ListResults results = ListResults.decode(yield session.send_command_async( - new ListCommand(session.generate_tag(), specifier), cancellable)); + new ListCommand.wildcarded(session.generate_tag(), "%", "%"), cancellable)); + + if (results.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string()); return results.get_all(); } - public async Geary.Imap.MailboxInformation? fetch_async(string? parent_name, string folder_name, - Cancellable? cancellable = null) throws Error { + public async Gee.Collection list(string parent, + string delim, Cancellable? cancellable = null) throws Error { // build a proper IMAP specifier - string specifier = parent_name ?? "/"; - specifier += (specifier.has_suffix("/")) ? folder_name : "/%s".printf(folder_name); + string specifier = parent; + specifier += specifier.has_suffix(delim) ? "%" : (delim + "%"); ClientSession session = yield get_authorized_session(cancellable); ListResults results = ListResults.decode(yield session.send_command_async( new ListCommand(session.generate_tag(), specifier), cancellable)); + if (results.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string()); + + return results.get_all(); + } + + public async Geary.Imap.MailboxInformation? fetch_async(string path, + Cancellable? cancellable = null) throws Error { + ClientSession session = yield get_authorized_session(cancellable); + + ListResults results = ListResults.decode(yield session.send_command_async( + new ListCommand(session.generate_tag(), path), cancellable)); + + if (results.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string()); + return (results.get_count() > 0) ? results.get_all()[0] : null; } @@ -93,6 +109,9 @@ public class Geary.Imap.ClientSessionManager { SelectExamineResults results; ClientSession session = yield select_examine_async(path, is_select, out results, cancellable); + if (results.status_response.status != Status.OK) + throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string()); + SelectedContext new_context = new SelectedContext(session, results); // Can't use the ternary operator due to this bug: @@ -131,6 +150,7 @@ public class Geary.Imap.ClientSessionManager { context.session.close_mailbox_async.begin(); } + // This should only be called when sessions_mutex is locked. private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error { debug("Creating new session to %s", cred.server); @@ -151,13 +171,24 @@ public class Geary.Imap.ClientSessionManager { } private async ClientSession get_authorized_session(Cancellable? cancellable) throws Error { + int token = yield sessions_mutex.claim_async(cancellable); + + ClientSession? found_session = null; foreach (ClientSession session in sessions) { string? mailbox; - if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) - return session; + if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) { + found_session = session; + + break; + } } - return yield create_new_authorized_session(cancellable); + if (found_session == null) + found_session = yield create_new_authorized_session(cancellable); + + sessions_mutex.release(token); + + return found_session; } private async ClientSession select_examine_async(string folder, bool is_select, @@ -183,5 +214,12 @@ public class Geary.Imap.ClientSessionManager { bool removed = sessions.remove(session); assert(removed); } + + /** + * Use only for debugging and logging. + */ + public string to_string() { + return cred.to_string(); + } } diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index 1bf4c8ed..7caf4930 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -413,10 +413,12 @@ public class Geary.Imap.ClientSession { cx.connected.connect(on_network_connected); cx.disconnected.connect(on_network_disconnected); cx.sent_command.connect(on_network_sent_command); + cx.flush_failure.connect(on_network_flush_error); cx.received_status_response.connect(on_received_status_response); cx.received_server_data.connect(on_received_server_data); cx.received_bad_response.connect(on_received_bad_response); - cx.receive_failure.connect(on_receive_failed); + cx.receive_failure.connect(on_network_receive_failure); + cx.deserialize_failure.connect(on_network_receive_failure); cx.connect_async.begin(connect_params.cancellable, on_connect_completed); @@ -988,7 +990,7 @@ public class Geary.Imap.ClientSession { } try { - yield cx.send_async(cmd, Priority.DEFAULT, cancellable); + yield cx.send_async(cmd, cancellable); } catch (Error err) { return new AsyncCommandResponse(null, user, err); } @@ -1050,6 +1052,11 @@ public class Geary.Imap.ClientSession { #endif } + private void on_network_flush_error(Error err) { + debug("Flush error on %s: %s", to_string(), err.message); + fsm.issue(Event.SEND_ERROR, null, null, err); + } + private void on_received_status_response(StatusResponse status_response) { assert(!current_cmd_response.is_sealed()); current_cmd_response.seal(status_response); @@ -1086,7 +1093,7 @@ public class Geary.Imap.ClientSession { debug("Received bad response %s: %s", root.to_string(), err.message); } - private void on_receive_failed(Error err) { + private void on_network_receive_failure(Error err) { debug("Receive failed: %s", err.message); fsm.issue(Event.RECV_ERROR, null, null, err); } diff --git a/src/engine/sqlite/api/sqlite-account.vala b/src/engine/sqlite/api/sqlite-account.vala index 7ff1298c..79f60999 100644 --- a/src/engine/sqlite/api/sqlite-account.vala +++ b/src/engine/sqlite/api/sqlite-account.vala @@ -4,12 +4,14 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { +public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount { private MailDatabase db; private FolderTable folder_table; private MessageTable message_table; public Account(Geary.Credentials cred) { + base ("SQLite account for %s".printf(cred.to_string())); + try { db = new MailDatabase(cred.user); } catch (Error err) { @@ -20,53 +22,76 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount { message_table = db.get_message_table(); } - public Geary.Email.Field get_required_fields_for_writing() { + public override Geary.Email.Field get_required_fields_for_writing() { return Geary.Email.Field.NONE; } - public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder, - Cancellable? cancellable = null) throws Error { - yield folder_table.create_async(new FolderRow(folder_table, folder.get_name(), Row.INVALID_ID), - cancellable); + private async int64 fetch_id_async(Geary.FolderPath path, Cancellable? cancellable = null) + throws Error { + FolderRow? row = yield folder_table.fetch_descend_async(path.as_list(), cancellable); + if (row == null) + throw new EngineError.NOT_FOUND("Cannot find local path to %s", path.to_string()); + + return row.id; } - public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection folders, + private async int64 fetch_parent_id_async(Geary.FolderPath path, Cancellable? cancellable = null) + throws Error { + return path.is_root() ? Row.INVALID_ID : yield fetch_id_async(path.get_parent(), cancellable); + } + + public async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null) + throws Error { + int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable); + yield folder_table.create_async(new FolderRow(folder_table, folder.get_path().basename, + parent_id), cancellable); + } + + public async void clone_many_folders_async(Gee.Collection folders, Cancellable? cancellable = null) throws Error { Gee.List rows = new Gee.ArrayList(); - foreach (Geary.Folder folder in folders) - rows.add(new FolderRow(db.get_folder_table(), folder.get_name(), Row.INVALID_ID)); + foreach (Geary.Folder folder in folders) { + int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable); + rows.add(new FolderRow(db.get_folder_table(), folder.get_path().basename, parent_id)); + } yield folder_table.create_many_async(rows, cancellable); } - public async Gee.Collection list_folders_async(Geary.Folder? parent, + public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { - Gee.List rows = yield folder_table.list_async(Row.INVALID_ID, cancellable); + int64 parent_id = (parent != null) + ? yield fetch_id_async(parent, cancellable) + : Row.INVALID_ID; + + if (parent != null) + assert(parent_id != Row.INVALID_ID); + + Gee.List rows = yield folder_table.list_async(parent_id, cancellable); + if (rows.size == 0) { + throw new EngineError.NOT_FOUND("No local folders in %s", + (parent != null) ? parent.get_fullpath() : "root"); + } Gee.Collection folders = new Gee.ArrayList(); - foreach (FolderRow row in rows) - folders.add(new Geary.Sqlite.Folder(db, row)); + foreach (FolderRow row in rows) { + Geary.FolderPath path = (parent != null) + ? parent.get_child(row.name) + : new Geary.FolderRoot(row.name, "/", Geary.Imap.Folder.CASE_SENSITIVE); + + folders.add(new Geary.Sqlite.Folder(db, row, path)); + } return folders; } - public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name, + public override async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { - FolderRow? row = yield folder_table.fetch_async(Row.INVALID_ID, folder_name, cancellable); + FolderRow? row = yield folder_table.fetch_descend_async(path.as_list(), cancellable); if (row == null) - throw new EngineError.NOT_FOUND("\"%s\" not found in local database", folder_name); + throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string()); - return new Geary.Sqlite.Folder(db, row); - } - - public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null) - throws Error { - // TODO - } - - public async void remove_many_folders_async(Gee.Set folders, - Cancellable? cancellable = null) throws Error { - // TODO + return new Geary.Sqlite.Folder(db, row, path); } public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count, diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index 1357e0e2..a33f585d 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -13,14 +13,13 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { private MessageTable message_table; private MessageLocationTable location_table; private ImapMessageLocationPropertiesTable imap_location_table; - private string name; + private Geary.FolderPath path; private bool opened = false; - internal Folder(MailDatabase db, FolderRow folder_row) throws Error { + internal Folder(MailDatabase db, FolderRow folder_row, Geary.FolderPath path) throws Error { this.db = db; this.folder_row = folder_row; - - name = folder_row.name; + this.path = path; message_table = db.get_message_table(); location_table = db.get_message_location_table(); @@ -32,8 +31,8 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); } - public override string get_name() { - return name; + public override Geary.FolderPath get_path() { + return path; } public override Geary.FolderProperties? get_properties() { @@ -75,7 +74,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { if (yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id, cancellable)) { throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s", - location.uid.to_string(), get_name()); + location.uid.to_string(), to_string()); } message_id = yield message_table.create_async( @@ -155,8 +154,10 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position, cancellable); - if (location_row == null) - throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name); + if (location_row == null) { + throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, + to_string()); + } assert(location_row.position == position); @@ -164,15 +165,17 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { location_row.id, cancellable); if (imap_location_row == null) { throw new EngineError.NOT_FOUND("No IMAP location properties at position %d in %s", - position, name); + position, to_string()); } assert(imap_location_row.location_id == location_row.id); MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, required_fields, cancellable); - if (message_row == null) - throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name); + if (message_row == null) { + throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, + to_string()); + } if (!message_row.fields.is_set(required_fields)) { throw new EngineError.INCOMPLETE_MESSAGE( diff --git a/src/engine/sqlite/email/sqlite-folder-table.vala b/src/engine/sqlite/email/sqlite-folder-table.vala index 60b65f21..f53c8244 100644 --- a/src/engine/sqlite/email/sqlite-folder-table.vala +++ b/src/engine/sqlite/email/sqlite-folder-table.vala @@ -87,5 +87,46 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { return (!result.finished) ? new FolderRow.from_query_result(this, result) : null; } + + public async FolderRow? fetch_descend_async(Gee.List path, Cancellable? cancellable = null) + throws Error { + assert(path.size > 0); + + int64 parent_id = Row.INVALID_ID; + + // walk the folder tree to the final node (which is at length - 1 - 1) + int length = path.size; + for (int ctr = 0; ctr < length - 1; ctr++) { + SQLHeavy.Query query; + if (parent_id != Row.INVALID_ID) { + query = db.prepare("SELECT id FROM FolderTable WHERE parent_id=? AND name=?"); + query.bind_int64(0, parent_id); + query.bind_string(1, path[ctr]); + } else { + query = db.prepare("SELECT id FROM FolderTable WHERE parent_id IS NULL AND name=?"); + query.bind_string(0, path[ctr]); + } + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + if (result.finished) + return null; + + int64 id = result.fetch_int64(0); + + // watch for loops, real bad if it happens ... could be more thorough here, but at least + // one level of checking is better than none + if (id == parent_id) { + warning("Loop found in database: parent of %lld is %lld in FolderTable", + parent_id, id); + + return null; + } + + parent_id = id; + } + + // do full fetch on this folder + return yield fetch_async(parent_id, path.last(), cancellable); + } } diff --git a/src/engine/state/state-machine.vala b/src/engine/state/state-machine.vala index 1c44bf8b..2d5fde6f 100644 --- a/src/engine/state/state-machine.vala +++ b/src/engine/state/state-machine.vala @@ -103,7 +103,7 @@ public class Geary.State.Machine { } public string to_string() { - return "Machine %s".printf(descriptor.name); + return "Machine %s [%s]".printf(descriptor.name, descriptor.get_state_string(state)); } } diff --git a/src/wscript b/src/wscript index cfcd40bd..ba607477 100644 --- a/src/wscript +++ b/src/wscript @@ -6,6 +6,7 @@ def build(bld): bld.common_src = [ '../common/common-date.vala', + '../common/common-intl.vala', '../common/common-yorba-application.vala' ] @@ -13,23 +14,28 @@ def build(bld): bld.common_packages = ['glib-2.0', 'unique-1.0', 'posix' ] bld.engine_src = [ + '../engine/api/geary-abstract-account.vala', '../engine/api/geary-abstract-folder.vala', '../engine/api/geary-account.vala', '../engine/api/geary-credentials.vala', '../engine/api/geary-email-location.vala', '../engine/api/geary-email-properties.vala', '../engine/api/geary-email.vala', + '../engine/api/geary-engine-account.vala', '../engine/api/geary-engine-error.vala', '../engine/api/geary-engine-folder.vala', '../engine/api/geary-engine.vala', + '../engine/api/geary-folder-path.vala', '../engine/api/geary-folder-properties.vala', '../engine/api/geary-folder.vala', - '../engine/api/geary-imap-engine.vala', + '../engine/api/geary-generic-imap-account.vala', '../engine/api/geary-local-interfaces.vala', '../engine/api/geary-remote-interfaces.vala', '../engine/common/common-interfaces.vala', '../engine/common/common-message-data.vala', + '../engine/common/common-nonblocking-mailbox.vala', + '../engine/common/common-nonblocking-mutex.vala', '../engine/common/common-nonblocking-semaphore.vala', '../engine/common/common-string.vala',