/* * Copyright 2016 Software Freedom Conservancy Inc. * Copyright 2017-2019 Michael Gratton . * * 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 folder_map = new Gee.HashMap(); private AccountProcessor? processor; private AccountSynchronizer sync; private TimeoutManager refresh_folder_timer; private Gee.Map> special_search_names = new Gee.HashMap>(); 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 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 list_folders() { var all = new Gee.HashSet(); all.add_all(this.folder_map.values); return all; } /** {@inheritDoc} */ public override Gee.Collection list_matching_folders(FolderPath? parent) throws EngineError.NOT_FOUND { return traverse(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(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 check_ids(Gee.Collection 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) 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? local_search_message_id_async( Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok, Gee.Collection? 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 list_local_email_async( Gee.Collection 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? local_search_async(Geary.SearchQuery query, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? 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? get_search_matches_async(Geary.SearchQuery query, Gee.Collection ids, Cancellable? cancellable = null) throws Error { return yield local.get_search_matches_async(query, check_ids(ids), cancellable); } public override async Gee.MultiMap? get_containing_folders_async(Gee.Collection ids, GLib.Cancellable? cancellable) throws GLib.Error { Gee.MultiMap map = new Gee.HashMultiMap(); 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 add_folders(Gee.Collection db_folders, bool are_existing) { Gee.TreeSet built_folders = new Gee.TreeSet( 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 folders = new Gee.LinkedList(); 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 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 specials) { Gee.Set changed = new Gee.HashSet(); 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 remove_folders(Gee.Collection folders) { Gee.TreeSet removed = new Gee.TreeSet( 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 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( 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? available, Gee.BidirSortedSet? 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 ids) { base.notify_email_appended(folder, ids); schedule_unseen_update(folder); } /** {@inheritDoc} */ protected override void notify_email_inserted(Geary.Folder folder, Gee.Collection ids) { base.notify_email_inserted(folder, ids); schedule_unseen_update(folder); } /** {@inheritDoc} */ protected override void notify_email_removed(Geary.Folder folder, Gee.Collection ids) { base.notify_email_removed(folder, ids); schedule_unseen_update(folder); } /** {@inheritDoc} */ protected override void notify_email_locally_removed(Geary.Folder folder, Gee.Collection ids) { base.notify_email_locally_removed(folder, ids); } /** {@inheritDoc} */ protected override void notify_email_flags_changed(Geary.Folder folder, Gee.Map 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 compiled = new Gee.ArrayList(); 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 get_special_search_names(Geary.SpecialFolderType type) { Gee.List loc_names = new Gee.ArrayList(); Gee.List unloc_names = new Gee.ArrayList(); 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 folders = new Gee.LinkedList(); 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 folders, Geary.FolderPath parent, Cancellable? cancellable) throws Error { Gee.Collection? 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 added_specials = new Gee.HashMap(); 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 existing_folders = Geary.traverse(this.account.list_folders()) .to_hash_map(f => f.path); Gee.Map remote_folders = new Gee.HashMap(); 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 folders, Geary.FolderPath? parent, Cancellable? cancellable) throws Error { bool results_suspect = false; Gee.List? 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 existing_folders, Gee.Map remote_folders, bool remote_folders_suspect, Cancellable? cancellable) { // update all remote folders properties in the local store and // active in the system Gee.HashSet altered_paths = new Gee.HashSet(); 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 to_add = Geary.traverse(remote_folders.values) .filter(f => !existing_folders.has_key(f.path)) .to_array_list(); // Remove if path in local but not remote Gee.ArrayList to_remove = Geary.traverse>(existing_folders) .filter(e => !remote_folders.has_key(e.key)) .map(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 to_build = new Gee.ArrayList(); 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 removed = this.generic_account.remove_folders(to_remove); Gee.BidirIterator 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(removed) .map(f => f.path).to_array_list() ); } // report all altered folders if (altered_paths.size > 0) { Gee.ArrayList altered = new Gee.ArrayList(); 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); } } } }