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:
Jim Nelson 2015-02-05 17:57:27 -08:00
parent 3d14719aa0
commit 354e2edbf8
25 changed files with 1010 additions and 118 deletions

View file

@ -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

View file

@ -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

View file

@ -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();
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View 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;
}
}

View file

@ -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();

View file

@ -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);
}
}
}

View file

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

View file

@ -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);

View file

@ -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) {

View file

@ -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",

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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);
}

View file

@ -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());
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 "";
}
}

View file

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

View file

@ -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();