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.
This commit is contained in:
parent
3d14719aa0
commit
354e2edbf8
25 changed files with 1010 additions and 118 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -456,6 +456,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
|
||||
|
|
|
|||
183
src/engine/api/geary-revokable.vala
Normal file
183
src/engine/api/geary-revokable.vala
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
|
||||
replay_queue.schedule(move);
|
||||
MoveEmailPrepare prepare = new MoveEmailPrepare(this, (Gee.List<ImapDB.EmailIdentifier>) to_move,
|
||||
cancellable);
|
||||
replay_queue.schedule(prepare);
|
||||
|
||||
yield move.wait_for_ready_async(cancellable);
|
||||
yield prepare.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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
115
src/engine/imap-engine/imap-engine-revokable-move.vala
Normal file
115
src/engine/imap-engine/imap-engine-revokable-move.vala
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
engine.replay_notify_email_inserted(moved_ids);
|
||||
engine.replay_notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.INSERTED);
|
||||
yield engine.local_folder.mark_removed_async(to_move, false, cancellable);
|
||||
|
||||
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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue