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