From df7a30cbe15e5c6474f3da3cb403d09ff81bb310 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Wed, 5 Aug 2020 14:10:11 +1000 Subject: [PATCH] Geary.Account: Add {de}register_local_folder methods Support API clients registering their own local folder implementations. Use this (and the last commit) to generalise handling of the outbox by GenericAccount by registering it when the outbox postie is started, and when creating a map of folders that contain specific ids. This also ensures API clients are informed of the outbox becoming available, allowing some special case code to be removed from the app controller. --- .../application/application-controller.vala | 2 - src/engine/api/geary-account.vala | 36 ++++++ .../imap-engine-generic-account.vala | 116 ++++++++++++++---- src/engine/outbox/outbox-folder.vala | 23 ---- test/mock/mock-account.vala | 12 ++ 5 files changed, 142 insertions(+), 47 deletions(-) diff --git a/src/client/application/application-controller.vala b/src/client/application/application-controller.vala index 473f18f7..38b1bfea 100644 --- a/src/client/application/application-controller.vala +++ b/src/client/application/application-controller.vala @@ -976,8 +976,6 @@ internal class Application.Controller : smtp.email_sent.connect(on_sent); smtp.sending_monitor.start.connect(on_sending_started); smtp.sending_monitor.finish.connect(on_sending_finished); - var outbox_context = new FolderContext(smtp.outbox); - context.add_folders(Geary.Collection.single(outbox_context)); } // Notify before opening so that listeners have a chance to diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 4a4850a5..d81661ca 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -434,6 +434,42 @@ public abstract class Geary.Account : BaseObject, Logging.Source { GLib.Cancellable? cancellable = null ) throws GLib.Error; + /** + * Registers a local folder with the account. + * + * The registering a local folder will cause the account will hook + * to the folder's signals such as {@link Folder.email_appended} + * and forward them on to the account-wide equivalents, include + * the list folder in the folder list, allow email in the folder + * to be found by account-wide operations, and so on. The folder + * will then be signalled as being available via {@link + * folders_available_unavailable}. + * + * A {@link EngineError.ALREADY_EXISTS} exception will be thrown + * if the given folder is already registered, or {@link + * EngineError.NOT_FOUND} if its path does not have {@link + * local_folder_root} as its root. + * + * @see deregister_local_folder + */ + public abstract void register_local_folder(Folder local) + throws GLib.Error; + + /** + * De-registers a local folder with the account. + * + * De-registering a previously registered local folder will signal + * it as being unavailable via {@link + * folders_available_unavailable} and unhook it from the account. + * + * A {@link local_folder_root} error will be thrown if the given + * folder is not already registered. + * + * @see register_local_folder + */ + public abstract void deregister_local_folder(Folder local) + throws GLib.Error; + /** * Search the local account for emails referencing a Message-ID value * (which can appear in the Message-ID header itself, as well as the diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 3b8536c3..8285af57 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -48,8 +48,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { private Cancellable? open_cancellable = null; private Nonblocking.Semaphore? remote_ready_lock = null; - private Gee.Map folder_map = + private Gee.Map remote_folders = new Gee.HashMap(); + private Gee.Map local_folders = + new Gee.HashMap(); private AccountProcessor? processor; private AccountSynchronizer sync; @@ -152,7 +154,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { // outgoing so local folders can be loaded first in case // queued mail gets sent and needs to get saved somewhere. yield this.imap.start(cancellable); - this.queue_operation(new StartPostie(this)); + this.queue_operation(new StartPostie(this, this.smtp.outbox)); // Kick off a background update of the search table. // @@ -185,13 +187,16 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { this.imap.discard_returned_sessions = true; this.remote_ready_lock.reset(); - // Close folders and ensure they do in fact close + // Notify folders are going away and wait for remotes to close - Gee.BidirSortedSet remotes = sort_by_path(this.folder_map.values); - this.folder_map.clear(); + var locals = sort_by_path(this.local_folders.values); + this.local_folders.clear(); + notify_folders_available_unavailable(null, locals); + + var remotes = sort_by_path(this.remote_folders.values); + this.remote_folders.clear(); notify_folders_available_unavailable(null, remotes); - - foreach (Geary.Folder folder in remotes) { + foreach (var folder in remotes) { debug("Waiting for remote to close: %s", folder.to_string()); yield folder.wait_for_close_async(); } @@ -402,7 +407,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { /** {@inheritDoc} */ public override Folder get_folder(FolderPath path) throws EngineError.NOT_FOUND { - Folder? folder = this.folder_map.get(path); + Folder? folder = null; + if (this.local.imap_folder_root.is_descendant(path)) { + folder = this.remote_folders.get(path); + } else if (this.local_folder_root.is_descendant(path)) { + folder = this.local_folders.get(path); + } if (folder == null) { throw new EngineError.NOT_FOUND( "Folder not found: %s", path.to_string() @@ -414,21 +424,37 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { /** {@inheritDoc} */ public override Gee.Collection list_folders() { var all = new Gee.HashSet(); - all.add_all(this.folder_map.values); + all.add_all(this.remote_folders.values); + all.add_all(this.local_folders.values); return all; } /** {@inheritDoc} */ public override Gee.Collection list_matching_folders(FolderPath? parent) throws EngineError.NOT_FOUND { - return traverse(folder_map.keys) + Gee.Map? folders = null; + if (this.local.imap_folder_root.is_descendant(parent)) { + folders = this.remote_folders; + } else if (this.local_folder_root.is_descendant(parent)) { + folders = this.local_folders; + } else { + throw new EngineError.NOT_FOUND( + "Unknown folder root: %s", parent.to_string() + ); + } + if (!folders.has_key(parent)) { + throw new EngineError.NOT_FOUND( + "Unknown parent: %s", parent.to_string() + ); + } + return traverse(folders.keys) .filter(p => { FolderPath? path_parent = p.parent; return ((parent == null && path_parent == null) || (parent != null && path_parent != null && path_parent.equal_to(parent))); }) - .map(p => folder_map.get(p)) + .map(p => folders.get(p)) .to_array_list(); } @@ -466,7 +492,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { FolderPath root = yield remote.get_default_personal_namespace(cancellable); FolderPath path = root.get_child(name); - if (this.folder_map.has_key(path)) { + if (this.remote_folders.has_key(path)) { throw new EngineError.ALREADY_EXISTS( "Folder already exists: %s", path.to_string() ); @@ -481,7 +507,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { remote_folder, cancellable ); add_folders(Collection.single(local_folder), false); - var folder = this.folder_map.get(path); + var folder = this.remote_folders.get(path); if (use != NONE) { promote_folders( Collection.single_map(use, folder) @@ -490,6 +516,41 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { return folder; } + /** {@inheritDoc} */ + public override void register_local_folder(Folder local) + throws GLib.Error { + var path = local.path; + if (this.local_folders.has_key(path)) { + throw new EngineError.ALREADY_EXISTS( + "Folder already exists: %s", path.to_string() + ); + } + if (!this.local_folder_root.is_descendant(path)) { + throw new EngineError.NOT_FOUND( + "Not a desendant of the local folder root: %s", path.to_string() + ); + } + this.local_folders.set(path, local); + notify_folders_available_unavailable( + sort_by_path(Collection.single(local)), null + ); + } + + /** {@inheritDoc} */ + public override void deregister_local_folder(Folder local) + throws GLib.Error { + var path = local.path; + if (!this.local_folders.has_key(path)) { + throw new EngineError.NOT_FOUND( + "Unknown folder: %s", path.to_string() + ); + } + notify_folders_available_unavailable( + null, sort_by_path(Collection.single(local)) + ); + this.local_folders.unset(path); + } + private ImapDB.EmailIdentifier check_id(Geary.EmailIdentifier id) throws EngineError { ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier; if (imapdb_id == null) @@ -563,7 +624,13 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { Gee.MultiMap map = new Gee.HashMultiMap(); yield this.local.get_containing_folders_async(ids, map, cancellable); - yield this.smtp.outbox.add_to_containing_folders_async(ids, map, cancellable); + foreach (var folder in this.local_folders.values) { + var path = folder.path; + var matching = yield folder.contains_identifiers(ids, cancellable); + foreach (var id in matching) { + map.set(id, path); + } + } return (map.size == 0) ? null : map; } @@ -608,7 +675,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { ); foreach(ImapDB.Folder db_folder in db_folders) { FolderPath path = db_folder.get_path(); - if (!this.folder_map.has_key(path)) { + if (!this.remote_folders.has_key(path)) { MinimalFolder folder = new_folder(db_folder); folder.report_problem.connect(notify_report_problem); if (folder.used_as == NONE) { @@ -618,7 +685,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { } } built_folders.add(folder); - this.folder_map.set(folder.path, folder); + this.remote_folders.set(folder.path, folder); } } @@ -699,9 +766,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { Account.folder_path_comparator ); foreach(Geary.Folder folder in folders) { - MinimalFolder? impl = this.folder_map.get(folder.path); + MinimalFolder? impl = this.remote_folders.get(folder.path); if (impl != null) { - this.folder_map.unset(folder.path); + this.remote_folders.unset(folder.path); removed.add(impl); } } @@ -742,7 +809,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { Gee.List search_names = special_search_names.get(use); foreach (string search_name in search_names) { FolderPath search_path = root.get_child(search_name); - foreach (FolderPath test_path in folder_map.keys) { + foreach (FolderPath test_path in this.remote_folders.keys) { if (test_path.compare_normalized_ci(search_path) == 0) { path = search_path; break; @@ -764,8 +831,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account { ); } - if (this.folder_map.has_key(path)) { - special = this.folder_map.get(path); + if (this.remote_folders.has_key(path)) { + special = this.remote_folders.get(path); promote_folders( Collection.single_map(use, special) ); @@ -1077,12 +1144,17 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation { internal class Geary.ImapEngine.StartPostie : AccountOperation { - internal StartPostie(Account account) { + private Outbox.Folder outbox; + + + internal StartPostie(Account account, Outbox.Folder outbox) { base(account); + this.outbox = outbox; } public override async void execute(GLib.Cancellable cancellable) throws GLib.Error { + this.account.register_local_folder(this.outbox); yield this.account.outgoing.start(cancellable); } diff --git a/src/engine/outbox/outbox-folder.vala b/src/engine/outbox/outbox-folder.vala index 9fb84c2b..51c3c7b2 100644 --- a/src/engine/outbox/outbox-folder.vala +++ b/src/engine/outbox/outbox-folder.vala @@ -383,29 +383,6 @@ public class Geary.Outbox.Folder : throw new EngineError.UNSUPPORTED("Folder special use cannot be changed"); } - internal async void - add_to_containing_folders_async(Gee.Collection ids, - Gee.MultiMap map, - GLib.Cancellable? cancellable) - throws GLib.Error { - check_open(); - yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { - foreach (Geary.EmailIdentifier id in ids) { - EmailIdentifier? outbox_id = id as EmailIdentifier; - if (outbox_id == null) - continue; - - OutboxRow? row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable); - if (row == null) - continue; - - map.set(id, path); - } - - return Db.TransactionOutcome.DONE; - }, cancellable); - } - // Utility for getting an email object back from an outbox row. private Geary.Email row_to_email(OutboxRow row) throws Error { Geary.Email? email = null; diff --git a/test/mock/mock-account.vala b/test/mock/mock-account.vala index 7d598a0a..173b2eca 100644 --- a/test/mock/mock-account.vala +++ b/test/mock/mock-account.vala @@ -88,6 +88,18 @@ public class Mock.Account : Geary.Account, ); } + /** {@inheritDoc} */ + public override void register_local_folder(Geary.Folder local) + throws GLib.Error { + void_call("register_local_folder", { local }); + } + + /** {@inheritDoc} */ + public override void deregister_local_folder(Geary.Folder local) + throws GLib.Error { + void_call("deregister_local_folder", { local }); + } + public override Geary.EmailIdentifier to_email_identifier(GLib.Variant serialised) throws Geary.EngineError.BAD_PARAMETERS { try {