geary/src/engine/imap-engine/imap-engine-generic-account.vala
2020-01-21 17:57:33 +01:00

1402 lines
53 KiB
Vala

/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2017-2019 Michael Gratton <mike@vee.net>.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/** Default IMAP session pool size. */
private const int IMAP_MIN_POOL_SIZE = 2;
// This is high since it's an expensive operation, and we'll go
// looking changes caused by local operations as they happen, so
// we don't need to double check.
private const int REFRESH_FOLDER_LIST_SEC = 15 * 60;
/** Minimum interval between account storage cleanup work */
private const uint APP_BACKGROUNDED_CLEANUP_WORK_INTERVAL_MINUTES = 60 * 24;
private const Geary.SpecialFolderType[] SUPPORTED_SPECIAL_FOLDERS = {
Geary.SpecialFolderType.DRAFTS,
Geary.SpecialFolderType.SENT,
Geary.SpecialFolderType.SPAM,
Geary.SpecialFolderType.TRASH,
Geary.SpecialFolderType.ARCHIVE,
};
private static GLib.VariantType email_id_type = new GLib.VariantType(
EmailIdentifier.BASE_VARIANT_TYPE
);
/** Service for incoming IMAP connections. */
public Imap.ClientService imap { get; private set; }
/** Service for outgoing SMTP connections. */
public Smtp.ClientService smtp { get; private set; }
/** Local database for the account. */
public ImapDB.Account local { get; private set; }
public signal void old_messages_background_cleanup_request(GLib.Cancellable? cancellable);
private bool open = false;
private Cancellable? open_cancellable = null;
private Nonblocking.Semaphore? remote_ready_lock = null;
private Gee.Map<FolderPath,MinimalFolder> folder_map =
new Gee.HashMap<FolderPath,MinimalFolder>();
private AccountProcessor? processor;
private AccountSynchronizer sync;
private TimeoutManager refresh_folder_timer;
private Gee.Map<Geary.SpecialFolderType, Gee.List<string>> special_search_names =
new Gee.HashMap<Geary.SpecialFolderType, Gee.List<string>>();
protected GenericAccount(AccountInformation config,
ImapDB.Account local,
Endpoint incoming_remote,
Endpoint outgoing_remote) {
Imap.ClientService imap = new Imap.ClientService(
config,
config.incoming,
incoming_remote
);
Smtp.ClientService smtp = new Smtp.ClientService(
config,
config.outgoing,
outgoing_remote
);
base(config, imap, smtp);
this.local = local;
this.contact_store = new ContactStoreImpl(local.db);
imap.min_pool_size = IMAP_MIN_POOL_SIZE;
imap.notify["current-status"].connect(
on_imap_status_notify
);
imap.set_logging_parent(this);
this.imap = imap;
smtp.outbox = new Outbox.Folder(this, local_folder_root, local);
smtp.report_problem.connect(notify_report_problem);
smtp.set_logging_parent(this);
this.smtp = smtp;
this.sync = new AccountSynchronizer(this);
this.refresh_folder_timer = new TimeoutManager.seconds(
REFRESH_FOLDER_LIST_SEC,
() => { this.update_remote_folders(); }
);
this.background_progress = new ReentrantProgressMonitor(ACTIVITY);
this.db_upgrade_monitor = local.upgrade_monitor;
this.db_vacuum_monitor = local.vacuum_monitor;
compile_special_search_names();
}
/** {@inheritDoc} */
public override async void open_async(Cancellable? cancellable = null) throws Error {
if (open)
throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string());
this.background_progress.notify_start();
try {
yield internal_open_async(cancellable);
} finally {
this.background_progress.notify_finish();
}
}
private async void internal_open_async(Cancellable? cancellable) throws Error {
this.open_cancellable = new Cancellable();
this.remote_ready_lock = new Nonblocking.Semaphore(this.open_cancellable);
this.processor = new AccountProcessor(this.background_progress);
this.processor.operation_error.connect(on_operation_error);
this.processor.set_logging_parent(this);
try {
yield this.local.open_async(cancellable);
} catch (Error err) {
// convert database-open errors
if (err is DatabaseError.CORRUPT)
throw new EngineError.CORRUPT("%s", err.message);
else if (err is DatabaseError.ACCESS)
throw new EngineError.PERMISSIONS("%s", err.message);
else if (err is DatabaseError.SCHEMA_VERSION)
throw new EngineError.VERSION("%s", err.message);
else
throw err;
}
this.last_storage_cleanup = yield this.local.fetch_last_cleanup_async(cancellable);
this.last_storage_cleanup_changed.connect ((dt) => {
this.local.set_last_cleanup_async.begin(dt, cancellable);
});
this.open = true;
notify_opened();
this.queue_operation(
new LoadFolders(this, this.local, get_supported_special_folders())
);
// Start the mail services. Start incoming directly, but queue
// outgoing so local folders can be loaded first in case
// queued mail gets sent and needs to get saved somewhere.
yield this.imap.start(cancellable);
this.queue_operation(new StartPostie(this));
// Kick off a background update of the search table.
//
// XXX since this hammers the database, this is an example of
// an operation for which we need an engine-wide operation
// queue, not just an account-wide queue.
this.queue_operation(new PopulateSearchTable(this));
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
if (!open)
return;
// Stop attempting to send any outgoing messages
try {
yield this.smtp.stop();
} catch (Error err) {
debug("Error stopping SMTP service: %s", err.message);
}
// Halt internal tasks early so they stop using local and
// remote connections.
this.refresh_folder_timer.reset();
this.open_cancellable.cancel();
this.processor.stop();
// Block obtaining and reusing IMAP connections. This *must*
// happen after internal tasks above are cancelled otherwise
// they may block while waiting/using a remote session.
this.imap.discard_returned_sessions = true;
this.remote_ready_lock.reset();
// Close folders and ensure they do in fact close
Gee.BidirSortedSet<Folder> remotes = sort_by_path(this.folder_map.values);
this.folder_map.clear();
notify_folders_available_unavailable(null, remotes);
foreach (Geary.Folder folder in remotes) {
debug("Waiting for remote to close: %s", folder.to_string());
yield folder.wait_for_close_async();
}
// Close IMAP service manager now that folders are closed
try {
yield this.imap.stop();
} catch (Error err) {
debug("Error stopping IMAP service: %s", err.message);
}
this.remote_ready_lock = null;
// Close local infrastructure
try {
yield local.close_async(cancellable);
} finally {
this.open = false;
notify_closed();
}
}
/** {@inheritDoc} */
public override bool is_open() {
return open;
}
public override async void rebuild_async(GLib.Cancellable? cancellable = null)
throws GLib.Error {
if (this.open) {
throw new EngineError.ALREADY_OPEN(
"Account cannot be open during rebuild"
);
}
message("Rebuilding account local data");
yield this.local.delete_all_data(cancellable);
message("Rebuild complete");
}
/**
* Queues an operation for execution by this account.
*
* The operation will added to the account's {@link
* AccountProcessor} and executed asynchronously by that when it
* reaches the front.
*/
public void queue_operation(AccountOperation op)
throws EngineError {
check_open();
debug("Enqueuing operation: %s", op.to_string());
this.processor.enqueue(op);
}
/**
* Claims a new IMAP account session from the pool.
*
* A new IMAP client session will be retrieved from the pool,
* connecting if needed, and used for a new account session. This
* call will wait until the pool is ready to provide sessions. The
* session must be returned via {@link release_account_session}
* after use.
*
* The account must have been opened before calling this method.
*/
public async Imap.AccountSession claim_account_session(Cancellable? cancellable = null)
throws Error {
check_open();
debug("Acquiring account session");
yield this.remote_ready_lock.wait_async(cancellable);
var client = yield this.imap.claim_authorized_session_async(cancellable);
var session = new Imap.AccountSession(this.local.imap_folder_root, client);
session.set_logging_parent(this.imap);
return session;
}
/**
* Returns an IMAP account session to the pool for re-use.
*/
public void release_account_session(Imap.AccountSession session) {
debug("Releasing account session");
Imap.ClientSession? old_session = session.close();
if (old_session != null) {
this.imap.release_session_async.begin(
old_session,
(obj, res) => {
try {
this.imap.release_session_async.end(res);
} catch (Error err) {
debug(
"Error releasing account session: %s", err.message
);
}
}
);
}
}
/**
* Claims a new IMAP folder session from the pool.
*
* A new IMAP client session will be retrieved from the pool,
* connecting if needed, and used for a new folder session. This
* call will wait until the pool is ready to provide sessions. The
* session must be returned via {@link release_folder_session}
* after use.
*
* The account must have been opened before calling this method.
*/
public async Imap.FolderSession claim_folder_session(Geary.FolderPath path,
Cancellable cancellable)
throws Error {
check_open();
debug("Acquiring folder session for: %s", path.to_string());
yield this.remote_ready_lock.wait_async(cancellable);
// We manually construct an account session here and then
// reuse it for the folder session so we only need to claim as
// single session from the pool, not two.
Imap.ClientSession? client =
yield this.imap.claim_authorized_session_async(cancellable);
Imap.AccountSession account = new Imap.AccountSession(
this.local.imap_folder_root, client
);
account.set_logging_parent(this.imap);
Imap.Folder? folder = null;
GLib.Error? folder_err = null;
try {
folder = yield account.fetch_folder_async(path, cancellable);
} catch (Error err) {
folder_err = err;
}
account.close();
Imap.FolderSession? folder_session = null;
if (folder_err == null) {
try {
folder_session = yield new Imap.FolderSession(
client, folder, cancellable
);
folder_session.set_logging_parent(this.imap);
} catch (Error err) {
folder_err = err;
}
}
if (folder_err != null) {
try {
yield this.imap.release_session_async(client);
} catch (Error release_err) {
debug("Error releasing folder session: %s", release_err.message);
}
throw folder_err;
}
return folder_session;
}
/**
* Returns an IMAP folder session to the pool for cleanup and re-use.
*/
public async void release_folder_session(Imap.FolderSession session) {
debug("Releasing folder session");
Imap.ClientSession? old_session = session.close();
if (old_session != null) {
try {
yield this.imap.release_session_async(old_session);
} catch (Error err) {
debug("Error releasing %s session: %s",
session.folder.path.to_string(),
err.message);
}
}
}
/** {@inheritDoc} */
public override EmailIdentifier to_email_identifier(GLib.Variant serialised)
throws EngineError.BAD_PARAMETERS {
if (!serialised.is_of_type(GenericAccount.email_id_type)) {
throw new EngineError.BAD_PARAMETERS("Invalid outer serialised type");
}
char type = (char) serialised.get_child_value(0).get_byte();
if (type == 'i')
return new ImapDB.EmailIdentifier.from_variant(serialised);
if (type == 'o')
return new Outbox.EmailIdentifier.from_variant(serialised);
throw new EngineError.BAD_PARAMETERS("Unknown serialised type: %c", type);
}
/** {@inheritDoc} */
public override FolderPath to_folder_path(GLib.Variant serialised)
throws EngineError.BAD_PARAMETERS {
FolderPath? path = null;
try {
path = this.local.imap_folder_root.from_variant(serialised);
} catch (EngineError.BAD_PARAMETERS err) {
path = this.local_folder_root.from_variant(serialised);
}
return path;
}
/** {@inheritDoc} */
public override Folder get_folder(FolderPath path)
throws EngineError.NOT_FOUND {
Folder? folder = this.folder_map.get(path);
if (folder == null) {
throw new EngineError.NOT_FOUND(
"Folder not found: %s", path.to_string()
);
}
return folder;
}
/** {@inheritDoc} */
public override Gee.Collection<Folder> list_folders() {
var all = new Gee.HashSet<Folder>();
all.add_all(this.folder_map.values);
return all;
}
/** {@inheritDoc} */
public override Gee.Collection<Folder> list_matching_folders(FolderPath? parent)
throws EngineError.NOT_FOUND {
return traverse<FolderPath>(folder_map.keys)
.filter(p => {
FolderPath? path_parent = p.parent;
return ((parent == null && path_parent == null) ||
(parent != null && path_parent != null &&
path_parent.equal_to(parent)));
})
.map<Geary.Folder>(p => folder_map.get(p))
.to_array_list();
}
public override async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
Cancellable? cancellable) throws Error {
if (!(special in get_supported_special_folders())) {
throw new EngineError.BAD_PARAMETERS(
"Invalid special folder type %s passed to get_required_special_folder_async",
special.to_string());
}
check_open();
Geary.Folder? folder = get_special_folder(special);
if (folder == null) {
Imap.AccountSession account = yield claim_account_session();
try {
folder = yield ensure_special_folder_async(account, special, cancellable);
} finally {
release_account_session(account);
}
}
return folder;
}
private ImapDB.EmailIdentifier check_id(Geary.EmailIdentifier id) throws EngineError {
ImapDB.EmailIdentifier? imapdb_id = id as ImapDB.EmailIdentifier;
if (imapdb_id == null)
throw new EngineError.BAD_PARAMETERS("EmailIdentifier %s not from ImapDB folder", id.to_string());
return imapdb_id;
}
private Gee.Collection<ImapDB.EmailIdentifier> check_ids(Gee.Collection<Geary.EmailIdentifier> ids)
throws EngineError {
foreach (Geary.EmailIdentifier id in ids) {
if (!(id is ImapDB.EmailIdentifier))
throw new EngineError.BAD_PARAMETERS("EmailIdentifier %s not from ImapDB folder", id.to_string());
}
return (Gee.Collection<ImapDB.EmailIdentifier>) ids;
}
/** {@inheritDoc} */
public override async SearchQuery new_search_query(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error {
return yield new ImapDB.SearchQuery(
this, local, query, strategy, cancellable
);
}
public override async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_search_message_id_async(
Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok,
Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? flag_blacklist,
Cancellable? cancellable = null) throws Error {
return yield local.search_message_id_async(
message_id, requested_fields, partial_ok, folder_blacklist, flag_blacklist, cancellable);
}
public override async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
return yield local.fetch_email_async(check_id(email_id), required_fields, cancellable);
}
/** {@inheritDoc} */
public override async Gee.List<Email> list_local_email_async(
Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null
) throws GLib.Error {
return yield local.list_email(
check_ids(ids), required_fields, cancellable
);
}
public override async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error {
if (offset < 0)
throw new EngineError.BAD_PARAMETERS("Offset must not be negative");
return yield local.search_async(query, limit, offset, folder_blacklist, search_ids, cancellable);
}
public override async Gee.Set<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
return yield local.get_search_matches_async(query, check_ids(ids), cancellable);
}
public override async Gee.MultiMap<EmailIdentifier,FolderPath>?
get_containing_folders_async(Gee.Collection<Geary.EmailIdentifier> ids,
GLib.Cancellable? cancellable)
throws GLib.Error {
Gee.MultiMap<EmailIdentifier,FolderPath> map =
new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
yield this.local.get_containing_folders_async(ids, map, cancellable);
yield this.smtp.outbox.add_to_containing_folders_async(ids, map, cancellable);
return (map.size == 0) ? null : map;
}
/** {@inheritDoc} */
public override async void cleanup_storage(GLib.Cancellable? cancellable) {
debug("Backgrounded storage cleanup check for %s account", this.information.display_name);
DateTime now = new DateTime.now_local();
DateTime? last_cleanup = this.last_storage_cleanup;
if (last_cleanup == null ||
(now.difference(last_cleanup) / TimeSpan.MINUTE > APP_BACKGROUNDED_CLEANUP_WORK_INTERVAL_MINUTES)) {
// Interval check is OK, start by detaching old messages
this.last_storage_cleanup = now;
this.old_messages_background_cleanup_request(cancellable);
} else if (local.db.want_background_vacuum) {
// Vacuum has been flagged as needed, run it
local.db.run_gc.begin(cancellable, false, true, this.imap, this.smtp);
}
}
/**
* Constructs a set of folders and adds them to the account.
*
* This constructs a high-level folder representation for each
* folder, adds them to this account object, fires the appropriate
* signals, then returns them. Both the local and remote folder
* equivalents need to exist beforehand — they are not created.
*
* If `are_existing` is true, the folders are assumed to have been
* seen before and the {@link Geary.Account.folders_created} signal is
* not fired.
*/
internal Gee.Collection<Folder> add_folders(Gee.Collection<ImapDB.Folder> db_folders,
bool are_existing) {
Gee.TreeSet<MinimalFolder> built_folders = new Gee.TreeSet<MinimalFolder>(
Account.folder_path_comparator
);
foreach(ImapDB.Folder db_folder in db_folders) {
if (!this.folder_map.has_key(db_folder.get_path())) {
MinimalFolder folder = new_folder(db_folder);
folder.report_problem.connect(notify_report_problem);
built_folders.add(folder);
this.folder_map.set(folder.path, folder);
}
}
if (!built_folders.is_empty) {
notify_folders_available_unavailable(built_folders, null);
if (!are_existing) {
notify_folders_created(built_folders);
}
}
return built_folders;
}
/**
* Fires appropriate signals for a single altered folder.
*
* This is functionally equivalent to {@link update_folders}.
*/
internal void update_folder(Geary.Folder folder) {
Gee.Collection<Geary.Folder> folders =
new Gee.LinkedList<Geary.Folder>();
folders.add(folder);
debug("Folder updated: %s", folder.path.to_string());
notify_folders_contents_altered(folders);
}
/**
* Fires appropriate signals for folders have been altered.
*
* This is functionally equivalent to {@link update_folder}.
*/
internal void update_folders(Gee.Collection<Geary.Folder> folders) {
if (!folders.is_empty) {
notify_folders_contents_altered(sort_by_path(folders));
}
}
/**
* Marks a folder as a specific special folder type.
*/
internal void promote_folders(Gee.Map<Geary.SpecialFolderType,Geary.Folder> specials) {
Gee.Set<Geary.Folder> changed = new Gee.HashSet<Geary.Folder>();
foreach (Geary.SpecialFolderType special in specials.keys) {
MinimalFolder? minimal = specials.get(special) as MinimalFolder;
if (minimal.special_folder_type != special) {
debug("Promoting %s to %s",
minimal.to_string(), special.to_string());
minimal.set_special_folder_type(special);
changed.add(minimal);
MinimalFolder? existing =
get_special_folder(special) as MinimalFolder;
if (existing != null && existing != minimal) {
existing.set_special_folder_type(SpecialFolderType.NONE);
changed.add(existing);
}
}
}
if (!changed.is_empty) {
folders_special_type(changed);
}
}
/**
* Removes a set of folders from the account.
*
* This removes the high-level folder representations from this
* account object, and fires the appropriate signals. Deletion of
* both the local and remote folder equivalents must be handled
* before, then after calling this method.
*
* A collection of folders that was actually removed is returned.
*/
internal Gee.BidirSortedSet<MinimalFolder>
remove_folders(Gee.Collection<Folder> folders) {
Gee.TreeSet<MinimalFolder> removed = new Gee.TreeSet<MinimalFolder>(
Account.folder_path_comparator
);
foreach(Geary.Folder folder in folders) {
MinimalFolder? impl = this.folder_map.get(folder.path);
if (impl != null) {
this.folder_map.unset(folder.path);
removed.add(impl);
}
}
if (!removed.is_empty) {
notify_folders_available_unavailable(null, removed);
notify_folders_deleted(removed);
}
return removed;
}
/**
* Locates a special folder, creating it if needed.
*/
internal async Folder
ensure_special_folder_async(Imap.AccountSession remote,
SpecialFolderType type,
GLib.Cancellable? cancellable)
throws GLib.Error {
Folder? special = get_special_folder(type);
if (special == null) {
FolderPath? path = information.get_special_folder_path(type);
if (path != null) {
if (!remote.is_folder_path_valid(path)) {
warning(
"Ignoring bad special folder path '%s' for type %s",
path.to_string(),
type.to_string()
);
path = null;
} else {
path = this.local.imap_folder_root.copy(path);
}
}
if (path == null) {
FolderPath root =
yield remote.get_default_personal_namespace(cancellable);
Gee.List<string> search_names = special_search_names.get(type);
foreach (string search_name in search_names) {
FolderPath search_path = root.get_child(search_name);
foreach (FolderPath test_path in folder_map.keys) {
if (test_path.compare_normalized_ci(search_path) == 0) {
path = search_path;
break;
}
}
if (path != null)
break;
}
if (path == null) {
path = root.get_child(search_names[0]);
}
debug("Guessed folder \'%s\' for special_path %s",
path.to_string(), type.to_string()
);
information.set_special_folder_path(type, path);
}
if (!this.folder_map.has_key(path)) {
debug("Creating \"%s\" to use as special folder %s",
path.to_string(), type.to_string());
GLib.Error? created_err = null;
try {
yield remote.create_folder_async(path, type, cancellable);
} catch (GLib.Error err) {
// Hang on to the error since the folder might exist
// on the remote, so try fetching it anyway.
created_err = err;
}
Imap.Folder? remote_folder = null;
try {
remote_folder = yield remote.fetch_folder_async(
path, cancellable
);
} catch (GLib.Error err) {
// If we couldn't fetch it after also failing to
// create it, it's probably due to the problem
// creating it, so throw that error instead.
if (created_err != null) {
throw created_err;
} else {
throw err;
}
}
ImapDB.Folder local_folder =
yield this.local.clone_folder_async(
remote_folder, cancellable
);
add_folders(
Collection.single(local_folder), created_err != null
);
}
special= this.folder_map.get(path);
promote_folders(
Collection.single_map<SpecialFolderType,Folder>(
type, special
)
);
}
return special;
}
/**
* Constructs a concrete folder implementation.
*
* Subclasses should implement this to return their flavor of a
* MinimalFolder with the appropriate interfaces attached. The
* returned folder should have its SpecialFolderType set using
* either the properties from the local folder or its path.
*
* This won't be called to build the Outbox or search folder, but
* for all others (including Inbox) it will.
*/
protected abstract MinimalFolder new_folder(ImapDB.Folder local_folder);
/** {@inheritDoc} */
protected override void
notify_folders_available_unavailable(Gee.BidirSortedSet<Folder>? available,
Gee.BidirSortedSet<Folder>? unavailable) {
base.notify_folders_available_unavailable(available, unavailable);
if (available != null) {
foreach (Geary.Folder folder in available) {
folder.email_appended.connect(notify_email_appended);
folder.email_inserted.connect(notify_email_inserted);
folder.email_removed.connect(notify_email_removed);
folder.email_locally_removed.connect(notify_email_locally_removed);
folder.email_locally_complete.connect(notify_email_locally_complete);
folder.email_flags_changed.connect(notify_email_flags_changed);
}
}
if (unavailable != null) {
foreach (Geary.Folder folder in unavailable) {
folder.email_appended.disconnect(notify_email_appended);
folder.email_inserted.disconnect(notify_email_inserted);
folder.email_removed.disconnect(notify_email_removed);
folder.email_locally_removed.disconnect(notify_email_locally_removed);
folder.email_locally_complete.disconnect(notify_email_locally_complete);
folder.email_flags_changed.disconnect(notify_email_flags_changed);
}
}
}
/** {@inheritDoc} */
protected override void notify_email_appended(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
base.notify_email_appended(folder, ids);
schedule_unseen_update(folder);
}
/** {@inheritDoc} */
protected override void notify_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
base.notify_email_inserted(folder, ids);
schedule_unseen_update(folder);
}
/** {@inheritDoc} */
protected override void notify_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
base.notify_email_removed(folder, ids);
schedule_unseen_update(folder);
}
/** {@inheritDoc} */
protected override void notify_email_locally_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
base.notify_email_locally_removed(folder, ids);
}
/** {@inheritDoc} */
protected override void notify_email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map) {
base.notify_email_flags_changed(folder, flag_map);
schedule_unseen_update(folder);
}
/**
* Hooks up and queues an {@link UpdateRemoteFolders} operation.
*/
private void update_remote_folders() {
this.refresh_folder_timer.reset();
UpdateRemoteFolders op = new UpdateRemoteFolders(
this,
get_supported_special_folders()
);
op.completed.connect(() => {
this.refresh_folder_timer.start();
});
try {
queue_operation(op);
} catch (Error err) {
// oh well
}
}
/**
* Hooks up and queues an {@link RefreshFolderUnseen} operation.
*/
private void schedule_unseen_update(Geary.Folder folder) {
MinimalFolder? impl = folder as MinimalFolder;
if (impl != null) {
impl.refresh_unseen();
}
}
protected virtual Geary.SpecialFolderType[] get_supported_special_folders() {
return SUPPORTED_SPECIAL_FOLDERS;
}
private void compile_special_search_names() {
/*
* Compiles the list of names used to search for special
* folders when they aren't known in advance and the server
* supports neither SPECIAL-USE not XLIST.
*
* Uses both translated and untranslated names in case the
* server has not localised the folders that match the login
* session's language. Also checks for lower-case versions of
* each.
*/
foreach (Geary.SpecialFolderType type in get_supported_special_folders()) {
Gee.List<string> compiled = new Gee.ArrayList<string>();
foreach (string names in get_special_search_names(type)) {
foreach (string name in names.split("|")) {
name = name.strip();
if (name.length != 0) {
if (!(name in compiled)) {
compiled.add(name);
}
name = name.down();
if (!(name in compiled)) {
compiled.add(name);
}
}
}
}
special_search_names.set(type, compiled);
}
}
private Gee.List<string> get_special_search_names(Geary.SpecialFolderType type) {
Gee.List<string> loc_names = new Gee.ArrayList<string>();
Gee.List<string> unloc_names = new Gee.ArrayList<string>();
switch (type) {
case Geary.SpecialFolderType.DRAFTS:
// List of general possible folder names to match for the
// Draft mailbox. Separate names using a vertical bar and
// put the most common localized name to the front for the
// default. English names do not need to be included.
loc_names.add(_("Drafts | Draft"));
unloc_names.add("Drafts | Draft");
break;
case Geary.SpecialFolderType.SENT:
// List of general possible folder names to match for the
// Sent mailbox. Separate names using a vertical bar and
// put the most common localized name to the front for the
// default. English names do not need to be included.
loc_names.add(_("Sent | Sent Mail | Sent Email | Sent E-Mail"));
unloc_names.add("Sent | Sent Mail | Sent Email | Sent E-Mail");
// The localised name(s) of the Sent folder name as used
// by MS Outlook/Exchange.
loc_names.add(NC_("Outlook localised name", "Sent Items"));
unloc_names.add("Sent Items");
break;
case Geary.SpecialFolderType.SPAM:
// List of general possible folder names to match for the
// Spam mailbox. Separate names using a vertical bar and
// put the most common localized name to the front for the
// default. English names do not need to be included.
loc_names.add(_("Junk | Spam | Junk Mail | Junk Email | Junk E-Mail | Bulk Mail | Bulk Email | Bulk E-Mail"));
unloc_names.add("Junk | Spam | Junk Mail | Junk Email | Junk E-Mail | Bulk Mail | Bulk Email | Bulk E-Mail");
break;
case Geary.SpecialFolderType.TRASH:
// List of general possible folder names to match for the
// Trash mailbox. Separate names using a vertical bar and
// put the most common localized name to the front for the
// default. English names do not need to be included.
loc_names.add(_("Trash | Rubbish | Rubbish Bin"));
unloc_names.add("Trash | Rubbish | Rubbish Bin");
// The localised name(s) of the Trash folder name as used
// by MS Outlook/Exchange.
loc_names.add(NC_("Outlook localised name", "Deleted Items"));
unloc_names.add("Deleted Items");
break;
case Geary.SpecialFolderType.ARCHIVE:
// List of general possible folder names to match for the
// Archive mailbox. Separate names using a vertical bar
// and put the most common localized name to the front for
// the default. English names do not need to be included.
loc_names.add(_("Archive | Archives"));
unloc_names.add("Archive | Archives");
break;
}
loc_names.add_all(unloc_names);
return loc_names;
}
private void check_open() throws EngineError {
if (!open)
throw new EngineError.OPEN_REQUIRED("Account %s not opened", to_string());
}
private void on_operation_error(AccountOperation op, Error error) {
notify_service_problem(this.information.incoming, error);
}
private void on_imap_status_notify() {
if (this.open) {
if (this.imap.current_status == CONNECTED) {
this.remote_ready_lock.blind_notify();
update_remote_folders();
} else {
this.remote_ready_lock.reset();
this.refresh_folder_timer.reset();
}
}
}
}
/**
* Account operation for loading local folders from the database.
*/
internal class Geary.ImapEngine.LoadFolders : AccountOperation {
private weak ImapDB.Account local;
private Geary.SpecialFolderType[] specials;
internal LoadFolders(GenericAccount account,
ImapDB.Account local,
Geary.SpecialFolderType[] specials) {
base(account);
this.local = local;
this.specials = specials;
}
public override async void execute(Cancellable cancellable) throws Error {
GenericAccount generic = (GenericAccount) this.account;
Gee.List<ImapDB.Folder> folders = new Gee.LinkedList<ImapDB.Folder>();
yield enumerate_local_folders_async(
folders, generic.local.imap_folder_root, cancellable
);
generic.add_folders(folders, true);
if (!folders.is_empty) {
// If we have some folders to load, then this isn't the
// first run, and hence the special folders should already
// exist
yield check_special_folders(cancellable);
}
}
private async void enumerate_local_folders_async(Gee.List<ImapDB.Folder> folders,
Geary.FolderPath parent,
Cancellable? cancellable)
throws Error {
Gee.Collection<ImapDB.Folder>? children = null;
try {
children = yield this.local.list_folders_async(parent, cancellable);
} catch (EngineError err) {
// don't pass on NOT_FOUND's, that means we need to go to
// the server for more info
if (!(err is EngineError.NOT_FOUND))
throw err;
}
if (children != null) {
foreach (ImapDB.Folder child in children) {
folders.add(child);
yield enumerate_local_folders_async(
folders, child.get_path(), cancellable
);
}
}
}
private async void check_special_folders(GLib.Cancellable cancellable)
throws GLib.Error {
// Local folders loaded that have the SPECIAL-USE flags set
// will have been promoted already via derived account type's
// new_child overrides or some other means. However for those
// that do not have the flag, check here against the local
// config and promote ASAP.
//
// Can't just use ensure_special_folder_async however since
// that will attempt to create the folders if missing, which
// is bad if offline.
GenericAccount generic = (GenericAccount) this.account;
Gee.Map<Geary.SpecialFolderType,Geary.Folder> added_specials =
new Gee.HashMap<Geary.SpecialFolderType,Geary.Folder>();
foreach (Geary.SpecialFolderType type in this.specials) {
if (generic.get_special_folder(type) == null) {
Geary.FolderPath? path =
generic.information.get_special_folder_path(type);
path = this.local.imap_folder_root.copy(path);
if (path != null) {
try {
Geary.Folder target = generic.get_folder(path);
added_specials.set(type, target);
} catch (Error err) {
debug(
"Previously used special folder %s not loaded: %s",
type.to_string(),
err.message
);
}
}
}
}
generic.promote_folders(added_specials);
}
}
/**
* Account operation for starting the outgoing service.
*/
internal class Geary.ImapEngine.StartPostie : AccountOperation {
internal StartPostie(Account account) {
base(account);
}
public override async void execute(GLib.Cancellable cancellable)
throws GLib.Error {
yield this.account.outgoing.start(cancellable);
}
}
/**
* Account operation for populating the full-text-search table.
*/
internal class Geary.ImapEngine.PopulateSearchTable : AccountOperation {
internal PopulateSearchTable(GenericAccount account) {
base(account);
}
public override async void execute(GLib.Cancellable cancellable)
throws GLib.Error {
yield ((GenericAccount) this.account).local.populate_search_table(
cancellable
);
}
}
/**
* Account operation that updates folders from the remote.
*/
internal class Geary.ImapEngine.UpdateRemoteFolders : AccountOperation {
private weak GenericAccount generic_account;
private Geary.SpecialFolderType[] specials;
internal UpdateRemoteFolders(GenericAccount account,
Geary.SpecialFolderType[] specials) {
base(account);
this.generic_account = account;
this.specials = specials;
}
public override async void execute(Cancellable cancellable) throws Error {
Gee.Map<FolderPath, Geary.Folder> existing_folders =
Geary.traverse<Geary.Folder>(this.account.list_folders())
.to_hash_map<FolderPath>(f => f.path);
Gee.Map<FolderPath, Imap.Folder> remote_folders =
new Gee.HashMap<FolderPath, Imap.Folder>();
GenericAccount account = (GenericAccount) this.account;
Imap.AccountSession remote = yield account.claim_account_session(
cancellable
);
try {
bool is_suspect = yield enumerate_remote_folders_async(
remote,
remote_folders,
account.local.imap_folder_root,
cancellable
);
debug("Existing folders:");
foreach (FolderPath path in existing_folders.keys) {
debug(" - %s (%u)", path.to_string(), path.hash());
}
debug("Remote folders:");
foreach (FolderPath path in remote_folders.keys) {
debug(" - %s (%u)", path.to_string(), path.hash());
}
// pair the local and remote folders and make sure
// everything is up-to-date
yield update_folders_async(
remote, existing_folders, remote_folders, is_suspect, cancellable
);
} finally {
account.release_account_session(remote);
}
}
private async bool enumerate_remote_folders_async(Imap.AccountSession remote,
Gee.Map<FolderPath,Imap.Folder> folders,
Geary.FolderPath? parent,
Cancellable? cancellable)
throws Error {
bool results_suspect = false;
Gee.List<Imap.Folder>? children = null;
try {
children = yield remote.fetch_child_folders_async(parent, cancellable);
} catch (Error err) {
// ignore everything but I/O and IMAP errors (cancellation is an IOError)
if (err is IOError || err is ImapError)
throw err;
debug("Ignoring error listing child folders of %s: %s",
(parent != null ? parent.to_string() : "root"), err.message);
results_suspect = true;
}
if (children != null) {
foreach (Imap.Folder child in children) {
FolderPath path = child.path;
folders.set(path, child);
if (child.properties.has_children.is_possible() &&
yield enumerate_remote_folders_async(
remote, folders, path, cancellable)) {
results_suspect = true;
}
}
}
return results_suspect;
}
private async void update_folders_async(Imap.AccountSession remote,
Gee.Map<FolderPath,Geary.Folder> existing_folders,
Gee.Map<FolderPath,Imap.Folder> remote_folders,
bool remote_folders_suspect,
Cancellable? cancellable) {
// update all remote folders properties in the local store and
// active in the system
Gee.HashSet<Geary.FolderPath> altered_paths = new Gee.HashSet<Geary.FolderPath>();
foreach (Imap.Folder remote_folder in remote_folders.values) {
MinimalFolder? minimal_folder = existing_folders.get(remote_folder.path)
as MinimalFolder;
if (minimal_folder == null)
continue;
// only worry about alterations if the remote is openable
if (remote_folder.properties.is_openable.is_possible()) {
ImapDB.Folder local_folder = minimal_folder.local_folder;
if (remote_folder.properties.have_contents_changed(local_folder.get_properties(),
minimal_folder.to_string())) {
altered_paths.add(remote_folder.path);
}
}
// always update, openable or not; have the folder update the UID info the next time
// it's opened
try {
yield minimal_folder.local_folder.update_folder_status(
remote_folder.properties, false, cancellable
);
} catch (Error update_error) {
debug("Unable to update local folder %s with remote properties: %s",
remote_folder.path.to_string(), update_error.message);
}
// set the engine folder's special type (but only promote,
// not demote, since getting the special folder type via
// its properties relies on the optional SPECIAL-USE or
// XLIST extensions) use this iteration to add discovered
// properties to map
if (minimal_folder.special_folder_type == SpecialFolderType.NONE)
minimal_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
}
// If path in remote but not local, need to add it
Gee.ArrayList<Imap.Folder> to_add = Geary.traverse<Imap.Folder>(remote_folders.values)
.filter(f => !existing_folders.has_key(f.path))
.to_array_list();
// Remove if path in local but not remote
Gee.ArrayList<Geary.Folder> to_remove
= Geary.traverse<Gee.Map.Entry<FolderPath,Geary.Folder>>(existing_folders)
.filter(e => !remote_folders.has_key(e.key))
.map<Geary.Folder>(e => (Geary.Folder) e.value)
.to_array_list();
// For folders to add, clone them and their properties
// locally, then add to the account
ImapDB.Account local = ((GenericAccount) this.account).local;
Gee.ArrayList<ImapDB.Folder> to_build = new Gee.ArrayList<ImapDB.Folder>();
foreach (Geary.Imap.Folder remote_folder in to_add) {
try {
to_build.add(
yield local.clone_folder_async(remote_folder, cancellable)
);
} catch (Error err) {
debug("Unable to clone folder %s in local store: %s",
remote_folder.path.to_string(),
err.message);
}
}
this.generic_account.add_folders(to_build, false);
if (remote_folders_suspect) {
debug("Skipping removing folders due to prior errors");
} else {
Gee.BidirSortedSet<MinimalFolder> removed =
this.generic_account.remove_folders(to_remove);
Gee.BidirIterator<MinimalFolder> removed_iterator =
removed.bidir_iterator();
bool has_prev = removed_iterator.last();
while (has_prev) {
MinimalFolder folder = removed_iterator.get();
try {
debug("Locally deleting removed folder %s", folder.to_string());
yield local.delete_folder_async(folder.path, cancellable);
} catch (Error e) {
debug("Unable to locally delete removed folder %s: %s", folder.to_string(), e.message);
}
has_prev = removed_iterator.previous();
}
// Let the remote know as well
remote.folders_removed(
Geary.traverse<Geary.Folder>(removed)
.map<FolderPath>(f => f.path).to_array_list()
);
}
// report all altered folders
if (altered_paths.size > 0) {
Gee.ArrayList<Geary.Folder> altered = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.FolderPath altered_path in altered_paths) {
if (existing_folders.has_key(altered_path))
altered.add(existing_folders.get(altered_path));
else
debug("Unable to report %s altered: no local representation", altered_path.to_string());
}
this.generic_account.update_folders(altered);
}
// Ensure each of the important special folders we need already exist
foreach (Geary.SpecialFolderType special in this.specials) {
try {
yield this.generic_account.ensure_special_folder_async(
remote, special, cancellable
);
} catch (Error e) {
warning("Unable to ensure special folder %s: %s", special.to_string(), e.message);
}
}
}
}
/**
* Account operation that updates a folder's unseen message count.
*
* This performs a IMAP STATUS on the folder, but only if it is not
* open - if it is open it is already maintaining its unseen count.
*/
internal class Geary.ImapEngine.RefreshFolderUnseen : FolderOperation {
internal RefreshFolderUnseen(MinimalFolder folder,
GenericAccount account) {
base(account, folder);
}
public override async void execute(Cancellable cancellable) throws Error {
GenericAccount account = (GenericAccount) this.account;
if (this.folder.get_open_state() == Geary.Folder.OpenState.CLOSED) {
Imap.AccountSession? remote = yield account.claim_account_session(
cancellable
);
try {
Imap.Folder remote_folder = yield remote.fetch_folder_async(
folder.path,
cancellable
);
// Implementation-specific hack: Although this is called
// when the MinimalFolder is closed, we can safely use
// local_folder since we are only using its properties,
// and the properties were loaded when the folder was
// first instantiated.
ImapDB.Folder local_folder = ((MinimalFolder) this.folder).local_folder;
if (remote_folder.properties.have_contents_changed(
local_folder.get_properties(),
this.folder.to_string())) {
yield local_folder.update_folder_status(
remote_folder.properties, true, cancellable
);
((GenericAccount) this.account).update_folder(this.folder);
}
} finally {
account.release_account_session(remote);
}
}
}
}