Commit 4feba6c9 authored by Jim Nelson's avatar Jim Nelson

Quicker and simpler folder normalization: Closes #7364

Folder normalization is now much simpler, faster, and requires
less resources than prior implementation.  Normalization affects
the ReplayOperations, so an interface was changed here.
parent 4e6e0bb0
......@@ -404,8 +404,8 @@ private class Geary.ImapDB.Account : BaseObject {
return exists;
}
public async Geary.ImapDB.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
public async Geary.ImapDB.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable)
throws Error {
check_open();
// check references table first
......@@ -908,8 +908,6 @@ private class Geary.ImapDB.Account : BaseObject {
// set to Db.INVALID_ROWID.
private bool do_fetch_folder_id(Db.Connection cx, Geary.FolderPath path, bool create, out int64 folder_id,
Cancellable? cancellable) throws Error {
check_open();
int length = path.get_path_length();
if (length < 0)
throw new EngineError.BAD_PARAMETERS("Invalid path %s", path.to_string());
......
......@@ -37,6 +37,10 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
this.message_id = message_id;
}
public bool has_uid() {
return (uid != null) && uid.is_valid();
}
public override int natural_sort_comparator(Geary.EmailIdentifier o) {
ImapDB.EmailIdentifier? other = o as ImapDB.EmailIdentifier;
if (other == null)
......
......@@ -548,6 +548,41 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return email;
}
public async Gee.SortedSet<Imap.UID>? list_uids_by_range_async(Imap.UID first_uid, Imap.UID last_uid,
Cancellable? cancellable) throws Error {
// order correctly
Imap.UID start, end;
if (first_uid.compare_to(last_uid) < 0) {
start = first_uid;
end = last_uid;
} else {
start = last_uid;
end = first_uid;
}
Gee.SortedSet<Imap.UID> uids = new Gee.TreeSet<Imap.UID>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare("""
SELECT ordering
FROM MessageLocationTable
WHERE folder_id = ? AND ordering >= ? AND ordering <= ?
""");
stmt.bind_rowid(0, folder_id);
stmt.bind_int64(1, start.value);
stmt.bind_int64(2, end.value);
Db.Result result = stmt.exec(cancellable);
while (!result.finished) {
uids.add(new Imap.UID(result.int64_at(0)));
result.next(cancellable);
}
return Db.TransactionOutcome.DONE;
}, cancellable);
return (uids.size > 0) ? uids : null;
}
// pos is 1-based. This method does not respect messages marked for removal.
public async ImapDB.EmailIdentifier? get_id_at_async(int pos, Cancellable? cancellable) throws Error {
assert(pos >= 1);
......@@ -625,6 +660,23 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return id;
}
public async Gee.Set<ImapDB.EmailIdentifier> get_ids_async(Gee.Collection<Imap.UID> uids,
Cancellable? cancellable) throws Error {
Gee.Set<ImapDB.EmailIdentifier> ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
foreach (Imap.UID uid in uids) {
LocationIdentifier? location = do_get_location_for_uid(cx, uid, ListFlags.NONE,
cancellable);
if (location != null)
ids.add(location.email_id);
}
return Db.TransactionOutcome.DONE;
}, cancellable);
return (ids.size > 0) ? ids : null;
}
// This does not respect messages marked for removal.
public async ImapDB.EmailIdentifier? get_earliest_id_async(Cancellable? cancellable) throws Error {
return yield get_id_extremes_async(true, cancellable);
......@@ -1055,8 +1107,23 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
// if found, merge, and associate if necessary
if (location != null) {
do_merge_email(cx, location, email, out pre_fields, out post_fields,
out updated_contacts, ref unread_count_change, !associated, cancellable);
if (!associated)
do_associate_with_folder(cx, location.message_id, location.uid, cancellable);
// If the email came from the Imap layer, we need to fill in the id.
ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
if (email_id.message_id == Db.INVALID_ROWID)
email_id.promote_with_message_id(location.message_id);
// special-case updating flags, which happens often and should only write to the DB
// if necessary
if (email.fields != Geary.Email.Field.FLAGS) {
do_merge_email(cx, location, email, out pre_fields, out post_fields,
out updated_contacts, ref unread_count_change, cancellable);
} else {
do_merge_email_flags(cx, location, email, out pre_fields, out post_fields,
out updated_contacts, ref unread_count_change, cancellable);
}
// return false to indicate a merge
return false;
......@@ -1481,23 +1548,15 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
}
if (new_fields.is_any_set(Geary.Email.Field.FLAGS)) {
// Fetch existing flags.
// Fetch existing flags to update unread count
Geary.EmailFlags? old_flags = do_get_email_flags_single(cx, row.id, cancellable);
Geary.EmailFlags new_flags = new Geary.Imap.EmailFlags(
Geary.Imap.MessageFlags.deserialize(row.email_flags));
if (old_flags != null) {
// Update unread count if needed.
if (old_flags.contains(Geary.EmailFlags.UNREAD) && !new_flags.contains(
Geary.EmailFlags.UNREAD))
unread_count_change--;
else if (!old_flags.contains(Geary.EmailFlags.UNREAD) && new_flags.contains(
Geary.EmailFlags.UNREAD))
unread_count_change++;
} else if (new_flags.contains(Geary.EmailFlags.UNREAD)) {
// No previous flags.
if (old_flags != null && (old_flags.is_unread() != new_flags.is_unread()))
unread_count_change += new_flags.is_unread() ? 1 : -1;
else if (new_flags.is_unread())
unread_count_change++;
}
Db.Statement stmt = cx.prepare(
"UPDATE MessageTable SET flags=? WHERE id=?");
......@@ -1590,24 +1649,45 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
}
}
private void do_merge_email(Db.Connection cx, LocationIdentifier location, Geary.Email email,
// This *replaces* the stored flags, it does not OR them ... this is simply a fast-path over
// do_merge_email(), as updating FLAGS happens often and doesn't require a lot of extra work
private void do_merge_email_flags(Db.Connection cx, LocationIdentifier location, Geary.Email email,
out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields,
out Gee.Collection<Contact> updated_contacts, ref int unread_count_change,
bool associate_with_folder, Cancellable? cancellable) throws Error {
// If the email came from the Imap layer, we need to fill in the id.
ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
if (email_id.message_id == Db.INVALID_ROWID)
email_id.promote_with_message_id(location.message_id);
int new_unread_count = 0;
Cancellable? cancellable) throws Error {
assert(email.fields == Geary.Email.Field.FLAGS);
if (associate_with_folder) {
// Note: no check is performed here to prevent double-adds. The caller of this method
// is responsible for only setting associate_with_folder if required.
do_associate_with_folder(cx, location.message_id, location.uid, cancellable);
unread_count_change++;
}
// no contacts were harmed in the production of this email
updated_contacts = new Gee.ArrayList<Contact>();
// fetch MessageRow and its fields, note that the fields now include FLAGS if they didn't
// already
MessageRow row = do_fetch_message_row(cx, location.message_id, Geary.Email.Field.FLAGS,
out pre_fields, cancellable);
post_fields = pre_fields;
// compare flags for (a) any change at all and (b) unread changes
Geary.Email row_email = row.to_email(location.email_id);
if (row_email.email_flags != null && row_email.email_flags.equal_to(email.email_flags))
return;
if (row_email.email_flags.is_unread() != email.email_flags.is_unread())
unread_count_change += email.email_flags.is_unread() ? 1 : -1;
// write them out to the message row
Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map = new Gee.HashMap<ImapDB.EmailIdentifier,
Geary.EmailFlags>();
map.set((ImapDB.EmailIdentifier) email.id, email.email_flags);
do_set_email_flags(cx, map, cancellable);
post_fields |= Geary.Email.Field.FLAGS;
}
private void do_merge_email(Db.Connection cx, LocationIdentifier location, Geary.Email email,
out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields,
out Gee.Collection<Contact> updated_contacts, ref int unread_count_change,
Cancellable? cancellable) throws Error {
// Default to an empty list, in case we never call do_merge_message_row.
updated_contacts = new Gee.LinkedList<Contact>();
......@@ -1627,6 +1707,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
do_add_attachments(cx, combined_email, location.message_id, cancellable);
// Merge in any fields in the submitted email that aren't already in the database or are mutable
int new_unread_count = 0;
if (((fetched_fields & email.fields) != email.fields) ||
email.fields.is_any_set(Geary.Email.MUTABLE_FIELDS)) {
Geary.Email.Field new_fields;
......
......@@ -5,17 +5,19 @@
*/
/**
* Because IMAP doesn't offer a standard mechanism for notifications of email flags changing,
* have to poll for changes, annoyingly. This class performs this task by monitoring the supplied
* Monitor an open {@link ImapEngine.GenericFolder} for changes to {@link EmailFlags}.
*
* Because IMAP doesn't offer a standard mechanism for server notifications of email flags changing,
* have to poll for changes. This class performs this task by monitoring the supplied
* folder for its "opened" and "closed" signals and periodically polling for changes.
*
* Note that EmailFlagWatcher doesn't maintain a reference to the Geary.Folder it's watching.
*/
private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
public const int DEFAULT_FLAG_WATCH_SEC = 3 * 60;
private const int PULL_CHUNK_COUNT = 100;
private const int MAX_EMAIL_WATCHED = 1000;
public bool enabled { get; set; default = true; }
......@@ -51,7 +53,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
cancellable = new Cancellable();
if (watch_id == 0)
watch_id = Timeout.add_seconds(seconds, on_flag_watch);
watch_id = Idle.add(on_opened_update_flags);
}
private void on_closed(Geary.Folder.CloseReason close_reason) {
......@@ -66,6 +68,17 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
watch_id = 0;
}
private bool on_opened_update_flags() {
if (enabled)
flag_watch_async.begin();
// this callback was immediately called due to open, schedule next ones for here on out
// on a timer
watch_id = Timeout.add_seconds(seconds, on_flag_watch);
return false;
}
private bool on_flag_watch() {
if (!enabled) {
// try again later
......@@ -100,7 +113,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
Geary.EmailIdentifier? lowest = null;
int total = 0;
do {
for (;;) {
Gee.List<Geary.Email>? list_local = yield folder.list_email_by_id_async(lowest,
PULL_CHUNK_COUNT, Geary.Email.Field.FLAGS, Geary.Folder.ListFlags.LOCAL_ONLY, cancellable);
if (list_local == null || list_local.is_empty)
......@@ -139,7 +152,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
if (!cancellable.is_cancelled() && changed_map.size > 0)
email_flags_changed(changed_map);
} while (total < MAX_EMAIL_WATCHED);
}
Logging.debug(Logging.Flag.PERIODIC, "do_flag_watch_async: completed %s, %d messages updates",
folder.to_string(), total);
......
......@@ -433,12 +433,28 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
}
}
// always update, openable or not; update UIDs if already open, otherwise will keep
// always update, openable or not; update UIDs if remote opened, otherwise will keep
// signalling that it's changed (because the only time UIDNEXT/UIDValidity is updated
// is when the folder is first opened)
// is when the remote folder is first opened)
try {
yield local.update_folder_status_async(remote_folder,
generic_folder.get_open_state() != Geary.Folder.OpenState.CLOSED, cancellable);
bool update_uid_info;
switch (generic_folder.get_open_state()) {
case Folder.OpenState.REMOTE:
case Folder.OpenState.BOTH:
update_uid_info = true;
break;
case Folder.OpenState.LOCAL:
case Folder.OpenState.OPENING:
case Folder.OpenState.CLOSED:
update_uid_info = false;
break;
default:
assert_not_reached();
}
yield local.update_folder_status_async(remote_folder, update_uid_info, cancellable);
} catch (Error update_error) {
debug("Unable to update local folder %s with remote properties: %s",
remote_folder.to_string(), update_error.message);
......
......@@ -9,11 +9,8 @@ private abstract class Geary.ImapEngine.ReceiveReplayOperation : Geary.ImapEngin
base (name, ReplayOperation.Scope.LOCAL_ONLY);
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
debug("Warning: ReceiveReplayOperation.query_local_writebehind_operation() called");
return true;
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
debug("Warning: ReceiveReplayOperation.notify_remote_removed_during_normalization() called");
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
......
......@@ -32,12 +32,6 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
CONTINUE
}
public enum WritebehindOperation {
CREATE,
REMOVE,
UPDATE_FLAGS
}
private static int next_opnum = 0;
public string name { get; set; }
......@@ -71,24 +65,16 @@ private abstract class Geary.ImapEngine.ReplayOperation : Geary.BaseObject {
* See Scope for conditions where this method will be called.
*
* This method is called only when the ReplayOperation is blocked waiting to execute a remote
* command and an exterior operation is going to occur that may alter the state on the local
* database (i.e. altering state behind the execution of this operation's replay_local_async()).
* This primarily happens during folder normalization (initial synchronization with the server
* command and its discovered that the supplied email(s) are no longer on the server.
* This happens during folder normalization (initial synchronization with the server
* when a folder is opened) where ReplayOperations are allowed to execute locally and enqueue
* for remote operation in preparation for the folder to open. (There may be other
* circumstances in the future where this method may be called.)
*
* The method should examine the supplied operation and return true if it's okay to proceed
* (and modifying its own operation to reflect the change that will occur before it's allowed to
* proceed, or merely not performing any operation in replay_remote_async()) or false if the
* supplied operation should *not* execute so that this ReplayOperation's command may execute
* shortly.
* for remote operation in preparation for the folder to fully open.
*
* flags will only be non-null when op is UPDATE_FLAGS. In that case, if this method returns
* true, it may also modify the EmailFlags. Those flags will be written to the local store.
* The ReplayOperation should remove any reference to the emails so not to attempt operation
* on the server. If it's discovered in replay_remote_async() that there are no more operations
* to perform, it should simply exit without contacting the server.
*/
public abstract bool query_local_writebehind_operation(WritebehindOperation op, EmailIdentifier id,
Imap.EmailFlags? flags);
public abstract void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids);
/**
* See Scope for conditions where this method will be called.
......
......@@ -15,10 +15,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
return Status.CONTINUE;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
// whatever, no problem, do what you will
return true;
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
......@@ -150,21 +147,12 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
* changes that need to be synchronized on the client. If this change is written before the
* enqueued replay operations execute, the potential exists to be unsynchronized.
*
* This call gives all enqueued remote replay operations a chance to cancel or update their
* own state due to a writebehind operation. See
* ReplayOperation.query_local_writebehind_operation() for more information.
* This call gives all enqueued remote replay operations a chance to update their own state.
* See ReplayOperation.notify_remote_removed_during_normalization() for more information.
*/
public bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
Geary.EmailIdentifier id, Imap.EmailFlags? flags) {
// Although any replay operation can cancel the writebehind operation, give all a chance to
// see it as it may affect their internal state
bool proceed = true;
foreach (ReplayOperation replay_op in remote_queue.get_all()) {
if (!replay_op.query_local_writebehind_operation(op, id, flags))
proceed = false;
}
return proceed;
public void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
foreach (ReplayOperation replay_op in remote_queue.get_all())
replay_op.notify_remote_removed_during_normalization(ids);
}
public async void close_async(Cancellable? cancellable = null) throws Error {
......
......@@ -75,44 +75,29 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
this.flags = flags;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
// don't need to check if id is present here, all paths deal with this possibility
// correctly
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
// remove email already picked up from local store ... for email reported via the
// callback, too late
if (accumulator != null) {
Collection.remove_if<Geary.Email>(accumulator, (email) => {
return ids.contains((ImapDB.EmailIdentifier) email.id);
});
}
switch (op) {
case ReplayOperation.WritebehindOperation.REMOVE:
// remove email already picked up from local store ... for email reported via the
// callback, too late
if (accumulator != null) {
Gee.HashSet<Geary.Email> wb_removed = new Gee.HashSet<Geary.Email>();
foreach (Geary.Email email in accumulator) {
if (email.id.equal_to(id))
wb_removed.add(email);
}
accumulator.remove_all(wb_removed);
// remove from unfulfilled list, as there's nothing to fetch from the server
// this funky little loop ensures that all mentions of the EmailIdentifier in
// the unfulfilled MultiMap are removed, but must restart loop because removing
// within a foreach invalidates the Iterator
foreach (Geary.EmailIdentifier id in ids) {
bool removed = false;
do {
removed = false;
foreach (Geary.Email.Field field in unfulfilled.get_keys()) {
removed = unfulfilled.remove(field, (ImapDB.EmailIdentifier) id);
if (removed)
break;
}
// remove from unfulfilled list, as there's nothing to fetch from the server
// this funky little loop ensures that all mentions of the EmailIdentifier in
// the unfulfilled MultiMap are removed, but must restart loop because removing
// within a foreach invalidates the Iterator
bool removed = false;
do {
removed = false;
foreach (Geary.Email.Field field in unfulfilled.get_keys()) {
removed = unfulfilled.remove(field, (ImapDB.EmailIdentifier) id);
if (removed)
break;
}
} while (removed);
return true;
default:
// ignored
return true;
} while (removed);
}
}
......
......@@ -30,22 +30,14 @@ private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation
return ReplayOperation.Status.CONTINUE;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
if (imapdb_id == null)
return true;
// only interested in messages going away (i.e. can't be copied) ...
// note that this method operates exactly the same way whether the EmailIdentifer is in
// the to_copy list or not.
if (op == ReplayOperation.WritebehindOperation.REMOVE)
to_copy.remove(imapdb_id);
return true;
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
to_copy.remove_all(ids);
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
if (to_copy.size == 0)
return ReplayOperation.Status.COMPLETED;
Gee.Set<Imap.UID>? uids = yield engine.local_folder.get_uids_async(to_copy,
ImapDB.Folder.ListFlags.NONE, cancellable);
......
......@@ -45,38 +45,19 @@ private class Geary.ImapEngine.ExpungeEmail : Geary.ImapEngine.SendReplayOperati
return ReplayOperation.Status.CONTINUE;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
if (imapdb_id == null)
return true;
if (!removed_ids.contains(imapdb_id))
return true;
switch (op) {
case ReplayOperation.WritebehindOperation.CREATE:
// don't allow for the message to be created, it will be removed on the server by
// this operation
return false;
case ReplayOperation.WritebehindOperation.REMOVE:
// removed locally, to be removed remotely, don't bother writing locally
return false;
default:
// ignored
return true;
}
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
removed_ids.remove_all(ids);
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
// Remove from server. Note that this causes the receive replay queue to kick into
// action, removing the e-mail but *NOT* firing a signal; the "remove marker" indicates
// that the signal has already been fired.
yield engine.remote_folder.remove_email_async(
new Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(removed_ids).to_array()),
cancellable);
if (removed_ids.size > 0) {
yield engine.remote_folder.remove_email_async(
new Imap.MessageSet.uid_sparse(ImapDB.EmailIdentifier.to_uids(removed_ids).to_array()),
cancellable);
}
return ReplayOperation.Status.COMPLETED;
}
......
......@@ -14,7 +14,7 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
private Folder.ListFlags flags;
private Cancellable? cancellable;
private Imap.UID? uid = null;
private bool writebehind_removed = false;
private bool remote_removed = false;
public FetchEmail(GenericFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
Folder.ListFlags flags, Cancellable? cancellable) {
......@@ -77,28 +77,13 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
return ReplayOperation.Status.CONTINUE;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
if (!this.id.equal_to(id))
return true;
switch (op) {
case ReplayOperation.WritebehindOperation.REMOVE:
writebehind_removed = true;
return true;
case ReplayOperation.WritebehindOperation.CREATE:
default:
// still need to do the full fetch for CREATE, since it's unknown (currently) what
// fields are available locally; otherwise, ignored
return true;
}
public override void notify_remote_removed_during_normalization(Gee.Collection<ImapDB.EmailIdentifier> ids) {
remote_removed = ids.contains(id);
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
if (writebehind_removed) {
throw new EngineError.NOT_FOUND("Unable to fetch %s in %s (removed with writebehind)",
if (remote_removed) {
throw new EngineError.NOT_FOUND("Unable to fetch %s in %s (removed from remote)",
id.to_string(), engine.to_string());
}
......
......@@ -67,6 +67,13 @@ private class Geary.ImapEngine.ListEmailBySparseID : Geary.ImapEngine.AbstractLi
return ReplayOperation.Status.CONTINUE;
}
public override void notify_remote_removed_during_normalization(
Gee.Collection<ImapDB.EmailIdentifier> removed_ids) {
ids.remove_all(removed_ids);
base.notify_remote_removed_during_normalization(removed_ids);
}
public override async void backout_local_async() throws Error {
// R/O, nothing to backout
}
......
......@@ -49,38 +49,9 @@ private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation
return ReplayOperation.Status.CONTINUE;
}
public override bool query_local_writebehind_operation(ReplayOperation.WritebehindOperation op,
EmailIdentifier id, Imap.EmailFlags? flags) {
ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
if (imapdb_id == null)
return true;
if (!original_flags.has_key(imapdb_id))
return true;
switch (op) {
case ReplayOperation.WritebehindOperation.REMOVE:
// don't bother updating on server
original_flags.unset(imapdb_id);
return true;
case ReplayOperation.WritebehindOperation.UPDATE_FLAGS: