[geary] Undo Archive/Trash/Move: Bug #721828
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary] Undo Archive/Trash/Move: Bug #721828
- Date: Fri, 6 Feb 2015 01:59:06 +0000 (UTC)
commit 354e2edbf8e8fb803feb048d6022073bc22c7d7d
Author: Jim Nelson <jim yorba org>
Date: Thu Feb 5 17:57:27 2015 -0800
Undo Archive/Trash/Move: Bug #721828
For now, the undo stack is 1-deep with no redo facility, which mimics
Gmail's facility. We can consider later offering a more involved
undo stack, but would like to try this out for now and work out the
kinks before becoming more aggressive.
src/CMakeLists.txt | 8 +-
src/client/application/geary-application.vala | 19 ++-
src/client/application/geary-controller.vala | 162 ++++++++++++++++--
src/client/components/main-toolbar.vala | 6 +
src/engine/api/geary-abstract-local-folder.vala | 13 ++-
src/engine/api/geary-engine.vala | 14 +-
src/engine/api/geary-folder-supports-archive.vala | 4 +-
src/engine/api/geary-folder-supports-move.vala | 4 +-
src/engine/api/geary-folder.vala | 8 +
src/engine/api/geary-revokable.vala | 183 ++++++++++++++++++++
src/engine/app/app-conversation-monitor.vala | 7 +-
src/engine/imap-db/imap-db-account.vala | 47 +++++-
.../gmail/imap-engine-gmail-folder.vala | 14 ++-
.../imap-engine/imap-engine-generic-account.vala | 40 +++--
.../imap-engine/imap-engine-minimal-folder.vala | 111 +++++++++++--
.../imap-engine/imap-engine-replay-queue.vala | 21 ++-
.../imap-engine-revokable-committed-move.vala | 64 +++++++
.../imap-engine/imap-engine-revokable-move.vala | 115 ++++++++++++
.../imap-engine-send-replay-operation.vala | 8 +-
...ail.vala => imap-engine-move-email-commit.vala} | 75 ++++-----
.../replay-ops/imap-engine-move-email-prepare.vala | 70 ++++++++
.../replay-ops/imap-engine-move-email-revoke.vala | 62 +++++++
.../replay-ops/imap-engine-user-close.vala | 45 +++++
src/engine/imap/api/imap-account.vala | 10 +-
.../transport/imap-client-session-manager.vala | 18 ++-
25 files changed, 1010 insertions(+), 118 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index bfb58fb..80a715a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -37,6 +37,7 @@ engine/api/geary-logging.vala
engine/api/geary-named-flag.vala
engine/api/geary-named-flags.vala
engine/api/geary-progress-monitor.vala
+engine/api/geary-revokable.vala
engine/api/geary-search-folder.vala
engine/api/geary-search-query.vala
engine/api/geary-service.vala
@@ -191,6 +192,8 @@ engine/imap-engine/imap-engine-generic-folder.vala
engine/imap-engine/imap-engine-minimal-folder.vala
engine/imap-engine/imap-engine-replay-operation.vala
engine/imap-engine/imap-engine-replay-queue.vala
+engine/imap-engine/imap-engine-revokable-move.vala
+engine/imap-engine/imap-engine-revokable-committed-move.vala
engine/imap-engine/imap-engine-send-replay-operation.vala
engine/imap-engine/gmail/imap-engine-gmail-account.vala
engine/imap-engine/gmail/imap-engine-gmail-all-mail-folder.vala
@@ -211,12 +214,15 @@ engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
engine/imap-engine/replay-ops/imap-engine-mark-email.vala
-engine/imap-engine/replay-ops/imap-engine-move-email.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
+engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
engine/imap-engine/replay-ops/imap-engine-remove-email.vala
engine/imap-engine/replay-ops/imap-engine-replay-append.vala
engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
+engine/imap-engine/replay-ops/imap-engine-user-close.vala
engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala
index f533254..5000ae6 100644
--- a/src/client/application/geary-application.vala
+++ b/src/client/application/geary-application.vala
@@ -59,7 +59,6 @@ public class GearyApplication : Gtk.Application {
* an exit, a callback should return true.
*/
public virtual signal bool exiting(bool panicked) {
- controller.close();
Date.terminate();
return true;
@@ -89,9 +88,9 @@ public class GearyApplication : Gtk.Application {
private string bin;
private File exec_dir;
-
private bool exiting_fired = false;
private int exitcode = 0;
+ private bool is_destroyed = false;
public GearyApplication() {
Object(application_id: APP_ID);
@@ -199,6 +198,17 @@ public class GearyApplication : Gtk.Application {
release();
}
+ private async void destroy_async() {
+ // see create_async() for reasoning hold/release is used
+ hold();
+
+ yield controller.close_async();
+
+ release();
+
+ is_destroyed = true;
+ }
+
public bool compose(string mailto) {
if (controller == null)
return false;
@@ -327,6 +337,11 @@ public class GearyApplication : Gtk.Application {
return;
}
+ // Give asynchronous destroy_async() a chance to complete
+ destroy_async.begin();
+ while (!is_destroyed || Gtk.events_pending())
+ Gtk.main_iteration();
+
if (Gtk.main_level() > 0)
Gtk.main_quit();
else
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 7a055f4..a8d1fb7 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -33,6 +33,7 @@ public class GearyController : Geary.BaseObject {
public const string ACTION_EMPTY_MENU = "GearyEmptyMenu";
public const string ACTION_EMPTY_SPAM = "GearyEmptySpam";
public const string ACTION_EMPTY_TRASH = "GearyEmptyTrash";
+ public const string ACTION_UNDO = "GearyUndo";
public const string ACTION_FIND_IN_CONVERSATION = "GearyFindInConversation";
public const string ACTION_FIND_NEXT_IN_CONVERSATION = "GearyFindNextInConversation";
public const string ACTION_FIND_PREVIOUS_IN_CONVERSATION = "GearyFindPreviousInConversation";
@@ -125,6 +126,7 @@ public class GearyController : Geary.BaseObject {
private Gee.List<string> pending_mailtos = new Gee.ArrayList<string>();
private Geary.Nonblocking.Mutex untrusted_host_prompt_mutex = new Geary.Nonblocking.Mutex();
private Gee.HashSet<Geary.Endpoint> validating_endpoints = new Gee.HashSet<Geary.Endpoint>();
+ private Geary.Revokable? revokable = null;
// List of windows we're waiting to close before Geary closes.
private Gee.List<ComposerWidget> waiting_to_close = new Gee.ArrayList<ComposerWidget>();
@@ -238,6 +240,9 @@ public class GearyController : Geary.BaseObject {
// instantiate here to ensure that Config is initialized and ready
autostart_manager = new AutostartManager();
+ // initialize revokable
+ save_revokable(null, null);
+
// Start Geary.
try {
yield Geary.Engine.instance.open_async(GearyApplication.instance.get_user_data_directory(),
@@ -251,13 +256,59 @@ public class GearyController : Geary.BaseObject {
}
/**
- * Stops the controller and shuts down Geary.
+ * At the moment, this is non-reversible, i.e. once closed a GearyController cannot be
+ * re-opened.
*/
- public void close() {
+ public async void close_async() {
+ // hide window while shutting down, as this can take a few seconds under certain conditions
+ main_window.hide();
+
+ // drop the Revokable, which will commit it if necessary
+ save_revokable(null, null);
+
+ // close the ConversationMonitor
+ try {
+ if (current_conversations != null) {
+ yield current_conversations.stop_monitoring_async(null);
+
+ // If not an Inbox, wait for it to close so all pending operations are flushed
+ if (!inboxes.values.contains(current_conversations.folder))
+ yield current_conversations.folder.wait_for_close_async(null);
+ }
+ } catch (Error err) {
+ message("Error closing conversation at shutdown: %s", err.message);
+ } finally {
+ current_conversations = null;
+ }
+
+ // close all Inboxes
+ foreach (Geary.Folder inbox in inboxes.values) {
+ try {
+ // close and wait for all pending operations to be flushed
+ yield inbox.close_async(null);
+ yield inbox.wait_for_close_async(null);
+ } catch (Error err) {
+ message("Error closing Inbox %s at shutdown: %s", inbox.to_string(), err.message);
+ }
+ }
+
+ // close all Accounts
+ foreach (Geary.Account account in email_stores.keys) {
+ try {
+ yield account.close_async(null);
+ } catch (Error err) {
+ message("Error closing account %s at shutdown: %s", account.to_string(), err.message);
+ }
+ }
+
main_window.destroy();
- main_window = null;
- current_account = null;
- account_selected(null);
+
+ // Turn off the lights and lock the door behind you
+ try {
+ yield Geary.Engine.instance.close_async(null);
+ } catch (Error err) {
+ message("Error closing Geary Engine instance: %s", err.message);
+ }
}
private void add_accelerator(string accelerator, string action) {
@@ -404,6 +455,9 @@ public class GearyController : Geary.BaseObject {
empty_trash.label = _("Empty _Trash…");
entries += empty_trash;
+ Gtk.ActionEntry undo = { ACTION_UNDO, "edit-undo-symbolic", null, "<Ctrl>Z", null, on_revoke };
+ entries += undo;
+
Gtk.ActionEntry zoom_in = { ACTION_ZOOM_IN, null, null, "<Ctrl>equal",
null, on_zoom_in };
entries += zoom_in;
@@ -1273,6 +1327,9 @@ public class GearyController : Geary.BaseObject {
Cancellable? conversation_cancellable = (current_is_inbox ?
inbox_cancellables.get(folder.account) : cancellable_folder);
+ // clear Revokable, as Undo is only available while a folder is selected
+ save_revokable(null, null);
+
// stop monitoring for conversations and close the folder
if (current_conversations != null) {
yield current_conversations.stop_monitoring_async(null);
@@ -1863,10 +1920,19 @@ public class GearyController : Geary.BaseObject {
return;
Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
- if (supports_move == null)
- return;
-
- supports_move.move_email_async.begin(ids, destination.path, cancellable_folder);
+ if (supports_move != null)
+ move_conversation_async.begin(supports_move, ids, destination.path, cancellable_folder);
+ }
+
+ private async void move_conversation_async(Geary.FolderSupport.Move source_folder,
+ Gee.List<Geary.EmailIdentifier> ids, Geary.FolderPath destination, Cancellable? cancellable) {
+ try {
+ save_revokable(yield source_folder.move_email_async(ids, destination, cancellable),
+ _("Undo move (Ctrl+Z)"));
+ } catch (Error err) {
+ debug("%s: Unable to move %d emails: %s", source_folder.to_string(), ids.size,
+ err.message);
+ }
}
private void on_open_attachment(Geary.Attachment attachment) {
@@ -2404,10 +2470,13 @@ public class GearyController : Geary.BaseObject {
debug("Archiving selected messages");
Geary.FolderSupport.Archive? supports_archive = current_folder as Geary.FolderSupport.Archive;
- if (supports_archive == null)
+ if (supports_archive == null) {
debug("Folder %s doesn't support archive", current_folder.to_string());
- else
- yield supports_archive.archive_email_async(ids, cancellable);
+ } else {
+ save_revokable(yield supports_archive.archive_email_async(ids, cancellable),
+ _("Undo archive (Ctrl+Z)"));
+ }
+
return;
}
@@ -2419,7 +2488,9 @@ public class GearyController : Geary.BaseObject {
Geary.SpecialFolderType.TRASH, cancellable)).path;
Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
if (supports_move != null) {
- yield supports_move.move_email_async(ids, trash_path, cancellable);
+ save_revokable(yield supports_move.move_email_async(ids, trash_path, cancellable),
+ _("Undo trash (Ctrl+Z)"));
+
return;
}
}
@@ -2450,6 +2521,71 @@ public class GearyController : Geary.BaseObject {
}
}
+ private void save_revokable(Geary.Revokable? new_revokable, string? description) {
+ // disconnect old revokable & blindly commit it
+ if (revokable != null) {
+ revokable.notify[Geary.Revokable.PROP_VALID].disconnect(on_revokable_valid_changed);
+ revokable.notify[Geary.Revokable.PROP_IN_PROCESS].disconnect(update_revokable_action);
+ revokable.committed.disconnect(on_revokable_committed);
+
+ revokable.commit_async.begin();
+ }
+
+ // store new revokable
+ revokable = new_revokable;
+
+ // connect to new revokable
+ if (revokable != null) {
+ revokable.notify[Geary.Revokable.PROP_VALID].connect(on_revokable_valid_changed);
+ revokable.notify[Geary.Revokable.PROP_IN_PROCESS].connect(update_revokable_action);
+ revokable.committed.connect(on_revokable_committed);
+ }
+
+ Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+ undo_action.tooltip = (revokable != null && description != null) ? description : _("Undo (Ctrl+Z)");
+
+ update_revokable_action();
+ }
+
+ private void update_revokable_action() {
+ Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+ undo_action.sensitive = revokable != null && revokable.valid && !revokable.in_process;
+ }
+
+ private void on_revokable_valid_changed() {
+ // remove revokable if it goes invalid
+ if (revokable != null && !revokable.valid)
+ save_revokable(null, null);
+ }
+
+ private void on_revokable_committed(Geary.Revokable? committed_revokable) {
+ if (committed_revokable == null)
+ return;
+
+ // use existing description
+ Gtk.Action undo_action = GearyApplication.instance.get_action(ACTION_UNDO);
+ save_revokable(committed_revokable, undo_action.tooltip);
+ }
+
+ private void on_revoke() {
+ if (revokable != null && revokable.valid)
+ revokable.revoke_async.begin(null, on_revoke_completed);
+ }
+
+ private void on_revoke_completed(Object? object, AsyncResult result) {
+ // Don't use the "revokable" instance because it might have gone null before this callback
+ // was reached
+ Geary.Revokable? origin = object as Geary.Revokable;
+ if (origin == null)
+ return;
+
+ try {
+ origin.revoke_async.end(result);
+ } catch (Error err) {
+ debug("Unable to revoke operation: %s", err.message);
+ }
+ }
+
private void on_zoom_in() {
main_window.conversation_viewer.web_view.zoom_in();
}
diff --git a/src/client/components/main-toolbar.vala b/src/client/components/main-toolbar.vala
index 79b3555..2250d3b 100644
--- a/src/client/components/main-toolbar.vala
+++ b/src/client/components/main-toolbar.vala
@@ -72,6 +72,10 @@ public class MainToolbar : PillHeaderbar {
insert.add(trash_delete_button = create_toolbar_button(null, GearyController.ACTION_TRASH_MESSAGE,
false));
Gtk.Box archive_trash_delete = create_pill_buttons(insert);
+ insert.clear();
+ insert.add(create_toolbar_button(null, GearyController.ACTION_UNDO, false));
+ Gtk.Box undo = create_pill_buttons(insert);
+
// Search bar.
search_entry.width_chars = 28;
search_entry.tooltip_text = _("Search all mail in account for keywords (Ctrl+S)");
@@ -88,6 +92,7 @@ public class MainToolbar : PillHeaderbar {
// pack_end() ordering is reversed in GtkHeaderBar in 3.12 and above
#if !GTK_3_12
add_end(archive_trash_delete);
+ add_end(undo);
add_end(search_upgrade_progress_bar);
add_end(search_entry);
#endif
@@ -103,6 +108,7 @@ public class MainToolbar : PillHeaderbar {
#if GTK_3_12
add_end(search_entry);
add_end(search_upgrade_progress_bar);
+ add_end(undo);
add_end(archive_trash_delete);
#endif
diff --git a/src/engine/api/geary-abstract-local-folder.vala b/src/engine/api/geary-abstract-local-folder.vala
index f78dcab..3b32a92 100644
--- a/src/engine/api/geary-abstract-local-folder.vala
+++ b/src/engine/api/geary-abstract-local-folder.vala
@@ -8,11 +8,12 @@
* Handles open/close for local folders.
*/
public abstract class Geary.AbstractLocalFolder : Geary.Folder {
- private int open_count = 0;
-
private ProgressMonitor _opening_monitor = new
Geary.ReentrantProgressMonitor(Geary.ProgressType.ACTIVITY);
public override Geary.ProgressMonitor opening_monitor { get { return _opening_monitor; } }
+ private int open_count = 0;
+ private Nonblocking.Semaphore closed_semaphore = new Nonblocking.Semaphore();
+
protected AbstractLocalFolder() {
}
@@ -39,6 +40,8 @@ public abstract class Geary.AbstractLocalFolder : Geary.Folder {
if (open_count++ > 0)
return false;
+ closed_semaphore.reset();
+
notify_opened(Geary.Folder.OpenState.LOCAL, properties.email_total);
return true;
@@ -48,8 +51,14 @@ public abstract class Geary.AbstractLocalFolder : Geary.Folder {
if (open_count == 0 || --open_count > 0)
return;
+ closed_semaphore.blind_notify();
+
notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE);
notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED);
}
+
+ public override async void wait_for_close_async(Cancellable? cancellable = null) throws Error {
+ yield closed_semaphore.wait_async(cancellable);
+ }
}
diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala
index ae807e9..3d2c8b8 100644
--- a/src/engine/api/geary-engine.vala
+++ b/src/engine/api/geary-engine.vala
@@ -136,8 +136,7 @@ public class Geary.Engine : BaseObject {
* when necessary.
*/
public async void open_async(File user_data_dir, File resource_dir,
- Geary.CredentialsMediator? authentication_mediator,
- Cancellable? cancellable = null) throws Error {
+ Geary.CredentialsMediator? authentication_mediator, Cancellable? cancellable = null) throws Error {
// initialize *before* opening the Engine ... all initialize code should assume the Engine
// is closed
initialize_library();
@@ -202,16 +201,19 @@ public class Geary.Engine : BaseObject {
public async void close_async(Cancellable? cancellable = null) throws Error {
if (!is_open)
return;
-
- foreach(AccountInformation account in accounts.values)
+
+ Gee.Collection<AccountInformation> unavailable_accounts = accounts.values;
+ accounts.clear();
+
+ foreach(AccountInformation account in unavailable_accounts)
account_unavailable(account);
-
+
user_data_dir = null;
resource_dir = null;
authentication_mediator = null;
accounts = null;
account_instances = null;
-
+
is_open = false;
closed();
}
diff --git a/src/engine/api/geary-folder-supports-archive.vala
b/src/engine/api/geary-folder-supports-archive.vala
index 02bcde8..a57d8d1 100644
--- a/src/engine/api/geary-folder-supports-archive.vala
+++ b/src/engine/api/geary-folder-supports-archive.vala
@@ -19,8 +19,10 @@ public interface Geary.FolderSupport.Archive : Geary.Folder {
* Archives the specified emails from the folder.
*
* The { link Geary.Folder} must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public abstract async void archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ public abstract async Geary.Revokable? archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error;
}
diff --git a/src/engine/api/geary-folder-supports-move.vala b/src/engine/api/geary-folder-supports-move.vala
index e4f45c3..2655c7d 100644
--- a/src/engine/api/geary-folder-supports-move.vala
+++ b/src/engine/api/geary-folder-supports-move.vala
@@ -20,8 +20,10 @@ public interface Geary.FolderSupport.Move : Geary.Folder {
* way but will return success.
*
* The { link Geary.Folder} must be opened prior to attempting this operation.
+ *
+ * @returns A { link Geary.Revokable} that may be used to revoke (undo) this operation later.
*/
- public abstract async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+ public abstract async Geary.Revokable? move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
}
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index 8f76fb3..cba29de 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -457,6 +457,14 @@ public abstract class Geary.Folder : BaseObject {
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
/**
+ * Wait for the Folder to fully close.
+ *
+ * Unlike { link wait_for_open_async}, this will ''always'' block until a { link Folder} is
+ * closed, even if it's not open.
+ */
+ public abstract async void wait_for_close_async(Cancellable? cancellable = null) throws Error;
+
+ /**
* Find the lowest- and highest-ordered { link EmailIdentifier}s in the
* folder, among the given set of EmailIdentifiers that may or may not be
* in the folder. If none of the given set are in the folder, return null.
diff --git a/src/engine/api/geary-revokable.vala b/src/engine/api/geary-revokable.vala
new file mode 100644
index 0000000..ccee2db
--- /dev/null
+++ b/src/engine/api/geary-revokable.vala
@@ -0,0 +1,183 @@
+/* Copyright 2014 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.
+ */
+
+/**
+ * A representation of an operation with the Geary Engine that may be revoked (undone) at a later
+ * time.
+ *
+ * The Revokable will do everything it can to commit the operation (if necessary) when its final
+ * ref is dropped. However, since the final ref can be dropped at an indeterminate time, it's
+ * advised that callers force the matter by scheduling it with { link commit_async}.
+ */
+
+public abstract class Geary.Revokable : BaseObject {
+ public const string PROP_VALID = "valid";
+ public const string PROP_IN_PROCESS = "in-process";
+
+ /**
+ * Indicates if { link revoke_async} or { link commit_async} are valid operations for this
+ * { link Revokable}.
+ *
+ * Due to later operations or notifications, it's possible for the Revokable to go invalid
+ * after being issued to the caller.
+ */
+ public bool valid { get; protected set; default = true; }
+
+ /**
+ * Indicates a { link revoke_async} or { link commit_async} operation is underway.
+ *
+ * Only one operation can occur at a time, and when complete the { link Revokable} will be
+ * invalid.
+ *
+ * @see valid
+ */
+ public bool in_process { get; protected set; default = false; }
+
+ private uint commit_timeout_id = 0;
+
+ /**
+ * Fired when the { link Revokable} has been revoked.
+ *
+ * { link valid} will stil be true when this is fired.
+ */
+ public signal void revoked();
+
+ /**
+ * Fired when the { link Revokable} has been committed.
+ *
+ * Some Revokables will offer a new Revokable to allow revoking the committed state.
+ *
+ * { link valid} will stil be true when this is fired.
+ */
+ public signal void committed(Geary.Revokable? commit_revokable);
+
+ /**
+ * Create a { link Revokable} with optional parameters.
+ *
+ * If commit_timeout_sec is nonzero, Revokable will automatically call { link commit_async}
+ * after the timeout expires if it is still { link valid}.
+ */
+ protected Revokable(int commit_timeout_sec = 0) {
+ if (commit_timeout_sec == 0)
+ return;
+
+ // This holds a reference to the Revokable, meaning cancelling the timeout in the dtor is
+ // largely symbolic, but so be it
+ commit_timeout_id = Timeout.add_seconds(commit_timeout_sec, on_timed_commit);
+
+ // various events that cancel the need for a timed commit; this is important to drop the
+ // ref to this object within the event loop
+ revoked.connect(cancel_timed_commit);
+ committed.connect(cancel_timed_commit);
+ notify[PROP_VALID].connect(cancel_timed_commit);
+ }
+
+ ~Revokable() {
+ cancel_timed_commit();
+ }
+
+ protected virtual void notify_revoked() {
+ revoked();
+ }
+
+ protected virtual void notify_committed(Geary.Revokable? commit_revokable) {
+ committed(commit_revokable);
+ }
+
+ /**
+ * Revoke (undo) the operation.
+ *
+ * If the call throws an Error that does not necessarily mean the { link Revokable} is
+ * invalid. Check { link valid}.
+ *
+ * @throws EngineError.ALREADY_OPEN if { link in_process} is true. EngineError.ALREADY_CLOSED
+ * if { link valid} is false.
+ */
+ public virtual async void revoke_async(Cancellable? cancellable = null) throws Error {
+ if (in_process)
+ throw new EngineError.ALREADY_OPEN("Already revoking or committing operation");
+
+ if (!valid)
+ throw new EngineError.ALREADY_CLOSED("Revokable not valid");
+
+ in_process = true;
+ try {
+ yield internal_revoke_async(cancellable);
+ } finally {
+ in_process = false;
+ }
+ }
+
+ /**
+ * The child class's implementation of { link revoke_async}.
+ *
+ * The default implementation of { link revoke_async} deals with state issues
+ * ({ link in_process}, throwing the appropriate Error, etc.) Child classes can override this
+ * method and only worry about the revoke operation itself.
+ *
+ * This call *must* set { link valid} before exiting. It must also call { link notify_revoked}
+ * if successful.
+ */
+ protected abstract async void internal_revoke_async(Cancellable? cancellable) throws Error;
+
+ /**
+ * Commits (completes) the operation immediately.
+ *
+ * Some { link Revokable} operations work by delaying the operation until time has passed or
+ * some situation occurs which requires the operation to complete. This call forces the
+ * operation to complete immediately rather than delay it for later.
+ *
+ * Even if the operation "actually" commits and is not delayed, calling commit_async() will
+ * make this Revokable invalid.
+ *
+ * @throws EngineError.ALREADY_OPEN if { link in_process} is true. EngineError.ALREADY_CLOSED
+ * if { link valid} is false.
+ */
+ public virtual async void commit_async(Cancellable? cancellable = null) throws Error {
+ if (in_process)
+ throw new EngineError.ALREADY_OPEN("Already revoking or committing operation");
+
+ if (!valid)
+ throw new EngineError.ALREADY_CLOSED("Revokable not valid");
+
+ in_process = true;
+ try {
+ yield internal_commit_async(cancellable);
+ } finally {
+ in_process = false;
+ }
+ }
+
+ /**
+ * The child class's implementation of { link commit_async}.
+ *
+ * The default implementation of { link commit_async} deals with state issues
+ * ({ link in_process}, throwing the appropriate Error, etc.) Child classes can override this
+ * method and only worry about the revoke operation itself.
+ *
+ * This call *must* set { link valid} before exiting. It must also call { link notify_committed}
+ * if successful.
+ */
+ protected abstract async void internal_commit_async(Cancellable? cancellable) throws Error;
+
+ private bool on_timed_commit() {
+ commit_timeout_id = 0;
+
+ if (valid && !in_process)
+ commit_async.begin();
+
+ return false;
+ }
+
+ private void cancel_timed_commit() {
+ if (commit_timeout_id == 0)
+ return;
+
+ Source.remove(commit_timeout_id);
+ commit_timeout_id = 0;
+ }
+}
+
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index db4373a..48943c1 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -631,7 +631,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
}
internal async void remove_emails_async(Gee.Collection<Geary.EmailIdentifier> removed_ids) {
- debug("%d messages(s) removed to %s, trimming/removing conversations...", removed_ids.size,
+ debug("%d messages(s) removed from %s, trimming/removing conversations...", removed_ids.size,
folder.to_string());
Gee.Collection<Geary.App.Conversation> removed;
@@ -732,7 +732,10 @@ public class Geary.App.ConversationMonitor : BaseObject {
* Attempts to load enough conversations to fill min_window_count.
*/
internal async void fill_window_async(bool is_insert) {
- if (!is_monitoring || min_window_count <= conversations.size)
+ if (!is_monitoring)
+ return;
+
+ if (!is_insert && min_window_count <= conversations.size)
return;
int initial_message_count = conversations.get_email_count();
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index b09685e..06cec3a 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -250,12 +250,17 @@ private class Geary.ImapDB.Account : BaseObject {
* update_uid_info is true.
*/
public async void update_folder_status_async(Geary.Imap.Folder imap_folder, bool update_uid_info,
- Cancellable? cancellable) throws Error {
+ bool respect_marked_for_remove, Cancellable? cancellable) throws Error {
check_open();
Geary.Imap.FolderProperties properties = imap_folder.properties;
Geary.FolderPath path = imap_folder.path;
+ // adjust for marked remove, but don't write these adjustments to the database -- they're
+ // only reflected in memory via the properties
+ int adjust_unseen = 0;
+ int adjust_total = 0;
+
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
int64 parent_id;
if (!do_fetch_parent_id(cx, path, true, out parent_id, cancellable)) {
@@ -264,6 +269,36 @@ private class Geary.ImapDB.Account : BaseObject {
return Db.TransactionOutcome.ROLLBACK;
}
+ int64 folder_id;
+ if (!do_fetch_folder_id(cx, path, false, out folder_id, cancellable))
+ folder_id = Db.INVALID_ROWID;
+
+ if (respect_marked_for_remove && folder_id != Db.INVALID_ROWID) {
+ Db.Statement stmt = cx.prepare("""
+ SELECT flags
+ FROM MessageTable
+ WHERE id IN (
+ SELECT message_id
+ FROM MessageLocationTable
+ WHERE folder_id = ? AND remove_marker = ?
+ )
+ """);
+ stmt.bind_rowid(0, folder_id);
+ stmt.bind_bool(1, true);
+
+ Db.Result results = stmt.exec(cancellable);
+ while (!results.finished) {
+ adjust_total++;
+
+ Imap.EmailFlags flags = new Imap.EmailFlags(Imap.MessageFlags.deserialize(
+ results.string_at(0)));
+ if (flags.contains(EmailFlags.UNREAD))
+ adjust_unseen++;
+
+ results.next(cancellable);
+ }
+ }
+
Db.Statement stmt;
if (parent_id != Db.INVALID_ROWID) {
stmt = cx.prepare(
@@ -298,7 +333,7 @@ private class Geary.ImapDB.Account : BaseObject {
if (db_folder != null) {
Imap.FolderProperties local_properties = db_folder.get_properties();
- local_properties.set_status_unseen(properties.unseen);
+ local_properties.set_status_unseen(Numeric.int_floor(properties.unseen - adjust_unseen, 0));
local_properties.recent = properties.recent;
local_properties.attrs = properties.attrs;
@@ -307,8 +342,12 @@ private class Geary.ImapDB.Account : BaseObject {
local_properties.uid_next = properties.uid_next;
}
- if (properties.status_messages >= 0)
- local_properties.set_status_message_count(properties.status_messages, false);
+ // only update STATUS MESSAGES count if previously set, but use this count as the
+ // "authoritative" value until another SELECT/EXAMINE or MESSAGES response
+ if (properties.status_messages >= 0) {
+ local_properties.set_status_message_count(
+ Numeric.int_floor(properties.status_messages - adjust_total, 0), true);
+ }
}
}
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
index 16e5529..567c9a0 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -17,9 +17,21 @@ private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archiv
return yield base.create_email_async(rfc822, flags, date_received, id, cancellable);
}
- public async void archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ public async Geary.Revokable? archive_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error {
+ // Use move_email_async("All Mail") here; Gmail will do the right thing and report
+ // it was copied with the pre-existing All Mail UID (in other words, no actual copy is
+ // performed). This allows for undoing an archive with the same code path as a move.
+ Geary.Folder? all_mail = account.get_special_folder(Geary.SpecialFolderType.ALL_MAIL);
+ if (all_mail != null)
+ return yield move_email_async(email_ids, all_mail.path, cancellable);
+
+ // although this shouldn't happen, fall back on our traditional archive, which is simply
+ // to remove the message from this label
+ message("%s: Unable to perform revokable archive: All Mail not found", to_string());
yield expunge_email_async(email_ids, cancellable);
+
+ return null;
}
public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 9c4361c..8d04ccf 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -319,6 +319,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
if (in_refresh_unseen.contains(folder))
return true;
+ // add here, remove in completed callback
+ in_refresh_unseen.add(folder);
+
refresh_unseen_async.begin(folder, null, on_refresh_unseen_completed);
refresh_unseen_timeout_ids.unset(folder.path);
@@ -334,21 +337,29 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
}
private async void refresh_unseen_async(Geary.Folder folder, Cancellable? cancellable) throws Error {
- in_refresh_unseen.add(folder);
-
debug("Refreshing unseen counts for %s", folder.to_string());
- bool folder_created;
- Imap.Folder remote_folder = yield remote.fetch_folder_async(folder.path,
- out folder_created, null, cancellable);
-
- if (!folder_created) {
- int unseen_count = yield remote.fetch_unseen_count_async(folder.path, cancellable);
- remote_folder.properties.set_status_unseen(unseen_count);
- yield local.update_folder_status_async(remote_folder, false, cancellable);
+ try {
+ bool folder_created;
+ Imap.Folder remote_folder = yield remote.fetch_folder_async(folder.path,
+ out folder_created, null, cancellable);
+
+ // if created, don't need to fetch count because it was fetched when it was created
+ int unseen, total;
+ if (!folder_created) {
+ yield remote.fetch_counts_async(folder.path, out unseen, out total, cancellable);
+ remote_folder.properties.set_status_unseen(unseen);
+ remote_folder.properties.set_status_message_count(total, false);
+ } else {
+ unseen = remote_folder.properties.unseen;
+ total = remote_folder.properties.email_total;
+ }
+
+ yield local.update_folder_status_async(remote_folder, false, true, cancellable);
+ } finally {
+ // added when call scheduled (above)
+ in_refresh_unseen.remove(folder);
}
-
- in_refresh_unseen.remove(folder);
}
private void reschedule_folder_refresh(bool immediate) {
@@ -535,6 +546,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
* not be reflected in the local database unless there's a separate connection to the server
* that is notified or detects these changes.
*
+ * The returned Folder must be opened prior to use and closed once completed. ''Leaving a
+ * Folder open will cause a connection leak.''
+ *
* It is not recommended this object be held open long-term, or that its status or notifications
* be directly written to the database unless you know exactly what you're doing. ''Caveat
* implementor.''
@@ -704,7 +718,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
// always update, openable or not; have the folder update the UID info the next time
// it's opened
try {
- yield local.update_folder_status_async(remote_folder, false, cancellable);
+ yield local.update_folder_status_async(remote_folder, false, false, cancellable);
} catch (Error update_error) {
debug("Unable to update local folder %s with remote properties: %s",
remote_folder.to_string(), update_error.message);
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 4d784f2..c117fc8 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -45,6 +45,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
private bool remote_opened = false;
private Nonblocking.ReportingSemaphore<bool> remote_semaphore =
new Nonblocking.ReportingSemaphore<bool>(false);
+ private Nonblocking.Semaphore closed_semaphore = new Nonblocking.Semaphore();
private ReplayQueue replay_queue;
private int remote_count = -1;
private uint open_remote_timer_id = 0;
@@ -52,6 +53,28 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
private Nonblocking.Mutex open_mutex = new Nonblocking.Mutex();
private Nonblocking.Mutex close_mutex = new Nonblocking.Mutex();
+ /**
+ * Called when the folder is closing (and not reestablishing a connection) and will be flushing
+ * the replay queue. Subscribers may add ReplayOperations to the list, which will be enqueued
+ * before the queue is flushed.
+ *
+ * Note that this is ''not'' fired if the queue is not being flushed.
+ */
+ public signal void closing(Gee.List<ReplayOperation> final_ops);
+
+ /**
+ * Fired when an { link EmailIdentifier} that was marked for removal is actually reported as
+ * removed (expunged) from the server.
+ *
+ * Marked messages are reported as removed when marked in the database, to make the operation
+ * appear speedy to the caller. When the message is finally acknowledged as removed by the
+ * server, "email-removed" is not fired to avoid double-reporting.
+ *
+ * Some internal code (especially Revokables) mark messages for removal but delay the network
+ * operation. They need to know if the message is removed by another client, however.
+ */
+ public signal void marked_email_removed(Gee.Collection<Geary.EmailIdentifier> removed);
+
public MinimalFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
_account = account;
@@ -79,6 +102,10 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
local_folder.email_complete.disconnect(on_email_complete);
}
+ protected virtual void notify_closing(Gee.List<ReplayOperation> final_ops) {
+ closing(final_ops);
+ }
+
/*
* These signal notifiers are marked public (note this is a private class) so the various
* ReplayOperations can directly fire the associated signals while within the queue.
@@ -524,6 +551,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
// reset to force waiting in wait_for_open_async()
remote_semaphore.reset();
+ // reset to force waiting in wait_for_close_async()
+ closed_semaphore.reset();
+
// Unless NO_DELAY is set, do NOT open the remote side here; wait for the ReplayQueue to
// require a remote connection or wait_for_open_async() to be called ... this allows for
// fast local-only operations to occur, local-only either because (a) the folder has all
@@ -745,6 +775,22 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
+ // Check open_count but only decrement inside of replay queue
+ if (open_count <= 0)
+ return;
+
+ UserClose user_close = new UserClose(this, cancellable);
+ replay_queue.schedule(user_close);
+
+ yield user_close.wait_for_ready_async(cancellable);
+ }
+
+ public override async void wait_for_close_async(Cancellable? cancellable = null) throws Error {
+ yield closed_semaphore.wait_async(cancellable);
+ }
+
+ internal async void user_close_async(Cancellable? cancellable) {
+ // decrement open_count and, if zero, continue closing Folder
if (open_count == 0 || --open_count > 0)
return;
@@ -754,11 +800,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
// block anyone from wait_until_open_async(), as this is no longer open
remote_semaphore.reset();
- ReplayDisconnect disconnect_op = new ReplayDisconnect(this,
- Imap.ClientSession.DisconnectReason.REMOTE_CLOSE, true, cancellable);
- replay_queue.schedule(disconnect_op);
-
- yield disconnect_op.wait_for_ready_async(cancellable);
+ // don't yield here, close_internal_async() needs to be called outside of the replay queue
+ // the open_count protects against this path scheduling it more than once
+ close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, true, cancellable);
}
// Close the remote connection and, if open_count is zero, the Folder itself. A Mutex is used
@@ -814,6 +858,16 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
// That said, only flush, close, and destroy the ReplayQueue if fully closing and not
// preparing for a connection reestablishment
if (open_count <= 0) {
+ // if closing and flushing the queue, give Revokables a chance to schedule their
+ // commit operations before going down
+ if (flush_pending) {
+ Gee.List<ReplayOperation> final_ops = new Gee.ArrayList<ReplayOperation>();
+ notify_closing(final_ops);
+
+ foreach (ReplayOperation op in final_ops)
+ replay_queue.schedule(op);
+ }
+
// Close the replay queues; if a "clean" close, flush pending operations so everything
// gets a chance to run; if forced close, drop everything outstanding
try {
@@ -878,6 +932,10 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
notify_closed(CloseReason.FOLDER_CLOSED);
+ // If not closing in the background, do it here
+ if (closing_remote_folder == null)
+ closed_semaphore.blind_notify();
+
debug("Folder %s closed", to_string());
}
@@ -926,6 +984,10 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
if (folder.open_count <= 0) {
debug("Not reestablishing connection to %s: closed", folder.to_string());
+ // need to do it here if not done in close_internal_locked_async()
+ if (remote_folder != null)
+ folder.closed_semaphore.blind_notify();
+
return;
}
@@ -1190,9 +1252,15 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
err.message);
}
- // notify of change
- if (!marked && owned_id != null)
- notify_email_removed(Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list());
+ // notify of change ... use "marked-email-removed" for marked email to allow internal code
+ // to be notified when a removed email is "really" removed
+ if (owned_id != null) {
+ Gee.List<EmailIdentifier> removed =
Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list();
+ if (!marked)
+ notify_email_removed(removed);
+ else
+ marked_email_removed(removed);
+ }
if (!marked)
notify_email_count_changed(reported_remote_count, CountChangeReason.REMOVED);
@@ -1372,19 +1440,36 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
return copy.destination_uids.size > 0 ? copy.destination_uids : null;
}
- public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+ public virtual async Geary.Revokable? move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
check_open("move_email_async");
check_ids("move_email_async", to_move);
// watch for moving to this folder, which is treated as a no-op
if (destination.equal_to(path))
- return;
+ return null;
+
+ MoveEmailPrepare prepare = new MoveEmailPrepare(this, (Gee.List<ImapDB.EmailIdentifier>) to_move,
+ cancellable);
+ replay_queue.schedule(prepare);
- MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
- replay_queue.schedule(move);
+ yield prepare.wait_for_ready_async(cancellable);
- yield move.wait_for_ready_async(cancellable);
+ if (prepare.prepared_for_move == null || prepare.prepared_for_move.size == 0)
+ return null;
+
+ return new RevokableMove(_account, this, destination, prepare.prepared_for_move);
+ }
+
+ public void schedule_op(ReplayOperation op) throws Error {
+ check_open("schedule_op");
+
+ replay_queue.schedule(op);
+ }
+
+ public async void exec_op_async(ReplayOperation op, Cancellable? cancellable) throws Error {
+ schedule_op(op);
+ yield op.wait_for_ready_async(cancellable);
}
private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index af43bad..a2e3b2b 100644
--- a/src/engine/imap-engine/imap-engine-replay-queue.vala
+++ b/src/engine/imap-engine/imap-engine-replay-queue.vala
@@ -9,6 +9,12 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
// see as high as 250ms
private const int NOTIFICATION_QUEUE_WAIT_MSEC = 1000;
+ private enum State {
+ OPEN,
+ CLOSING,
+ CLOSED
+ }
+
private class CloseReplayQueue : ReplayOperation {
public CloseReplayQueue() {
// LOCAL_AND_REMOTE to make sure this operation is flushed all the way down the pipe
@@ -57,8 +63,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
private Gee.ArrayList<ReplayOperation> notification_queue = new Gee.ArrayList<ReplayOperation>();
private Scheduler.Scheduled? notification_timer = null;
private int64 next_submission_number = 0;
-
- private bool is_closed = false;
+ private State state = State.OPEN;
public virtual signal void scheduled(ReplayOperation op) {
Logging.debug(Logging.Flag.REPLAY, "[%s] ReplayQueue::scheduled: %s %s", to_string(),
@@ -143,7 +148,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
*/
public bool schedule(ReplayOperation op) {
// ReplayClose is allowed past the velvet ropes even as the hoi palloi is turned away
- if (is_closed && !(op is CloseReplayQueue)) {
+ if (state != State.OPEN && !(op is CloseReplayQueue)) {
debug("Unable to schedule replay operation %s on %s: replay queue closed", op.to_string(),
to_string());
@@ -188,7 +193,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
* Returns false if the operation was not schedule (queue already closed).
*/
public bool schedule_server_notification(ReplayOperation op) {
- if (is_closed) {
+ if (state != State.OPEN) {
debug("Unable to schedule notification operation %s on %s: replay queue closed", op.to_string(),
to_string());
@@ -293,7 +298,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
* A ReplayQueue cannot be re-opened.
*/
public async void close_async(bool flush_pending, Cancellable? cancellable = null) throws Error {
- if (is_closed)
+ if (state != State.OPEN)
return;
// cancel notification queue timeout
@@ -306,8 +311,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
// mark as closed now to prevent further scheduling ... ReplayClose gets special
// consideration in schedule()
- is_closed = true;
-
+ state = State.CLOSING;
closing();
// if not flushing pending, clear out all waiting operations, backing out any that need to
@@ -322,6 +326,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
yield close_op.wait_for_ready_async(cancellable);
+ state = State.CLOSED;
closed();
}
@@ -472,7 +477,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
// wait until the remote folder is opened (or throws an exception, in which case closed)
try {
- if (!is_close_op && folder_opened)
+ if (!is_close_op && folder_opened && state == State.OPEN)
yield owner.wait_for_open_async();
} catch (Error remote_err) {
debug("Folder %s closed or failed to open, remote replay queue closing: %s",
diff --git a/src/engine/imap-engine/imap-engine-revokable-committed-move.vala
b/src/engine/imap-engine/imap-engine-revokable-committed-move.vala
new file mode 100644
index 0000000..864672f
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-revokable-committed-move.vala
@@ -0,0 +1,64 @@
+/* Copyright 2014 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.
+ */
+
+/**
+ * A { link Geary.Revokable} for moving email back to its source after committed with
+ * { link RevokableMove}.
+ */
+
+private class Geary.ImapEngine.RevokableCommittedMove : Revokable {
+ private GenericAccount account;
+ private FolderPath source;
+ private FolderPath destination;
+ private Gee.Set<Imap.UID> destination_uids;
+
+ public RevokableCommittedMove(GenericAccount account, FolderPath source, FolderPath destination,
+ Gee.Set<Imap.UID> destination_uids) {
+ this.account = account;
+ this.source = source;
+ this.destination = destination;
+ this.destination_uids = destination_uids;
+ }
+
+ protected override async void internal_revoke_async(Cancellable? cancellable) throws Error {
+ Imap.Folder? detached_destination = null;
+ try {
+ // use a detached folder to quickly open, issue command, and leave, without full
+ // normalization that MinimalFolder requires
+ detached_destination = yield account.fetch_detached_folder_async(destination, cancellable);
+
+ yield detached_destination.open_async(cancellable);
+
+ foreach (Imap.MessageSet msg_set in Imap.MessageSet.uid_sparse(destination_uids)) {
+ // don't use Cancellable to try to make operations atomic
+ yield detached_destination.copy_email_async(msg_set, source, null);
+ yield detached_destination.remove_email_async(msg_set.to_list(), null);
+
+ if (cancellable != null && cancellable.is_cancelled())
+ throw new IOError.CANCELLED("Revoke cancelled");
+ }
+
+ notify_revoked();
+ } finally {
+ if (detached_destination != null) {
+ try {
+ yield detached_destination.close_async(cancellable);
+ } catch (Error err) {
+ // ignored
+ }
+ }
+
+ valid = false;
+ }
+ }
+
+ protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
+ // pretty simple: already committed, so done
+ notify_committed(null);
+ valid = false;
+ }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-revokable-move.vala
b/src/engine/imap-engine/imap-engine-revokable-move.vala
new file mode 100644
index 0000000..cb7f9f8
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-revokable-move.vala
@@ -0,0 +1,115 @@
+/* Copyright 2014 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.
+ */
+
+/**
+ * A @{link Geary.Revokable} for { link MinimalFolder} move operations.
+ *
+ * This will delay executing the move until (a) the source Folder is closed or (b) a timeout passes.
+ * Even then, it will fire its "committed" signal with a { link RevokableCommittedMove} to allow
+ * the user to undo the operation, albeit taking more time to connect, open the destination folder,
+ * and move the mail back.
+ */
+
+private class Geary.ImapEngine.RevokableMove : Revokable {
+ private const int COMMIT_TIMEOUT_SEC = 60;
+
+ private GenericAccount account;
+ private ImapEngine.MinimalFolder source;
+ private FolderPath destination;
+ private Gee.Set<ImapDB.EmailIdentifier> move_ids;
+
+ public RevokableMove(GenericAccount account, ImapEngine.MinimalFolder source, FolderPath destination,
+ Gee.Set<ImapDB.EmailIdentifier> move_ids) {
+ base (COMMIT_TIMEOUT_SEC);
+
+ this.account = account;
+ this.source = source;
+ this.destination = destination;
+ this.move_ids = move_ids;
+
+ account.folders_available_unavailable.connect(on_folders_available_unavailable);
+ source.email_removed.connect(on_source_email_removed);
+ source.marked_email_removed.connect(on_source_email_removed);
+ source.closing.connect(on_source_closing);
+ }
+
+ ~RevokableMove() {
+ account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
+ source.email_removed.disconnect(on_source_email_removed);
+ source.marked_email_removed.disconnect(on_source_email_removed);
+ source.closing.disconnect(on_source_closing);
+
+ // if still valid, schedule operation so its executed
+ if (valid && source.get_open_state() != Folder.OpenState.CLOSED) {
+ debug("Freeing revokable, scheduling move %d emails from %s to %s", move_ids.size,
+ source.path.to_string(), destination.to_string());
+
+ try {
+ source.schedule_op(new MoveEmailCommit(source, move_ids, destination, null));
+ } catch (Error err) {
+ debug("Move from %s to %s failed: %s", source.path.to_string(), destination.to_string(),
+ err.message);
+ }
+ }
+ }
+
+ protected override async void internal_revoke_async(Cancellable? cancellable) throws Error {
+ try {
+ yield source.exec_op_async(new MoveEmailRevoke(source, move_ids, cancellable),
+ cancellable);
+
+ // valid must still be true before firing
+ notify_revoked();
+ } finally {
+ valid = false;
+ }
+ }
+
+ protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
+ try {
+ MoveEmailCommit op = new MoveEmailCommit(source, move_ids, destination, cancellable);
+ yield source.exec_op_async(op, cancellable);
+
+ // valid must still be true before firing
+ notify_committed(new RevokableCommittedMove(account, source.path, destination,
op.destination_uids));
+ } finally {
+ valid = false;
+ }
+ }
+
+ private void on_folders_available_unavailable(Gee.List<Folder>? available, Gee.List<Folder>?
unavailable) {
+ // look for either of the folders going away
+ if (unavailable != null) {
+ foreach (Folder folder in unavailable) {
+ if (folder.path.equal_to(source.path) || folder.path.equal_to(destination)) {
+ valid = false;
+
+ break;
+ }
+ }
+ }
+ }
+
+ private void on_source_email_removed(Gee.Collection<EmailIdentifier> ids) {
+ // one-way switch
+ if (!valid)
+ return;
+
+ foreach (EmailIdentifier id in ids)
+ move_ids.remove((ImapDB.EmailIdentifier) id);
+
+ valid = move_ids.size > 0;
+ }
+
+ private void on_source_closing(Gee.List<ReplayOperation> final_ops) {
+ if (!valid)
+ return;
+
+ final_ops.add(new MoveEmailCommit(source, move_ids, destination, null));
+ valid = false;
+ }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-send-replay-operation.vala
b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
index 5ae0dce..50449ce 100644
--- a/src/engine/imap-engine/imap-engine-send-replay-operation.vala
+++ b/src/engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -5,11 +5,15 @@
*/
private abstract class Geary.ImapEngine.SendReplayOperation : Geary.ImapEngine.ReplayOperation {
- public SendReplayOperation(string name, ReplayOperation.OnError on_remote_error = OnError.THROW) {
+ protected SendReplayOperation(string name, ReplayOperation.OnError on_remote_error = OnError.THROW) {
base (name, ReplayOperation.Scope.LOCAL_AND_REMOTE, on_remote_error);
}
- public SendReplayOperation.only_remote(string name, ReplayOperation.OnError on_remote_error =
OnError.THROW) {
+ protected SendReplayOperation.only_local(string name, ReplayOperation.OnError on_remote_error =
OnError.THROW) {
+ base (name, ReplayOperation.Scope.LOCAL_ONLY, on_remote_error);
+ }
+
+ protected SendReplayOperation.only_remote(string name, ReplayOperation.OnError on_remote_error =
OnError.THROW) {
base (name, ReplayOperation.Scope.REMOTE_ONLY, on_remote_error);
}
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
similarity index 57%
rename from src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
rename to src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
index d0ae775..d0de4d5 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
@@ -4,68 +4,49 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation {
+/**
+ * Stage two of a { link RevokableMove}: move messages from folder to destination.
+ */
+
+private class Geary.ImapEngine.MoveEmailCommit : Geary.ImapEngine.SendReplayOperation {
+ public Gee.Set<Imap.UID> destination_uids = new Gee.HashSet<Imap.UID>();
+
private MinimalFolder engine;
private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
private Geary.FolderPath destination;
private Cancellable? cancellable;
- private Gee.Set<ImapDB.EmailIdentifier>? moved_ids = null;
- private int original_count = 0;
private Gee.List<Imap.MessageSet>? remaining_msg_sets = null;
-
- public MoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move,
- Geary.FolderPath destination, Cancellable? cancellable = null) {
- base("MoveEmail", OnError.RETRY);
-
+
+ public MoveEmailCommit(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_move,
+ Geary.FolderPath destination, Cancellable? cancellable) {
+ base.only_remote("MoveEmailCommit", OnError.RETRY);
+
this.engine = engine;
-
+
this.to_move.add_all(to_move);
this.destination = destination;
this.cancellable = cancellable;
}
public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
- // don't bother updating on server or backing out locally
- if (moved_ids != null)
- moved_ids.remove_all(ids);
+ to_move.remove_all(ids);
}
public override async ReplayOperation.Status replay_local_async() throws Error {
- if (to_move.size <= 0)
- return ReplayOperation.Status.COMPLETED;
-
- int remote_count;
- int last_seen_remote_count;
- original_count = engine.get_remote_counts(out remote_count, out last_seen_remote_count);
-
- // as this value is only used for reporting, offer best-possible service
- if (original_count < 0)
- original_count = to_move.size;
-
- moved_ids = yield engine.local_folder.mark_removed_async(to_move, true, cancellable);
- if (moved_ids == null || moved_ids.size == 0)
- return ReplayOperation.Status.COMPLETED;
-
- engine.replay_notify_email_removed(moved_ids);
-
- engine.replay_notify_email_count_changed(Numeric.int_floor(original_count - to_move.size, 0),
- Geary.Folder.CountChangeReason.REMOVED);
-
return ReplayOperation.Status.CONTINUE;
}
public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
- if (moved_ids != null)
- ids.add_all(moved_ids);
+ ids.add_all(to_move);
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
- if (moved_ids.size == 0)
+ if (to_move.size == 0)
return ReplayOperation.Status.COMPLETED;
// Remaining MessageSets are persisted in case of network retries
if (remaining_msg_sets == null)
- remaining_msg_sets = Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(moved_ids));
+ remaining_msg_sets = Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(to_move));
if (remaining_msg_sets == null || remaining_msg_sets.size == 0)
return ReplayOperation.Status.COMPLETED;
@@ -78,7 +59,12 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
throw new IOError.CANCELLED("Move email to %s cancelled", engine.remote_folder.to_string());
Imap.MessageSet msg_set = iter.get();
- yield engine.remote_folder.copy_email_async(msg_set, destination, null);
+
+ Gee.Map<Imap.UID, Imap.UID>? map = yield engine.remote_folder.copy_email_async(msg_set,
+ destination, null);
+ if (map != null)
+ destination_uids.add_all(map.values);
+
yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
// completed successfully, remove from list in case of retry
@@ -87,14 +73,19 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
return ReplayOperation.Status.COMPLETED;
}
-
+
public override async void backout_local_async() throws Error {
- yield engine.local_folder.mark_removed_async(moved_ids, false, cancellable);
+ if (to_move.size == 0)
+ return;
+
+ yield engine.local_folder.mark_removed_async(to_move, false, cancellable);
- engine.replay_notify_email_inserted(moved_ids);
- engine.replay_notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.INSERTED);
+ int count = engine.get_remote_counts(null, null);
+
+ engine.replay_notify_email_inserted(to_move);
+ engine.replay_notify_email_count_changed(count + to_move.size, Folder.CountChangeReason.INSERTED);
}
-
+
public override string describe_state() {
return "%d email IDs to %s".printf(to_move.size, destination.to_string());
}
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
new file mode 100644
index 0000000..58b4e4a
--- /dev/null
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
@@ -0,0 +1,70 @@
+/* Copyright 2012-2014 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.
+ */
+
+/**
+ * Stage one of a { link RevokableMove}: collect valid { link ImapDB.EmailIdentifiers}, mark
+ * messages as removed, and update counts.
+ */
+
+private class Geary.ImapEngine.MoveEmailPrepare : Geary.ImapEngine.SendReplayOperation {
+ public Gee.Set<ImapDB.EmailIdentifier>? prepared_for_move = null;
+
+ private MinimalFolder engine;
+ private Cancellable? cancellable;
+ private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
+
+ public MoveEmailPrepare(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_move,
+ Cancellable? cancellable) {
+ base.only_local("MoveEmailPrepare", OnError.RETRY);
+
+ this.engine = engine;
+ this.to_move.add_all(to_move);
+ this.cancellable = cancellable;
+ }
+
+ public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ if (prepared_for_move != null)
+ prepared_for_move.remove_all(ids);
+ }
+
+ public override async ReplayOperation.Status replay_local_async() throws Error {
+ if (to_move.size <= 0)
+ return ReplayOperation.Status.COMPLETED;
+
+ int count = engine.get_remote_counts(null, null);
+
+ // as this value is only used for reporting, offer best-possible service
+ if (count < 0)
+ count = to_move.size;
+
+ prepared_for_move = yield engine.local_folder.mark_removed_async(to_move, true, cancellable);
+ if (prepared_for_move == null || prepared_for_move.size == 0)
+ return ReplayOperation.Status.COMPLETED;
+
+ engine.replay_notify_email_removed(prepared_for_move);
+
+ engine.replay_notify_email_count_changed(
+ Numeric.int_floor(count - prepared_for_move.size, 0),
+ Folder.CountChangeReason.REMOVED);
+
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ }
+
+ public override async ReplayOperation.Status replay_remote_async() throws Error {
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override async void backout_local_async() throws Error {
+ }
+
+ public override string describe_state() {
+ return "%d email IDs".printf(prepared_for_move != null ? prepared_for_move.size : 0);
+ }
+}
+
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
new file mode 100644
index 0000000..15de189
--- /dev/null
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
@@ -0,0 +1,62 @@
+/* Copyright 2012-2014 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.
+ */
+
+/**
+ * Revoked { link RevokableMove}: Unmark emails as removed and update counts.
+ */
+
+private class Geary.ImapEngine.MoveEmailRevoke : Geary.ImapEngine.SendReplayOperation {
+ private MinimalFolder engine;
+ private Gee.List<ImapDB.EmailIdentifier> to_revoke = new Gee.ArrayList<ImapDB.EmailIdentifier>();
+ private Cancellable? cancellable;
+
+ public MoveEmailRevoke(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_revoke,
+ Cancellable? cancellable) {
+ base.only_local("MoveEmailRevoke", OnError.RETRY);
+
+ this.engine = engine;
+
+ this.to_revoke.add_all(to_revoke);
+ this.cancellable = cancellable;
+ }
+
+ public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ to_revoke.remove_all(ids);
+ }
+
+ public override async ReplayOperation.Status replay_local_async() throws Error {
+ if (to_revoke.size == 0)
+ return ReplayOperation.Status.COMPLETED;
+
+ Gee.Set<ImapDB.EmailIdentifier>? revoked = yield engine.local_folder.mark_removed_async(
+ to_revoke, false, cancellable);
+ if (revoked == null || revoked.size == 0)
+ return ReplayOperation.Status.COMPLETED;
+
+ int count = engine.get_remote_counts(null, null);
+
+ engine.replay_notify_email_inserted(revoked);
+ engine.replay_notify_email_count_changed(count + revoked.size,
+ Geary.Folder.CountChangeReason.INSERTED);
+
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ }
+
+ public override async ReplayOperation.Status replay_remote_async() throws Error {
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override async void backout_local_async() throws Error {
+ }
+
+ public override string describe_state() {
+ return "%d email IDs".printf(to_revoke.size);
+ }
+}
+
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-user-close.vala
b/src/engine/imap-engine/replay-ops/imap-engine-user-close.vala
new file mode 100644
index 0000000..14e279f
--- /dev/null
+++ b/src/engine/imap-engine/replay-ops/imap-engine-user-close.vala
@@ -0,0 +1,45 @@
+/* Copyright 2012-2014 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.
+ */
+
+private class Geary.ImapEngine.UserClose : Geary.ImapEngine.ReplayOperation {
+ private MinimalFolder owner;
+ private Cancellable? cancellable;
+
+ public UserClose(MinimalFolder owner, Cancellable? cancellable) {
+ base ("UserClose", Scope.LOCAL_ONLY);
+
+ this.owner = owner;
+ this.cancellable = cancellable;
+ }
+
+ public override void notify_remote_removed_position(Imap.SequenceNumber removed) {
+ }
+
+ public override void notify_remote_removed_ids(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ }
+
+ public override void get_ids_to_be_remote_removed(Gee.Collection<ImapDB.EmailIdentifier> ids) {
+ }
+
+ public override async ReplayOperation.Status replay_local_async() throws Error {
+ yield owner.user_close_async(cancellable);
+
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override async void backout_local_async() throws Error {
+ }
+
+ public override async ReplayOperation.Status replay_remote_async() throws Error {
+ // should not be called
+ return ReplayOperation.Status.COMPLETED;
+ }
+
+ public override string describe_state() {
+ return "";
+ }
+}
+
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 42bbf71..3a0a7f6 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -308,8 +308,8 @@ private class Geary.Imap.Account : BaseObject {
}
}
- public async int fetch_unseen_count_async(FolderPath path, Cancellable? cancellable)
- throws Error {
+ public async void fetch_counts_async(FolderPath path, out int unseen, out int total,
+ Cancellable? cancellable) throws Error {
check_open();
MailboxInformation? mailbox_info = path_to_mailbox.get(path);
@@ -320,8 +320,10 @@ private class Geary.Imap.Account : BaseObject {
path.to_string());
}
- StatusData data = yield fetch_status_async(path, { StatusDataType.UNSEEN }, cancellable);
- return data.unseen;
+ StatusData data = yield fetch_status_async(path, { StatusDataType.UNSEEN, StatusDataType.MESSAGES },
+ cancellable);
+ unseen = data.unseen;
+ total = data.messages;
}
private async StatusData fetch_status_async(FolderPath path, StatusDataType[] status_types,
diff --git a/src/engine/imap/transport/imap-client-session-manager.vala
b/src/engine/imap/transport/imap-client-session-manager.vala
index c84ccce..e11ac2e 100644
--- a/src/engine/imap/transport/imap-client-session-manager.vala
+++ b/src/engine/imap/transport/imap-client-session-manager.vala
@@ -137,10 +137,15 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
// TODO: This isn't the best (deterministic) way to deal with this, but it's easy and works
// for now
+ int attempts = 0;
while (sessions.size > 0) {
debug("Waiting for ClientSessions to disconnect from ClientSessionManager...");
Timeout.add(250, close_async.callback);
yield;
+
+ // give up after three seconds
+ if (++attempts > 12)
+ break;
}
}
@@ -341,7 +346,8 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
public async void release_session_async(ClientSession session, Cancellable? cancellable)
throws Error {
- check_open();
+ // Don't check_open(), it's valid for this to be called when is_open is false, that happens
+ // during mop-up
MailboxSpecifier? mailbox;
ClientSession.Context context = session.get_context(out mailbox);
@@ -383,9 +389,15 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
assert_not_reached();
}
- if (unreserve) {
+ if (!unreserve)
+ return;
+
+ // if not open, disconnect, which will remove from the reserved pool anyway
+ if (!is_open) {
+ yield force_disconnect_async(session, true);
+ } else {
try {
- // don't respect Cancellable because this *must* happen; don't want this lingering
+ // don't respect Cancellable because this *must* happen; don't want this lingering
// on the reserved list forever
int token = yield sessions_mutex.claim_async();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]