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.
This commit is contained in:
Michael Gratton 2020-08-05 14:10:11 +10:00 committed by Michael James Gratton
parent 910228093c
commit df7a30cbe1
5 changed files with 142 additions and 47 deletions

View file

@ -976,8 +976,6 @@ internal class Application.Controller :
smtp.email_sent.connect(on_sent); smtp.email_sent.connect(on_sent);
smtp.sending_monitor.start.connect(on_sending_started); smtp.sending_monitor.start.connect(on_sending_started);
smtp.sending_monitor.finish.connect(on_sending_finished); 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 // Notify before opening so that listeners have a chance to

View file

@ -434,6 +434,42 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
GLib.Cancellable? cancellable = null GLib.Cancellable? cancellable = null
) throws GLib.Error; ) 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 * Search the local account for emails referencing a Message-ID value
* (which can appear in the Message-ID header itself, as well as the * (which can appear in the Message-ID header itself, as well as the

View file

@ -48,8 +48,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
private Cancellable? open_cancellable = null; private Cancellable? open_cancellable = null;
private Nonblocking.Semaphore? remote_ready_lock = null; private Nonblocking.Semaphore? remote_ready_lock = null;
private Gee.Map<FolderPath,MinimalFolder> folder_map = private Gee.Map<FolderPath,MinimalFolder> remote_folders =
new Gee.HashMap<FolderPath,MinimalFolder>(); new Gee.HashMap<FolderPath,MinimalFolder>();
private Gee.Map<FolderPath,Folder> local_folders =
new Gee.HashMap<FolderPath,Folder>();
private AccountProcessor? processor; private AccountProcessor? processor;
private AccountSynchronizer sync; 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 // outgoing so local folders can be loaded first in case
// queued mail gets sent and needs to get saved somewhere. // queued mail gets sent and needs to get saved somewhere.
yield this.imap.start(cancellable); 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. // 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.imap.discard_returned_sessions = true;
this.remote_ready_lock.reset(); 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<Folder> remotes = sort_by_path(this.folder_map.values); var locals = sort_by_path(this.local_folders.values);
this.folder_map.clear(); 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); notify_folders_available_unavailable(null, remotes);
foreach (var folder in remotes) {
foreach (Geary.Folder folder in remotes) {
debug("Waiting for remote to close: %s", folder.to_string()); debug("Waiting for remote to close: %s", folder.to_string());
yield folder.wait_for_close_async(); yield folder.wait_for_close_async();
} }
@ -402,7 +407,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/** {@inheritDoc} */ /** {@inheritDoc} */
public override Folder get_folder(FolderPath path) public override Folder get_folder(FolderPath path)
throws EngineError.NOT_FOUND { 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) { if (folder == null) {
throw new EngineError.NOT_FOUND( throw new EngineError.NOT_FOUND(
"Folder not found: %s", path.to_string() "Folder not found: %s", path.to_string()
@ -414,21 +424,37 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/** {@inheritDoc} */ /** {@inheritDoc} */
public override Gee.Collection<Folder> list_folders() { public override Gee.Collection<Folder> list_folders() {
var all = new Gee.HashSet<Folder>(); var all = new Gee.HashSet<Folder>();
all.add_all(this.folder_map.values); all.add_all(this.remote_folders.values);
all.add_all(this.local_folders.values);
return all; return all;
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
public override Gee.Collection<Folder> list_matching_folders(FolderPath? parent) public override Gee.Collection<Folder> list_matching_folders(FolderPath? parent)
throws EngineError.NOT_FOUND { throws EngineError.NOT_FOUND {
return traverse<FolderPath>(folder_map.keys) Gee.Map<FolderPath,Folder>? 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<FolderPath>(folders.keys)
.filter(p => { .filter(p => {
FolderPath? path_parent = p.parent; FolderPath? path_parent = p.parent;
return ((parent == null && path_parent == null) || return ((parent == null && path_parent == null) ||
(parent != null && path_parent != null && (parent != null && path_parent != null &&
path_parent.equal_to(parent))); path_parent.equal_to(parent)));
}) })
.map<Geary.Folder>(p => folder_map.get(p)) .map<Geary.Folder>(p => folders.get(p))
.to_array_list(); .to_array_list();
} }
@ -466,7 +492,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
FolderPath root = FolderPath root =
yield remote.get_default_personal_namespace(cancellable); yield remote.get_default_personal_namespace(cancellable);
FolderPath path = root.get_child(name); 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( throw new EngineError.ALREADY_EXISTS(
"Folder already exists: %s", path.to_string() "Folder already exists: %s", path.to_string()
); );
@ -481,7 +507,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
remote_folder, cancellable remote_folder, cancellable
); );
add_folders(Collection.single(local_folder), false); add_folders(Collection.single(local_folder), false);
var folder = this.folder_map.get(path); var folder = this.remote_folders.get(path);
if (use != NONE) { if (use != NONE) {
promote_folders( promote_folders(
Collection.single_map<Folder.SpecialUse,Folder>(use, folder) Collection.single_map<Folder.SpecialUse,Folder>(use, folder)
@ -490,6 +516,41 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return folder; 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 { private ImapDB.EmailIdentifier check_id(Geary.EmailIdentifier id) throws EngineError {
ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier; ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
if (imapdb_id == null) if (imapdb_id == null)
@ -563,7 +624,13 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
Gee.MultiMap<EmailIdentifier,FolderPath> map = Gee.MultiMap<EmailIdentifier,FolderPath> map =
new Gee.HashMultiMap<EmailIdentifier,FolderPath>(); new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
yield this.local.get_containing_folders_async(ids, map, cancellable); 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; 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) { foreach(ImapDB.Folder db_folder in db_folders) {
FolderPath path = db_folder.get_path(); 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); MinimalFolder folder = new_folder(db_folder);
folder.report_problem.connect(notify_report_problem); folder.report_problem.connect(notify_report_problem);
if (folder.used_as == NONE) { if (folder.used_as == NONE) {
@ -618,7 +685,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
} }
} }
built_folders.add(folder); 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 Account.folder_path_comparator
); );
foreach(Geary.Folder folder in folders) { 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) { if (impl != null) {
this.folder_map.unset(folder.path); this.remote_folders.unset(folder.path);
removed.add(impl); removed.add(impl);
} }
} }
@ -742,7 +809,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
Gee.List<string> search_names = special_search_names.get(use); Gee.List<string> search_names = special_search_names.get(use);
foreach (string search_name in search_names) { foreach (string search_name in search_names) {
FolderPath search_path = root.get_child(search_name); 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) { if (test_path.compare_normalized_ci(search_path) == 0) {
path = search_path; path = search_path;
break; break;
@ -764,8 +831,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
); );
} }
if (this.folder_map.has_key(path)) { if (this.remote_folders.has_key(path)) {
special = this.folder_map.get(path); special = this.remote_folders.get(path);
promote_folders( promote_folders(
Collection.single_map<Folder.SpecialUse,Folder>(use, special) Collection.single_map<Folder.SpecialUse,Folder>(use, special)
); );
@ -1077,12 +1144,17 @@ internal class Geary.ImapEngine.LoadFolders : AccountOperation {
internal class Geary.ImapEngine.StartPostie : AccountOperation { internal class Geary.ImapEngine.StartPostie : AccountOperation {
internal StartPostie(Account account) { private Outbox.Folder outbox;
internal StartPostie(Account account, Outbox.Folder outbox) {
base(account); base(account);
this.outbox = outbox;
} }
public override async void execute(GLib.Cancellable cancellable) public override async void execute(GLib.Cancellable cancellable)
throws GLib.Error { throws GLib.Error {
this.account.register_local_folder(this.outbox);
yield this.account.outgoing.start(cancellable); yield this.account.outgoing.start(cancellable);
} }

View file

@ -383,29 +383,6 @@ public class Geary.Outbox.Folder :
throw new EngineError.UNSUPPORTED("Folder special use cannot be changed"); throw new EngineError.UNSUPPORTED("Folder special use cannot be changed");
} }
internal async void
add_to_containing_folders_async(Gee.Collection<Geary.EmailIdentifier> ids,
Gee.MultiMap<Geary.EmailIdentifier,FolderPath> 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. // Utility for getting an email object back from an outbox row.
private Geary.Email row_to_email(OutboxRow row) throws Error { private Geary.Email row_to_email(OutboxRow row) throws Error {
Geary.Email? email = null; Geary.Email? email = null;

View file

@ -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) public override Geary.EmailIdentifier to_email_identifier(GLib.Variant serialised)
throws Geary.EngineError.BAD_PARAMETERS { throws Geary.EngineError.BAD_PARAMETERS {
try { try {