910 lines
41 KiB
Vala
910 lines
41 KiB
Vala
/* Copyright 2011-2012 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.GenericImapFolder : Geary.AbstractFolder {
|
|
internal const int REMOTE_FETCH_CHUNK_COUNT = 50;
|
|
|
|
private const Geary.Email.Field NORMALIZATION_FIELDS = Geary.Email.Field.PROPERTIES;
|
|
|
|
internal Sqlite.Folder local_folder { get; protected set; }
|
|
internal Imap.Folder? remote_folder { get; protected set; default = null; }
|
|
internal SpecialFolder? special_folder { get; protected set; default = null; }
|
|
internal int remote_count { get; private set; default = -1; }
|
|
|
|
private weak GenericImapAccount account;
|
|
private Imap.Account remote;
|
|
private Sqlite.Account local;
|
|
private EmailFlagWatcher email_flag_watcher;
|
|
private EmailPrefetcher email_prefetcher;
|
|
private bool opened = false;
|
|
private NonblockingSemaphore remote_semaphore;
|
|
private ReplayQueue? replay_queue = null;
|
|
private NonblockingMutex normalize_email_positions_mutex = new NonblockingMutex();
|
|
|
|
public GenericImapFolder(GenericImapAccount account, Imap.Account remote, Sqlite.Account local,
|
|
Sqlite.Folder local_folder, SpecialFolder? special_folder) {
|
|
this.account = account;
|
|
this.remote = remote;
|
|
this.local = local;
|
|
this.local_folder = local_folder;
|
|
this.special_folder = special_folder;
|
|
|
|
email_flag_watcher = new EmailFlagWatcher(this);
|
|
email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
|
|
|
|
email_prefetcher = new EmailPrefetcher(this);
|
|
}
|
|
|
|
~EngineFolder() {
|
|
if (opened)
|
|
warning("Folder %s destroyed without closing", to_string());
|
|
}
|
|
|
|
public override Geary.FolderPath get_path() {
|
|
return local_folder.get_path();
|
|
}
|
|
|
|
public override Geary.SpecialFolderType? get_special_folder_type() {
|
|
if (special_folder == null) {
|
|
return null;
|
|
} else {
|
|
return special_folder.folder_type;
|
|
}
|
|
}
|
|
|
|
private Imap.FolderProperties? get_folder_properties() {
|
|
Imap.FolderProperties? properties = null;
|
|
|
|
// Get properties in order of authoritativeness:
|
|
// - Ask open remote folder
|
|
// - Query account object if it's seen them in its traversals
|
|
// - Fetch from local store
|
|
if (remote_folder != null)
|
|
properties = remote_folder.get_properties();
|
|
|
|
if (properties == null)
|
|
properties = account.get_properties_for_folder(local_folder.get_path());
|
|
|
|
if (properties == null)
|
|
properties = local_folder.get_properties();
|
|
|
|
return properties;
|
|
}
|
|
|
|
public override Geary.Trillian has_children() {
|
|
Imap.FolderProperties? properties = get_folder_properties();
|
|
|
|
return (properties != null) ? properties.has_children : Trillian.UNKNOWN;
|
|
}
|
|
|
|
public override Geary.Folder.OpenState get_open_state() {
|
|
if (!opened)
|
|
return Geary.Folder.OpenState.CLOSED;
|
|
|
|
if (local_folder.opened)
|
|
return (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL;
|
|
else if (remote_folder != null)
|
|
return Geary.Folder.OpenState.REMOTE;
|
|
|
|
// opened flag set but neither open; indicates opening state
|
|
return Geary.Folder.OpenState.OPENING;
|
|
}
|
|
|
|
public override async bool create_email_async(Geary.Email email, Cancellable? cancellable) throws Error {
|
|
check_open("create_email_async");
|
|
|
|
throw new EngineError.READONLY("Engine currently read-only");
|
|
}
|
|
|
|
private async bool normalize_folders(Geary.Imap.Folder remote_folder, Cancellable? cancellable) throws Error {
|
|
debug("normalize_folders %s", to_string());
|
|
|
|
Geary.Imap.FolderProperties? local_properties = local_folder.get_properties();
|
|
Geary.Imap.FolderProperties? remote_properties = 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 (%s) and/or validity (%s)",
|
|
get_path().to_string(), (local_properties.uid_next == null).to_string(),
|
|
(local_properties.uid_validity == null).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 (%s) and/or validity (%s)",
|
|
get_path().to_string(), (remote_properties.uid_next == null).to_string(),
|
|
(remote_properties.uid_validity == null).to_string());
|
|
|
|
return false;
|
|
}
|
|
|
|
if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
|
|
// TODO: Don't deal with UID validity changes yet
|
|
error("UID validity changed: %lld -> %lld", local_properties.uid_validity.value,
|
|
remote_properties.uid_validity.value);
|
|
}
|
|
|
|
// from here on the only write operations being performed on the folder are creating or updating
|
|
// existing emails or removing them, both operations being performed using EmailIdentifiers
|
|
// rather than positional addressing ... this means the order of operation is not important
|
|
// and can be batched up rather than performed serially
|
|
NonblockingBatch batch = new NonblockingBatch();
|
|
|
|
// fetch email from earliest email to last to (a) remove any deletions and (b) update
|
|
// any flags that may have changed
|
|
Geary.Imap.UID? earliest_uid = yield 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 normalize", to_string());
|
|
|
|
return true;
|
|
}
|
|
|
|
Geary.Imap.EmailIdentifier earliest_id = new Geary.Imap.EmailIdentifier(earliest_uid);
|
|
|
|
// Get the local emails in the range
|
|
Gee.List<Geary.Email>? old_local = yield local_folder.list_email_by_id_async(
|
|
earliest_id, int.MAX, NORMALIZATION_FIELDS, Geary.Folder.ListFlags.NONE, false,
|
|
cancellable);
|
|
|
|
// be sure they're sorted from earliest to latest
|
|
if (old_local != null)
|
|
old_local.sort(Geary.Email.compare_id_ascending);
|
|
|
|
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;
|
|
}
|
|
|
|
// Get the remote emails in the range to either add any not known, remove deleted messages,
|
|
// and update the flags of the remainder
|
|
Gee.List<Geary.Email>? old_remote = yield remote_folder.list_email_async(
|
|
new Imap.MessageSet.uid_range_to_highest(earliest_uid), NORMALIZATION_FIELDS,
|
|
cancellable);
|
|
|
|
// sort earliest to latest
|
|
if (old_remote != null)
|
|
old_remote.sort(Geary.Email.compare_id_ascending);
|
|
|
|
int remote_length = (old_remote != null) ? old_remote.size : 0;
|
|
|
|
int remote_ctr = 0;
|
|
int local_ctr = 0;
|
|
Gee.ArrayList<Geary.EmailIdentifier> appended_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
|
Gee.ArrayList<Geary.EmailIdentifier> removed_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
|
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flags_changed = new Gee.HashMap<Geary.EmailIdentifier,
|
|
Geary.EmailFlags>();
|
|
for (;;) {
|
|
if (local_ctr >= local_length || remote_ctr >= remote_length)
|
|
break;
|
|
|
|
Geary.Email remote_email = old_remote[remote_ctr];
|
|
Geary.Email local_email = old_local[local_ctr];
|
|
|
|
Geary.Imap.UID remote_uid = ((Geary.Imap.EmailIdentifier) remote_email.id).uid;
|
|
Geary.Imap.UID local_uid = ((Geary.Imap.EmailIdentifier) local_email.id).uid;
|
|
|
|
if (remote_uid.value == local_uid.value) {
|
|
// same, update flags (if changed) and move on
|
|
Geary.Imap.EmailProperties local_email_properties =
|
|
(Geary.Imap.EmailProperties) local_email.properties;
|
|
Geary.Imap.EmailProperties remote_email_properties =
|
|
(Geary.Imap.EmailProperties) remote_email.properties;
|
|
|
|
if (!local_email_properties.equals(remote_email_properties)) {
|
|
batch.add(new CreateLocalEmailOperation(local_folder, remote_email, NORMALIZATION_FIELDS));
|
|
flags_changed.set(remote_email.id, remote_email.properties.email_flags);
|
|
}
|
|
|
|
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
|
|
batch.add(new CreateLocalEmailOperation(local_folder, remote_email, NORMALIZATION_FIELDS));
|
|
appended_ids.add(remote_email.id);
|
|
|
|
remote_ctr++;
|
|
} else {
|
|
assert(remote_uid.value > local_uid.value);
|
|
|
|
// local's email on the server has been removed, remove locally
|
|
batch.add(new RemoveLocalEmailOperation(local_folder, local_email.id));
|
|
removed_ids.add(local_email.id);
|
|
|
|
local_ctr++;
|
|
}
|
|
}
|
|
|
|
// add newly-discovered emails to local store ... only report these as appended; earlier
|
|
// CreateEmailOperations were updates of emails existing previously or additions of emails
|
|
// that were on the server earlier but not stored locally (i.e. this value represents emails
|
|
// added to the top of the stack)
|
|
for (; remote_ctr < remote_length; remote_ctr++) {
|
|
batch.add(new CreateLocalEmailOperation(local_folder, old_remote[remote_ctr],
|
|
NORMALIZATION_FIELDS));
|
|
appended_ids.add(old_remote[remote_ctr].id);
|
|
}
|
|
|
|
// remove anything left over ... use local count rather than remote as we're still in a stage
|
|
// where only the local messages are available
|
|
for (; local_ctr < local_length; local_ctr++) {
|
|
batch.add(new RemoveLocalEmailOperation(local_folder, old_local[local_ctr].id));
|
|
removed_ids.add(old_local[local_ctr].id);
|
|
}
|
|
|
|
// execute them all at once
|
|
yield batch.execute_all_async(cancellable);
|
|
|
|
if (batch.get_first_exception_message() != null) {
|
|
debug("Error while preparing opened folder %s: %s", to_string(),
|
|
batch.get_first_exception_message());
|
|
}
|
|
|
|
// throw the first exception, if one occurred
|
|
batch.throw_first_exception();
|
|
|
|
// notify emails that have been removed (see note above about why not all Creates are
|
|
// signalled)
|
|
if (removed_ids.size > 0) {
|
|
debug("Notifying of %d removed emails since %s last seen", removed_ids.size, to_string());
|
|
notify_email_removed(removed_ids);
|
|
}
|
|
|
|
// notify additions
|
|
if (appended_ids.size > 0) {
|
|
debug("Notifying of %d appended emails since %s last seen", appended_ids.size, to_string());
|
|
notify_email_appended(appended_ids);
|
|
}
|
|
|
|
// notify flag changes
|
|
if (flags_changed.size > 0) {
|
|
debug("Notifying of %d changed flags since %s last seen", flags_changed.size, to_string());
|
|
notify_email_flags_changed(flags_changed);
|
|
}
|
|
|
|
debug("Completed normalize_folder %s", to_string());
|
|
|
|
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());
|
|
|
|
opened = true;
|
|
|
|
remote_semaphore = new Geary.NonblockingSemaphore();
|
|
|
|
// start the replay queue
|
|
replay_queue = new ReplayQueue(get_path().to_string());
|
|
|
|
try {
|
|
yield local_folder.open_async(readonly, cancellable);
|
|
} catch (Error err) {
|
|
notify_open_failed(OpenFailed.LOCAL_FAILED, err);
|
|
|
|
// schedule close now
|
|
close_internal_async.begin(CloseReason.LOCAL_ERROR, CloseReason.REMOTE_CLOSE, cancellable);
|
|
|
|
throw err;
|
|
}
|
|
|
|
// Rather than wait for the remote folder to open (which blocks completion of this method),
|
|
// attempt to open in the background and treat this folder as "opened". If the remote
|
|
// doesn't open, this folder remains open but only able to work with the local cache.
|
|
//
|
|
// Note that any use of remote_folder in this class should first call
|
|
// 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);
|
|
}
|
|
|
|
private async void open_remote_async(bool readonly, Cancellable? cancellable) {
|
|
try {
|
|
debug("Opening remote %s", local_folder.get_path().to_string());
|
|
Imap.Folder folder = (Imap.Folder) yield remote.fetch_folder_async(local_folder.get_path(),
|
|
cancellable);
|
|
|
|
yield folder.open_async(readonly, cancellable);
|
|
|
|
// allow subclasses to examine the opened folder and resolve any vital
|
|
// inconsistencies
|
|
if (yield normalize_folders(folder, cancellable)) {
|
|
// update flags, properties, etc.
|
|
yield local.update_folder_async(folder, cancellable);
|
|
|
|
// signals
|
|
folder.messages_appended.connect(on_remote_messages_appended);
|
|
folder.message_at_removed.connect(on_remote_message_at_removed);
|
|
folder.disconnected.connect(on_remote_disconnected);
|
|
|
|
// state
|
|
remote_count = folder.get_email_count();
|
|
|
|
// all set; bless the remote folder as opened
|
|
remote_folder = folder;
|
|
} else {
|
|
debug("Unable to prepare remote folder %s: prepare_opened_file() failed", to_string());
|
|
notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
|
|
|
|
// schedule immediate close
|
|
close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, cancellable);
|
|
|
|
return;
|
|
}
|
|
} catch (Error open_err) {
|
|
debug("Unable to open or prepare remote folder %s: %s", to_string(), open_err.message);
|
|
notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, open_err);
|
|
|
|
// schedule immediate close
|
|
close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, cancellable);
|
|
|
|
return;
|
|
}
|
|
|
|
int count;
|
|
try {
|
|
count = (remote_folder != null)
|
|
? remote_count
|
|
: yield local_folder.get_email_count_async(cancellable);
|
|
} catch (Error count_err) {
|
|
debug("Unable to fetch count from local folder: %s", count_err.message);
|
|
|
|
count = 0;
|
|
}
|
|
|
|
// 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);
|
|
|
|
remote_folder = null;
|
|
remote_count = -1;
|
|
|
|
notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, notify_err);
|
|
|
|
// schedule immediate close
|
|
close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, cancellable);
|
|
|
|
return;
|
|
}
|
|
|
|
// notify any subscribers with similar information
|
|
notify_opened(
|
|
(remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL,
|
|
count);
|
|
}
|
|
|
|
// Returns true if the remote folder is ready, false otherwise
|
|
internal async bool wait_for_remote_to_open(Cancellable? cancellable = null) throws Error {
|
|
if (remote_folder != null)
|
|
return true;
|
|
|
|
yield remote_semaphore.wait_async(cancellable);
|
|
|
|
return (remote_folder != null);
|
|
}
|
|
|
|
public override async void close_async(Cancellable? cancellable = null) throws Error {
|
|
yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, cancellable);
|
|
}
|
|
|
|
private async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason remote_reason,
|
|
Cancellable? cancellable) throws Error {
|
|
if (!opened)
|
|
return;
|
|
|
|
// set this now to avoid multiple close_async(), particularly nested inside one of the signals
|
|
// fired here
|
|
opened = false;
|
|
|
|
// Notify all callers waiting for the remote folder that it's not coming available
|
|
Imap.Folder? closing_remote_folder = remote_folder;
|
|
remote_folder = null;
|
|
remote_count = -1;
|
|
|
|
try {
|
|
remote_semaphore.notify();
|
|
} catch (Error err) {
|
|
debug("close_internal_async: Unable to fire remote semaphore: %s", err.message);
|
|
}
|
|
|
|
if (closing_remote_folder != null) {
|
|
closing_remote_folder.messages_appended.disconnect(on_remote_messages_appended);
|
|
closing_remote_folder.message_at_removed.disconnect(on_remote_message_at_removed);
|
|
closing_remote_folder.disconnected.disconnect(on_remote_disconnected);
|
|
|
|
// to avoid keeping the caller waiting while the remote end closes, close it in the
|
|
// background
|
|
//
|
|
// TODO: Problem with this is that we cannot effectively signal or report a close error,
|
|
// because by the time this operation completes the folder is considered closed. That
|
|
// may not be important to most callers, however.
|
|
if (remote_reason == CloseReason.REMOTE_CLOSE)
|
|
closing_remote_folder.close_async.begin(cancellable);
|
|
|
|
notify_closed(remote_reason);
|
|
} else {
|
|
notify_closed(Geary.Folder.CloseReason.REMOTE_CLOSE);
|
|
}
|
|
|
|
// close local store
|
|
try {
|
|
if (local_reason == CloseReason.LOCAL_CLOSE)
|
|
yield local_folder.close_async(cancellable);
|
|
|
|
notify_closed(local_reason);
|
|
} catch (Error local_err) {
|
|
debug("Error closing %s local store: %s", to_string(), local_err.message);
|
|
}
|
|
|
|
// Close the replay queues *after* the folder has been closed (in case any final upcalls
|
|
// come and can be handled)
|
|
try {
|
|
if (replay_queue != null)
|
|
yield replay_queue.close_async();
|
|
} catch (Error replay_queue_err) {
|
|
debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
|
|
}
|
|
|
|
replay_queue = null;
|
|
|
|
notify_closed(CloseReason.FOLDER_CLOSED);
|
|
}
|
|
|
|
private void on_remote_messages_appended(int total) {
|
|
debug("on_remote_messages_appended: total=%d", total);
|
|
replay_queue.schedule(new ReplayAppend(this, total));
|
|
}
|
|
|
|
// Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
|
|
// normalized placeholder in the local database of the message, so all positions are
|
|
// properly relative to the end of the message list; once this is done, notify user of new
|
|
// messages. If duplicates, create_email_async() will fall through to an updated merge,
|
|
// which is exactly what we want.
|
|
//
|
|
// This MUST only be called from ReplayAppend.
|
|
internal async void do_replay_appended_messages(int new_remote_count) {
|
|
debug("do_replay_appended_messages %s: remote_count=%d new_remote_count=%d", to_string(),
|
|
remote_count, new_remote_count);
|
|
|
|
try {
|
|
// If remote doesn't fully open, then don't fire signal, as we'll be unable to
|
|
// normalize the folder
|
|
if (!yield wait_for_remote_to_open())
|
|
return;
|
|
|
|
// normalize starting at the message *after* the highest position of the local store,
|
|
// which has now changed
|
|
Imap.MessageSet msg_set = new Imap.MessageSet.range_to_highest(remote_count + 1);
|
|
Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(
|
|
msg_set, Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, null);
|
|
if (list != null && list.size > 0) {
|
|
debug("do_replay_appended_messages: %d new messages from %s in %s", list.size,
|
|
msg_set.to_string(), to_string());
|
|
|
|
// add new messages to local store
|
|
Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>(
|
|
Hashable.hash_func, Equalable.equal_func);
|
|
Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>(
|
|
Hashable.hash_func, Equalable.equal_func);
|
|
foreach (Geary.Email email in list) {
|
|
// need to report both if it was created (not known before) and appended (which
|
|
// could mean created or simply a known email associated with this folder)
|
|
if (yield local_folder.create_email_async(email, null)) {
|
|
created.add(email.id);
|
|
} else {
|
|
debug("do_replay_appended_messages: appended email ID %s already known in account, associating with %s...",
|
|
email.id.to_string(), to_string());
|
|
}
|
|
|
|
appended.add(email.id);
|
|
}
|
|
|
|
// save new remote count
|
|
bool changed = (remote_count != new_remote_count);
|
|
remote_count = new_remote_count;
|
|
|
|
if (appended.size > 0)
|
|
notify_email_appended(appended);
|
|
|
|
if (created.size > 0)
|
|
notify_email_locally_appended(created);
|
|
|
|
if (changed)
|
|
notify_email_count_changed(remote_count, CountChangeReason.ADDED);
|
|
} else {
|
|
debug("do_replay_appended_messages: no new messages in %s in %s",
|
|
msg_set.to_string(), to_string());
|
|
}
|
|
} catch (Error err) {
|
|
debug("Unable to normalize local store of newly appended messages to %s: %s",
|
|
to_string(), err.message);
|
|
}
|
|
}
|
|
|
|
private void on_remote_message_at_removed(int position, int total) {
|
|
debug("on_remote_message_at_removed: position=%d total=%d", position, total);
|
|
replay_queue.schedule(new ReplayRemoval(this, position, total));
|
|
}
|
|
|
|
// This MUST only be called from ReplayRemoval.
|
|
internal async void do_replay_remove_message(int remote_position, int new_remote_count,
|
|
Geary.EmailIdentifier? id) {
|
|
debug("do_replay_remove_message: remote_position=%d new_remote_count=%d id=%s",
|
|
remote_position, new_remote_count, (id != null) ? id.to_string() : "(null)");
|
|
|
|
if (remote_position < 1)
|
|
assert(id != null);
|
|
else
|
|
assert(new_remote_count >= 0);
|
|
|
|
Geary.EmailIdentifier? owned_id = id;
|
|
if (owned_id == null) {
|
|
try {
|
|
owned_id = yield local_folder.id_from_remote_position(remote_position, remote_count);
|
|
} catch (Error err) {
|
|
debug("Unable to determine ID of removed message #%d from %s: %s", remote_position,
|
|
to_string(), err.message);
|
|
}
|
|
}
|
|
|
|
bool marked = false;
|
|
if (owned_id != null) {
|
|
debug("do_replay_remove_message: removing from local store Email ID %s", owned_id.to_string());
|
|
try {
|
|
// Reflect change in the local store and notify subscribers
|
|
yield local_folder.remove_marked_email_async(owned_id, out marked, null);
|
|
|
|
if (!marked)
|
|
notify_email_removed(new Geary.Singleton<Geary.EmailIdentifier>(owned_id));
|
|
} catch (Error err2) {
|
|
debug("Unable to remove message #%d from %s: %s", remote_position, to_string(),
|
|
err2.message);
|
|
}
|
|
}
|
|
|
|
// save new remote count and notify of change
|
|
bool changed = (remote_count != new_remote_count);
|
|
remote_count = new_remote_count;
|
|
|
|
if (!marked && changed)
|
|
notify_email_count_changed(remote_count, CountChangeReason.REMOVED);
|
|
}
|
|
|
|
private void on_remote_disconnected(Geary.Folder.CloseReason reason) {
|
|
debug("on_remote_disconnected: reason=%s", reason.to_string());
|
|
replay_queue.schedule(new ReplayDisconnect(this, reason));
|
|
}
|
|
|
|
internal async void do_replay_remote_disconnected(Geary.Folder.CloseReason reason) {
|
|
debug("do_replay_remote_disconnected reason=%s", reason.to_string());
|
|
assert(reason == CloseReason.REMOTE_CLOSE || reason == CloseReason.REMOTE_ERROR);
|
|
|
|
// because close_internal_async() issues ReceiveReplayQueue.close_async() (which cannot
|
|
// be called from within a ReceiveReplayOperation), schedule the close rather than
|
|
// yield for it ... can't simply call the async .begin variant because, depending on
|
|
// the situation, it may not yield until it attempts to close the ReceiveReplayQueue,
|
|
// which is the problem we're attempting to work around
|
|
Idle.add(() => {
|
|
close_internal_async.begin(CloseReason.LOCAL_CLOSE, reason, null);
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
public override async int get_email_count_async(Cancellable? cancellable = null) throws Error {
|
|
check_open("get_email_count_async");
|
|
|
|
// if connected or connecting, use stashed remote count (which is always kept current once
|
|
// remote folder is opened)
|
|
if (opened) {
|
|
if (yield wait_for_remote_to_open(cancellable))
|
|
return remote_count;
|
|
}
|
|
|
|
return yield local_folder.get_email_count_async(cancellable);
|
|
}
|
|
|
|
public override async Gee.List<Geary.Email>? list_email_async(int low, int count,
|
|
Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null)
|
|
throws Error {
|
|
if (count == 0)
|
|
return null;
|
|
|
|
// block on do_list_email_async(), using an accumulator to gather the emails and return
|
|
// them all at once to the caller
|
|
Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
|
|
yield do_list_email_async("list_email_async", low, count, required_fields, flags, accumulator,
|
|
null, cancellable);
|
|
|
|
return accumulator;
|
|
}
|
|
|
|
// TODO: Capture Error and report via EmailCallback.
|
|
public override void lazy_list_email(int low, int count, Geary.Email.Field required_fields,
|
|
Geary.Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) {
|
|
// schedule do_list_email_async(), using the callback to drive availability of email
|
|
do_list_email_async.begin("lazy_list_email", low, count, required_fields, flags, null, cb,
|
|
cancellable);
|
|
}
|
|
|
|
private async void do_list_email_async(string method, int low, int count, Geary.Email.Field required_fields,
|
|
Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator, EmailCallback? cb,
|
|
Cancellable? cancellable) throws Error {
|
|
check_open(method);
|
|
check_flags(method, flags);
|
|
check_span_specifiers(low, count);
|
|
|
|
if (count == 0) {
|
|
// signal finished
|
|
if (cb != null)
|
|
cb(null, null);
|
|
|
|
return;
|
|
}
|
|
|
|
// Schedule list operation and wait for completion.
|
|
ListEmail op = new ListEmail(this, low, count, required_fields, flags, accumulator, cb,
|
|
cancellable);
|
|
replay_queue.schedule(op);
|
|
|
|
yield op.wait_for_ready_async(cancellable);
|
|
}
|
|
|
|
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier initial_id,
|
|
int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
|
|
Cancellable? cancellable = null) throws Error {
|
|
Gee.List<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
|
|
yield do_list_email_by_id_async("list_email_by_id_async", initial_id, count, required_fields,
|
|
flags, list, null, cancellable);
|
|
|
|
return (list.size > 0) ? list : null;
|
|
}
|
|
|
|
// TODO: Capture Error and report via EmailCallback.
|
|
public override void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
|
|
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
|
|
Cancellable? cancellable = null) {
|
|
do_lazy_list_email_by_id_async.begin("lazy_list_email_by_id", initial_id, count, required_fields,
|
|
flags, cb, cancellable);
|
|
}
|
|
|
|
private async void do_lazy_list_email_by_id_async(string method, Geary.EmailIdentifier initial_id,
|
|
int count, Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
|
|
Cancellable? cancellable) {
|
|
try {
|
|
yield do_list_email_by_id_async(method, initial_id, count, required_fields, flags, null, cb,
|
|
cancellable);
|
|
} catch (Error err) {
|
|
cb(null, err);
|
|
}
|
|
}
|
|
|
|
private async void do_list_email_by_id_async(string method, Geary.EmailIdentifier initial_id, int count,
|
|
Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
|
|
EmailCallback? cb, Cancellable? cancellable) throws Error {
|
|
check_open(method);
|
|
check_flags(method, flags);
|
|
|
|
// listing by ID requires the remote to be open and fully synchronized, as there's no
|
|
// reliable way to determine certain counts and positions without it
|
|
//
|
|
// TODO: Need to deal with this in a sane manner when offline
|
|
if (!yield wait_for_remote_to_open(cancellable))
|
|
throw new EngineError.SERVER_UNAVAILABLE("Must be synchronized with server for listing by ID");
|
|
assert(remote_count >= 0);
|
|
|
|
// Schedule list operation and wait for completion.
|
|
ListEmailByID op = new ListEmailByID(this, initial_id, count, required_fields, flags, accumulator,
|
|
cb, cancellable);
|
|
replay_queue.schedule(op);
|
|
|
|
yield op.wait_for_ready_async(cancellable);
|
|
}
|
|
|
|
public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
|
|
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
|
|
check_open("list_local_email_fields_async");
|
|
|
|
return yield local_folder.list_email_fields_by_id_async(ids, cancellable);
|
|
}
|
|
|
|
public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
|
|
Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
|
|
throws Error {
|
|
check_open("fetch_email_async");
|
|
check_flags("fetch_email_async", flags);
|
|
|
|
FetchEmail op = new FetchEmail(this, id, required_fields, flags, cancellable);
|
|
replay_queue.schedule(op);
|
|
|
|
yield op.wait_for_ready_async(cancellable);
|
|
|
|
if (op.email == null) {
|
|
throw new EngineError.NOT_FOUND("Email %s not found in %s", id.to_string(), to_string());
|
|
} else if (!op.email.fields.fulfills(required_fields)) {
|
|
throw new EngineError.INCOMPLETE_MESSAGE("Email %s in %s does not fulfill required fields %Xh (has %Xh)",
|
|
id.to_string(), to_string(), required_fields, op.email.fields);
|
|
}
|
|
|
|
return op.email;
|
|
}
|
|
|
|
public override async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
|
|
Cancellable? cancellable = null) throws Error {
|
|
check_open("remove_email_async");
|
|
|
|
replay_queue.schedule(new RemoveEmail(this, email_ids, cancellable));
|
|
}
|
|
|
|
private void check_open(string method) throws EngineError {
|
|
if (!opened)
|
|
throw new EngineError.OPEN_REQUIRED("%s failed: folder %s is not open", method, to_string());
|
|
}
|
|
|
|
private void check_flags(string method, Folder.ListFlags flags) throws EngineError {
|
|
if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY) && flags.is_all_set(Folder.ListFlags.FORCE_UPDATE)) {
|
|
throw new EngineError.BAD_PARAMETERS("%s %s failed: LOCAL_ONLY and FORCE_UPDATE are mutually exclusive",
|
|
to_string(), method);
|
|
}
|
|
}
|
|
|
|
// Converts a remote position to a local position, assuming that the remote has been completely
|
|
// opened. local_count must be supplied because that's not held by EngineFolder (unlike
|
|
// remote_count).
|
|
//
|
|
// Returns a negative value if not available in local folder or remote is not open yet.
|
|
internal int remote_position_to_local_position(int remote_pos, int local_count) {
|
|
return (remote_count >= 0) ? remote_pos - (remote_count - local_count) : -1;
|
|
}
|
|
|
|
// Converts a local position to a remote position, assuming that the remote has been completely
|
|
// opened. See remote_position_to_local_position for more caveats.
|
|
//
|
|
// Returns a negative value if remote is not open.
|
|
internal int local_position_to_remote_position(int local_pos, int local_count) {
|
|
return (remote_count >= 0) ? remote_count - (local_count - local_pos) : -1;
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Note that this method doesn't return a remote_count because that's maintained by the
|
|
// EngineFolder as a member variable.
|
|
internal async void normalize_email_positions_async(int low, int count, out int local_count,
|
|
Cancellable? cancellable) throws Error {
|
|
if (!yield wait_for_remote_to_open(cancellable)) {
|
|
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
|
|
}
|
|
|
|
int mutex_token = yield normalize_email_positions_mutex.claim_async(cancellable);
|
|
|
|
Error? error = null;
|
|
try {
|
|
local_count = yield local_folder.get_email_count_async(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 high = (low + (count - 1)).clamp(1, remote_count);
|
|
int local_low = (local_count > 0) ? (remote_count - local_count) + 1 : remote_count;
|
|
if (high >= local_low) {
|
|
normalize_email_positions_mutex.release(ref mutex_token);
|
|
return;
|
|
}
|
|
|
|
int prefetch_count = local_low - high;
|
|
|
|
debug("prefetching %d (%d) for %s (local_low=%d)", high, prefetch_count, to_string(),
|
|
local_low);
|
|
|
|
// Normalize the local folder by fetching EmailIdentifiers for all missing email as well
|
|
// as fields for duplicate detection
|
|
Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(
|
|
new Imap.MessageSet.range(high, prefetch_count),
|
|
Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, 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());
|
|
}
|
|
|
|
NonblockingBatch batch = new NonblockingBatch();
|
|
|
|
foreach (Geary.Email email in list) {
|
|
batch.add(new CreateLocalEmailOperation(local_folder, email,
|
|
Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION));
|
|
}
|
|
|
|
yield batch.execute_all_async(cancellable);
|
|
batch.throw_first_exception();
|
|
|
|
// Collect which EmailIdentifiers were created and report them
|
|
Gee.HashSet<Geary.EmailIdentifier> created_ids = new Gee.HashSet<Geary.EmailIdentifier>(
|
|
Hashable.hash_func, Equalable.equal_func);
|
|
foreach (int id in batch.get_ids()) {
|
|
CreateLocalEmailOperation? op = batch.get_operation(id) as CreateLocalEmailOperation;
|
|
if (op != null && op.created)
|
|
created_ids.add(op.email.id);
|
|
}
|
|
|
|
if (created_ids.size > 0)
|
|
notify_email_locally_appended(created_ids);
|
|
} catch (Error e) {
|
|
local_count = 0; // prevent compiler warning
|
|
error = e;
|
|
}
|
|
|
|
normalize_email_positions_mutex.release(ref mutex_token);
|
|
|
|
if (error != null)
|
|
throw error;
|
|
}
|
|
|
|
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
|
|
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
|
|
Cancellable? cancellable = null) throws Error {
|
|
check_open("mark_email_async");
|
|
if (!yield wait_for_remote_to_open(cancellable))
|
|
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
|
|
|
|
replay_queue.schedule(new MarkEmail(this, to_mark, flags_to_add, flags_to_remove,
|
|
cancellable));
|
|
}
|
|
|
|
public override async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
|
|
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
|
|
check_open("copy_email_async");
|
|
if (!yield wait_for_remote_to_open(cancellable))
|
|
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
|
|
|
|
replay_queue.schedule(new CopyEmail(this, to_copy, destination));
|
|
}
|
|
|
|
public override async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
|
|
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
|
|
check_open("move_email_async");
|
|
if (!yield wait_for_remote_to_open(cancellable))
|
|
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
|
|
|
|
replay_queue.schedule(new MoveEmail(this, to_move, destination));
|
|
}
|
|
|
|
private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
|
|
notify_email_flags_changed(changed);
|
|
}
|
|
}
|
|
|