Commit 0533bc97 authored by Jim Nelson's avatar Jim Nelson

Further work on detecting message removal when folder first selected: #3805

Needed to rethink storage strategies as I researched this and realized that a true scarce database -- where the database is sparsely populated both in columns and rows -- is not feasible due to IMAP's UID rules.  The strategy now means that the database rows are contiguous from the highest (newest) message to the oldest *requested by the user*.  This is a better situation than having to download the UID for the entire folder.
parent 6b8951bf
......@@ -51,11 +51,12 @@ CREATE TABLE MessageLocationTable (
id INTEGER PRIMARY KEY,
message_id INTEGER REFERENCES MessageTable ON DELETE CASCADE,
folder_id INTEGER REFERENCES FolderTable ON DELETE CASCADE,
position INTEGER
ordering INTEGER
);
CREATE INDEX MessageLocationTableMessageIDIndex ON MessageLocationTable(message_id);
CREATE INDEX MessageLocationTableFolderIDIndex ON MessageLocationTable(folder_id);
CREATE INDEX MessageLocationTableOrderingIndex ON MessageLocationTable(ordering ASC);
--
-- IMAP-specific tables
......@@ -68,7 +69,9 @@ CREATE INDEX MessageLocationTableFolderIDIndex ON MessageLocationTable(folder_id
CREATE TABLE ImapFolderPropertiesTable (
id INTEGER PRIMARY KEY,
folder_id INTEGER UNIQUE REFERENCES FolderTable ON DELETE CASCADE,
last_seen_total INTEGER,
uid_validity INTEGER,
uid_next INTEGER,
attributes TEXT
);
......@@ -81,20 +84,10 @@ CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable
CREATE TABLE ImapMessagePropertiesTable (
id INTEGER PRIMARY KEY,
message_id INTEGER UNIQUE REFERENCES MessageTable ON DELETE CASCADE,
flags TEXT
flags TEXT,
internaldate TEXT,
rfc822_size INTEGER
);
CREATE INDEX ImapMessagePropertiesTableMessageIDIndex ON ImapMessagePropertiesTable(message_id);
--
-- ImapMessageLocationPropertiesTable
--
CREATE TABLE ImapMessageLocationPropertiesTable (
id INTEGER PRIMARY KEY,
location_id INTEGER UNIQUE REFERENCES MessageLocationTable ON DELETE CASCADE,
uid INTEGER
);
CREATE INDEX ImapMessageLocationPropertiesTableLocationIDIndex ON ImapMessageLocationPropertiesTable(location_id);
......@@ -214,7 +214,7 @@ public class MainWindow : Gtk.Window {
yield current_folder.open_async(true);
current_folder.lazy_list_email_async(1, 1000, MessageListStore.REQUIRED_FIELDS,
current_folder.lazy_list_email_async(-1, 50, MessageListStore.REQUIRED_FIELDS,
on_list_email_ready);
}
......
......@@ -61,7 +61,7 @@ public class MessageListStore : Gtk.TreeStore {
// The Email should've been fetched with REQUIRED_FIELDS.
public void append_envelope(Geary.Email envelope) {
assert(envelope.fields.fulfills(Geary.Email.Field.ENVELOPE));
assert(envelope.fields.fulfills(REQUIRED_FIELDS));
Gtk.TreeIter iter;
append(out iter, null);
......
/* Copyright 2011 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.
*/
namespace Arrays {
public G[]? list_to_array<G>(Gee.List<G>? list) {
if (list == null)
return null;
int length = list.size;
G[] array = new G[length];
for (int ctr = 0; ctr < length; ctr++)
array[ctr] = list[ctr];
return array;
}
public int int_find_low(int[] ar) {
assert(ar.length > 0);
int low = int.MAX;
foreach (int i in ar) {
if (i < low)
low = i;
}
return low;
}
public void int_find_high_low(int[] ar, out int low, out int high) {
assert(ar.length > 0);
low = int.MAX;
high = int.MIN;
foreach (int i in ar) {
if (i < low)
low = i;
if (i > high)
high = i;
}
}
}
......@@ -84,6 +84,9 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields,
Cancellable? cancellable = null) throws Error;
public abstract async void remove_email_async(Geary.Email email, Cancellable? cancellable = null)
throws Error;
public virtual string to_string() {
return get_path().to_string();
}
......
......@@ -36,7 +36,7 @@ public class Geary.Email : Object {
return (this & required_fields) == required_fields;
}
public inline bool is_set(Field required_fields) {
public inline bool is_any_set(Field required_fields) {
return (this & required_fields) != 0;
}
......
......@@ -11,6 +11,7 @@ public errordomain Geary.EngineError {
NOT_FOUND,
READONLY,
BAD_PARAMETERS,
INCOMPLETE_MESSAGE
INCOMPLETE_MESSAGE,
SERVER_UNAVAILABLE
}
......@@ -7,11 +7,10 @@
private class Geary.EngineFolder : Geary.AbstractFolder {
private const int REMOTE_FETCH_CHUNK_COUNT = 10;
protected RemoteAccount remote;
protected LocalAccount local;
protected RemoteFolder? remote_folder = null;
protected LocalFolder local_folder;
private RemoteAccount remote;
private LocalAccount local;
private RemoteFolder? remote_folder = null;
private LocalFolder local_folder;
private bool opened = false;
private Geary.Common.NonblockingSemaphore remote_semaphore =
new Geary.Common.NonblockingSemaphore(true);
......@@ -43,6 +42,22 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
throw new EngineError.READONLY("Engine currently read-only");
}
/**
* This method is called by EngineFolder when the folder has been opened. It allows for
* subclasses to examine either folder and cleanup any inconsistencies that have developed
* since the last time it was opened.
*
* Implementations should *not* use this as an opportunity to re-sync the entire database;
* EngineFolder does that automatically on-demand. Rather, this should be used to re-sync
* inconsistencies that hamper or preclude fetching messages out of the database accurately.
*
* This will only be called if both the local and remote folder have been opened.
*/
protected virtual async bool prepare_opened_folder(Geary.Folder local_folder, Geary.Folder remote_folder,
Cancellable? cancellable) throws Error {
return true;
}
public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error {
if (opened)
throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string());
......@@ -57,43 +72,53 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// wait_for_remote_to_open(), which uses a NonblockingSemaphore to indicate that the remote
// is open (or has failed to open). This allows for early calls to list and fetch emails
// can work out of the local cache until the remote is ready.
open_remote_async.begin(readonly, cancellable, on_open_remote_completed);
open_remote_async.begin(readonly, cancellable);
opened = true;
}
private async void open_remote_async(bool readonly, Cancellable? cancellable) throws Error {
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(),
cancellable);
yield folder.open_async(readonly, cancellable);
remote_folder = folder;
remote_folder.updated.connect(on_remote_updated);
remote_semaphore.notify();
}
private void on_open_remote_completed(Object? source, AsyncResult result) {
private async void open_remote_async(bool readonly, Cancellable? cancellable) {
try {
open_remote_async.end(result);
notify_opened(Geary.Folder.OpenState.BOTH);
} catch (Error err) {
debug("Unable to open remote folder %s: %s", to_string(), err.message);
debug("Opening remote %s", local_folder.get_path().to_string());
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(),
cancellable);
yield folder.open_async(readonly, cancellable);
remote_folder = null;
try {
remote_semaphore.notify();
} catch (Error err) {
debug("Unable to notify remote folder ready: %s", err.message);
// allow subclasses to examine the opened folder and resolve any vital
// inconsistencies
if (yield prepare_opened_folder(local_folder, folder, cancellable)) {
// update flags, properties, etc.
yield local.update_folder_async(folder, cancellable);
// all set; bless the remote folder as opened
remote_folder = folder;
remote_folder.updated.connect(on_remote_updated);
} else {
debug("Unable to prepare remote folder %s: prepare_opened_file() failed", to_string());
}
notify_opened(Geary.Folder.OpenState.LOCAL);
} catch (Error err) {
debug("Unable to open or prepare remote folder %s: %s", to_string(), err.message);
}
// notify any threads of execution waiting for the remote folder to open that the result
// of that operation is ready
try {
remote_semaphore.notify();
} catch (Error notify_err) {
debug("Unable to fire semaphore notifying remote folder ready/not ready: %s",
notify_err.message);
}
// notify any subscribers with similar information
notify_opened(
(remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL);
}
private async void wait_for_remote_to_open() throws Error {
// Returns true if the remote folder is ready, false otherwise
private async bool wait_for_remote_to_open() throws Error {
yield remote_semaphore.wait_async();
return (remote_folder != null);
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
......@@ -118,8 +143,14 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
}
public override async int get_email_count(Cancellable? cancellable = null) throws Error {
// TODO
return 0;
// TODO: Use monitoring to avoid round-trip to the server
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
if (remote_folder != null)
return yield remote_folder.get_email_count(cancellable);
return yield local_folder.get_email_count(cancellable);
}
public override async Gee.List<Geary.Email>? list_email_async(int low, int count,
......@@ -144,8 +175,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private async void do_list_email_async(int low, int count, Geary.Email.Field required_fields,
Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null)
throws Error {
assert(low >= 1);
assert(count >= 0 || count == -1);
check_span_specifiers(low, count);
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
......@@ -158,6 +188,11 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return;
}
// normalize the position (ordering) of what's available locally with the situation on
// the server
int remote_count = yield normalize_email_positions_async(low, count, cancellable);
normalize_span_specifiers(ref low, ref count, remote_count);
Gee.List<Geary.Email>? local_list = null;
try {
local_list = yield local_folder.list_email_async(low, count, required_fields,
......@@ -259,6 +294,12 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return;
}
// normalize the position (ordering) of what's available locally with the situation on
// the server
int low, high;
Arrays.int_find_high_low(by_position, out low, out high);
yield normalize_email_positions_async(low, high - low + 1, cancellable);
Gee.List<Geary.Email>? local_list = null;
try {
local_list = yield local_folder.list_email_sparse_async(by_position, required_fields,
......@@ -346,17 +387,18 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private async Gee.List<Geary.Email>? remote_list_email(int[] needed_by_position,
Geary.Email.Field required_fields, EmailCallback? cb, Cancellable? cancellable) throws Error {
// possible to call remote multiple times, wait for it to open once and go
yield wait_for_remote_to_open();
if (!yield wait_for_remote_to_open())
return null;
debug("Background fetching %d emails for %s", needed_by_position.length, to_string());
Gee.List<Geary.Email> full = new Gee.ArrayList<Geary.Email>();
int index = 0;
while (index <= needed_by_position.length) {
while (index < needed_by_position.length) {
// if a callback is specified, pull the messages down in chunks, so they can be reported
// incrementally
unowned int[] list;
int[] list;
if (cb != null) {
int list_count = int.min(REMOTE_FETCH_CHUNK_COUNT, needed_by_position.length - index);
list = needed_by_position[index:index + list_count];
......@@ -443,7 +485,9 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
fields = fields.set(Geary.Email.Field.REFERENCES);
// fetch from network
yield wait_for_remote_to_open();
if (!yield wait_for_remote_to_open())
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
Geary.Email email = yield remote_folder.fetch_email_async(num, fields, cancellable);
// save to local store
......@@ -452,10 +496,72 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return email;
}
public override async void remove_email_async(Geary.Email email, Cancellable? cancellable = null)
throws Error {
if (!opened)
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string());
if (remote_folder == null) {
throw new EngineError.READONLY("Unable to delete from %s: remote unavailable",
to_string());
}
yield remote_folder.remove_email_async(email, cancellable);
yield local_folder.remove_email_async(email, cancellable);
}
private void on_local_updated() {
}
private void on_remote_updated() {
}
// In order to maintain positions for all messages without storing all of them locally,
// the database stores entries for the lowest requested email to the highest (newest), which
// means there can be no gaps between the last in the database and the last on the server.
// This method takes care of that.
//
// Returns the email count on remote_folder.
private async int normalize_email_positions_async(int low, int count, Cancellable? cancellable)
throws Error {
if (!yield wait_for_remote_to_open())
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
int local_count = yield local_folder.get_email_count(cancellable);
int remote_count = yield remote_folder.get_email_count(cancellable);
// fixup span specifier
normalize_span_specifiers(ref low, ref count, remote_count);
// Only prefetch properties for messages not being asked for by the user
// (any messages that may be between the user's high and the remote's high, assuming that
// all messages in local_count are contiguous from the highest email position, which is
// taken care of my prepare_opened_folder_async())
int local_low = remote_count - local_count + 1;
if (low >= local_low)
return remote_count;
int prefetch_count = local_low - low;
debug("prefetching %d (%d) for %s", low, prefetch_count, to_string());
// Use PROPERTIES as they're the most useful information for certain actions (such as
// finding duplicates when we start using INTERNALDATE and RFC822.SIZE) and cheap to fetch
// TODO: Consider only fetching their UID; would need Geary.Email.Field.LOCATION (or\
// perhaps NONE is considered a call for just the UID).
Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(low, prefetch_count,
Geary.Email.Field.PROPERTIES, cancellable);
if (list == null || list.size != prefetch_count) {
throw new EngineError.BAD_PARAMETERS("Unable to prefetch %d email starting at %d in %s",
count, low, to_string());
}
foreach (Geary.Email email in list)
yield local_folder.create_email_async(email, cancellable);
debug("prefetched %d for %s", prefetch_count, to_string());
return remote_count;
}
}
......@@ -116,12 +116,12 @@ public interface Geary.Folder : Object {
* from 1 to n.
*
* Note that this only returns the number of messages available to the backing medium. In the
* case of the local store, this might be less than the number on the network server. Folders
* case of the local store, this might differ from the number on the network server. Folders
* created by Engine are aggregating objects and will return the true count. However, this
* might require a round-trip to the server.
*
* Also note that local folders may be sparsely populated. get_count() returns the last position
* available, but not all emails from 1 to n may be available.
* Also note that local folders may be sparsely populated. get_email_count() returns the last
* position available, but not all emails from 1 to n may be available.
*
* The Folder must be opened prior to attempting this operation.
*/
......@@ -147,12 +147,20 @@ public interface Geary.Folder : Object {
/**
* Returns a list of messages that fulfill the required_fields flags starting at the low
* position and moving up to (low + count). If count is -1, the returned list starts at low
* and proceeds to all available emails. The returned list is not guaranteed to be in any
* particular order.
* and proceeds to all available emails. If low is -1, the *last* (most recent) 'count' emails
* are returned. If both low and count are -1, it's no different than calling with low as
* 1 and count -1, that is, all emails are returned. (See normalize_span_specifiers() for
* a utility function that handles all aspects of these requirements.)
*
* The returned list is not guaranteed to be in any particular order. The position index
* (starting from low) *is* ordered, however, from oldest to newest (in terms of receipt by the
* SMTP server, not necessarily the Sent: field), so if the caller wants the latest emails,
* they should calculate low by subtracting from get_email_count() or set low to -1 and use
* count to fetch the last n emails.
*
* If any position in low to (low + count) are out of range, only the email within range are
* reported. No error is thrown. This allows callers to blindly request the first n emails
* in a folder without determining the count first.
* reported. No error is thrown. This allows callers to blindly request the first or last n
* emails in a folder without determining the count first.
*
* Note that this only returns the emails with the required fields that are available to the
* Folder's backing medium. The local store may have fewer or incomplete messages, meaning that
......@@ -163,13 +171,9 @@ public interface Geary.Folder : Object {
* and fetch from the network only what it needs, so that the caller gets a full list.
* Note that this means the call may require a round-trip to the server.
*
* TODO: Delayed listing methods (where what's available are reported via a callback after the
* async method has completed) will be implemented in the future for more responsive behavior.
* These may be operations only available from Folders returned by Engine.
*
* The Folder must be opened prior to attempting this operation.
*
* low is one-based.
* low is one-based, unless -1 is specified, as explained above.
*/
public abstract async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
......@@ -193,8 +197,7 @@ public interface Geary.Folder : Object {
* only the emails within range are reported. The list is not guaranteed to be in any
* particular order.
*
* See the notes in list_email_async() regarding issues about local versus remote stores and
* possible future additions to the API.
* See the notes in list_email_async() regarding issues about local versus remote stores.
*
* The Folder must be opened prior to attempting this operation.
*
......@@ -228,6 +231,56 @@ public interface Geary.Folder : Object {
public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields,
Cancellable? cancellable = null) throws Error;
/**
* Removes the email from the folder, determined by its EmailLocation. If the email location
* is invalid for any reason, EngineError.NOT_FOUND is thrown.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async void remove_email_async(Geary.Email email, Cancellable? cancellable = null)
throws Error;
/**
* check_span_specifiers() verifies that the span specifiers match the requirements set by
* list_email_async() and lazy_list_email_async(). If not, this method throws
* EngineError.BAD_PARAMETERS.
*/
protected static void check_span_specifiers(int low, int count) throws EngineError {
if ((low < 1 && low != -1) || (count < 0 && count != -1))
throw new EngineError.BAD_PARAMETERS("low=%d count=%d", low, count);
}
/**
* normalize_span_specifiers() deals with the varieties of span specifiers that can be passed
* to list_email_async() and lazy_list_email_async(). Note that this function is for
* implementations to convert 'low' and 'count' into positive values (1-based in the case of
* low) that are within an appropriate range.
*
* The caller should plug in 'low' and 'count' passed from the user as well as the total
* number of emails available (i.e. the complete span is 1..total).
*/
protected static void normalize_span_specifiers(ref int low, ref int count, int total)
throws EngineError {
check_span_specifiers(low, count);
if (total < 0)
throw new EngineError.BAD_PARAMETERS("total=%d", total);
// if both are -1, it's no different than low=1 count=-1 (that is, return all email)
if (low == -1 && count == -1)
low = 1;
// if count is -1, it's like a globbed star (return everything starting at low)
if (count == -1 || total == 0)
count = total;
if (low == -1)
low = (total - count).clamp(1, total);
if ((low + count - 1) > total)
count = (total - low + 1).clamp(1, total);
}
/**
* Used for debugging. Should not be used for user-visible labels.
*/
......
......@@ -8,4 +8,167 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
public GenericImapFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) {
base (remote, local, local_folder);
}
// Check if the remote folder's ordering has changed since last opened
protected override async bool prepare_opened_folder(Geary.Folder local_folder,
Geary.Folder remote_folder, Cancellable? cancellable) throws Error {
Geary.Imap.FolderProperties? local_properties =
(Geary.Imap.FolderProperties?) local_folder.get_properties();
Geary.Imap.FolderProperties? remote_properties =
(Geary.Imap.FolderProperties?) remote_folder.get_properties();
// both sets of properties must be available
if (local_properties == null) {
debug("Unable to verify UID validity for %s: missing local properties", get_path().to_string());
return false;
}
if (remote_properties == null) {
debug("Unable to verify UID validity for %s: missing remote properties", get_path().to_string());
return false;
}
// and both must have their next UID's (it's possible they don't if it's a non-selectable
// folder)
if (local_properties.uid_next == null || local_properties.uid_validity == null) {
debug("Unable to verify UID next for %s: missing local UID next or validity",
get_path().to_string());
return false;
}
if (remote_properties.uid_next == null || remote_properties.uid_validity == null) {
debug("Unable to verify UID next for %s: missing remote UID next or validity",
get_path().to_string());
return false;
}
if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
// TODO: Don't deal with UID validity changes yet
debug("UID validity changed: %lld -> %lld", local_properties.uid_validity.value,
remote_properties.uid_validity.value);
breakpoint();
}
Geary.Imap.Folder imap_remote_folder = (Geary.Imap.Folder) remote_folder;
Geary.Sqlite.Folder imap_local_folder = (Geary.Sqlite.Folder) local_folder;
// if same, no problem-o
if (local_properties.uid_next.value != remote_properties.uid_next.value) {
debug("UID next changed: %lld -> %lld", local_properties.uid_next.value,
remote_properties.uid_next.value);
// fetch everything from the last seen UID (+1) to the current next UID
// TODO: Could break this fetch up in chunks if it helps
Gee.List<Geary.Email>? newest = yield imap_remote_folder.list_email_uid_async(
local_properties.uid_next, null, Geary.Email.Field.PROPERTIES, cancellable);
if (newest != null && newest.size > 0) {
debug("saving %d newest emails", newest.size);
foreach (Geary.Email email in newest) {
try {
yield local_folder.create_email_async(email, cancellable);
} catch (Error newest_err) {
debug("Unable to save new email in %s: %s", to_string(), newest_err.message);
}
}
}
}
// fetch email from earliest email to last to (a) remove any deletions and (b) update
// any flags that may have changed
Geary.Imap.UID last_uid = new Geary.Imap.UID(local_properties.uid_next.value - 1);
Geary.Imap.UID? earliest_uid = yield imap_local_folder.get_earliest_uid_async(cancellable);
// if no earliest UID, that means no messages in local store, so nothing to update
if (earliest_uid == null || !earliest_uid.is_valid()) {
debug("No earliest UID in %s, nothing to update", to_string());
return true;
}
Gee.List<Geary.Email>? old_local = yield imap_local_folder.list_email_uid_async(earliest_uid,
last_uid, Geary.Email.Field.PROPERTIES, cancellable);
int local_length = (old_local != null) ? old_local.size : 0;
// as before, if empty folder, nothing to update
if (local_length == 0) {
debug("Folder %s empty, nothing to update", to_string());
return true;
}
Gee.List<Geary.Email>? old_remote = yield imap_remote_folder.list_email_uid_async(earliest_uid,
last_uid, Geary.Email.Field.PROPERTIES, cancellable);
int remote_length = (old_remote != null) ? old_remote.size : 0;
int remote_ctr = 0;
int local_ctr = 0;
for (;;) {
if (local_ctr >= local_length || remote_ctr >= remote_length)
break;
Geary.Imap.UID remote_uid =
((Geary.Imap.EmailLocation) old_remote[remote_ctr].location).uid;
Geary.Imap.UID local_uid =
((Geary.Imap.EmailLocation) old_local[local_ctr].location).uid;
if (remote_uid.value == local_uid.value) {
// same, update flags and move on
try {
yield imap_local_folder.update_email_async(old_remote[remote_ctr], true,
cancellable);
} catch (Error update_err) {
debug("Unable to update old email in %s: %s", to_string(), update_err.message);
}
remote_ctr++;
local_ctr++;
} else if (remote_uid.value < local_uid.value) {
// one we'd not seen before is present, add and move to next remote
try {
yield local_folder.create_email_async(old_remote[remote_ctr], cancellable);
} catch (Error add_err) {
debug("Unable to add new email to %s: %s", to_string(), add_err.message);
}
remote_ctr++;
} else {
assert(remote_uid.value > local_uid.value);
// local's email on the server has been removed, remove locally
try {
yield local_folder.remove_email_async(old_local[local_ctr], cancellable);
} catch (Error remove_err) {
debug("Unable to remove discarded email from %s: %s", to_string(),
remove_err.message);
}
local_ctr++;
}
}
// add newly-discovered emails to local store
for (; remote_ctr < remote_length; remote_ctr++) {
try {
yield local_folder.create_email_async(old_remote[remote_ctr], cancellable);
} catch (Error append_err) {
debug("Unable to append new email to %s: %s", to_string(), append_err.message);
}
}
// remove anything left over