Use server greeting to update IMAP quirks, so that if e.g. a GMail account is configured as a generic account, it still gets the right quirks.
463 lines
14 KiB
Vala
463 lines
14 KiB
Vala
/*
|
|
* Copyright 2016 Software Freedom Conservancy Inc.
|
|
* Copyright 2018-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.
|
|
*/
|
|
|
|
/**
|
|
* Manages email account instances and their life-cycle.
|
|
*
|
|
* An engine represents and contains interfaces into the rest of the
|
|
* email library. Instances are initialized by constructing them and
|
|
* closed by calling {@link close}. Also use this class for verifying
|
|
* and adding {@link AccountInformation} objects to check and start
|
|
* using email accounts.
|
|
*/
|
|
public class Geary.Engine : BaseObject {
|
|
|
|
|
|
// Set low to avoid leaving the user hanging too long when
|
|
// validating a service.
|
|
private const uint VALIDATION_TIMEOUT = 15;
|
|
|
|
|
|
// Workaround for Vala issue #659. See shared_endpoints below.
|
|
private class EndpointWeakRef {
|
|
|
|
GLib.WeakRef weak_ref;
|
|
|
|
public EndpointWeakRef(Endpoint endpoint) {
|
|
this.weak_ref = GLib.WeakRef(endpoint);
|
|
}
|
|
|
|
public Endpoint? get() {
|
|
return this.weak_ref.get() as Endpoint;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
private static bool is_initialized = false;
|
|
|
|
static construct {
|
|
// Work around GNOME/glib#541
|
|
typeof(Imap.MailboxAttributes).name();
|
|
}
|
|
|
|
// This can't be called from within the ctor, as initialization
|
|
// code may want to access the Engine instance to make their own
|
|
// calls and, in particular, subscribe to signals.
|
|
//
|
|
// TODO: It would make sense to have a terminate_library() call,
|
|
// but it technically should not be called until the application
|
|
// is exiting, not merely if the Engine is closed, as termination
|
|
// means shutting down resources for good
|
|
private static void initialize_library() {
|
|
if (!Engine.is_initialized) {
|
|
Engine.is_initialized = true;
|
|
|
|
Logging.init();
|
|
RFC822.init();
|
|
Imap.init();
|
|
HTML.init();
|
|
}
|
|
}
|
|
|
|
|
|
/** Determines if any accounts have been added to this instance. */
|
|
public bool has_accounts {
|
|
get { return this.is_open && !this.accounts.is_empty; }
|
|
}
|
|
|
|
/** Determines the number of accounts added to this instance. */
|
|
public uint accounts_count {
|
|
get { return this.accounts.size; }
|
|
}
|
|
|
|
/** Location of the directory containing shared resource files. */
|
|
public File resource_dir { get; private set; }
|
|
|
|
private bool is_open = true;
|
|
private Gee.List<Account> accounts = new Gee.ArrayList<Account>();
|
|
|
|
// Would use a `weak Endpoint` value type for this map instead of
|
|
// the custom class, but we can't currently reassign built-in
|
|
// weak refs back to a strong ref at the moment, nor use a
|
|
// GLib.WeakRef as a generics param. See Vala issue #659.
|
|
private Gee.Map<string,EndpointWeakRef?> shared_endpoints =
|
|
new Gee.HashMap<string,EndpointWeakRef?>();
|
|
|
|
/**
|
|
* Fired when an account becomes available in the engine.
|
|
*
|
|
* Accounts are made available as soon as they're added to the
|
|
* engine.
|
|
*/
|
|
public signal void account_available(AccountInformation account);
|
|
|
|
/**
|
|
* Fired when an account becomes unavailable in the engine.
|
|
*
|
|
* Accounts are become available as soon when they are removed from the
|
|
* engine or the engine is closed.
|
|
*/
|
|
public signal void account_unavailable(AccountInformation account);
|
|
|
|
|
|
/** Constructs a new engine instance. */
|
|
public Engine(GLib.File resource_dir) {
|
|
Engine.initialize_library();
|
|
this.resource_dir = resource_dir;
|
|
}
|
|
|
|
/**
|
|
* Uninitializes the engine, and removes all known accounts.
|
|
*/
|
|
public void close()
|
|
throws GLib.Error {
|
|
if (is_open) {
|
|
// Copy the collection of accounts so they can be removed
|
|
// from it
|
|
foreach (var account in traverse(this.accounts).to_linked_list()) {
|
|
remove_account(account.information);
|
|
}
|
|
this.accounts.clear();
|
|
this.is_open = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if an account with a specific configuration has been added.
|
|
*/
|
|
public bool has_account(AccountInformation config) {
|
|
return this.accounts.any_match(account => account.information == config);
|
|
}
|
|
|
|
/**
|
|
* Returns the account for the given configuration, if present.
|
|
*
|
|
* Throws an error if the engine has not been opened or if the
|
|
* requested account does not exist.
|
|
*/
|
|
public Account get_account(AccountInformation config) throws GLib.Error {
|
|
check_opened();
|
|
|
|
Account? account = this.accounts.first_match(
|
|
account => account.information == config
|
|
);
|
|
if (account == null) {
|
|
throw new EngineError.NOT_FOUND("No such account");
|
|
}
|
|
return account;
|
|
}
|
|
|
|
/**
|
|
* Returns the account for the given configuration id, if present.
|
|
*
|
|
* Throws an error if the engine has not been opened or if the
|
|
* requested account does not exist.
|
|
*/
|
|
public Account get_account_for_id(string id) throws GLib.Error {
|
|
check_opened();
|
|
|
|
Account? account = this.accounts.first_match(
|
|
account => account.information.id == id
|
|
);
|
|
if (account == null) {
|
|
throw new EngineError.NOT_FOUND("No such account");
|
|
}
|
|
return account;
|
|
}
|
|
|
|
/**
|
|
* Returns a read-only collection of current accounts.
|
|
*
|
|
* The collection is guaranteed to be ordered by {@link
|
|
* AccountInformation.compare_ascending}.
|
|
*
|
|
* Throws an error if the engine has not been opened.
|
|
*/
|
|
public Gee.Collection<Account> get_accounts() throws GLib.Error {
|
|
check_opened();
|
|
|
|
return accounts.read_only_view;
|
|
}
|
|
|
|
/**
|
|
* Adds the account to the engine.
|
|
*
|
|
* The account will not be automatically opened, this must be done
|
|
* once added.
|
|
*/
|
|
public void add_account(AccountInformation config) throws GLib.Error {
|
|
check_opened();
|
|
|
|
if (has_account(config)) {
|
|
throw new EngineError.ALREADY_EXISTS("Account already exists");
|
|
}
|
|
|
|
ImapDB.Account local = new ImapDB.Account(
|
|
config,
|
|
config.data_dir,
|
|
this.resource_dir.get_child("sql")
|
|
);
|
|
Endpoint incoming_remote = get_shared_endpoint(
|
|
config.service_provider, config.incoming
|
|
);
|
|
Endpoint outgoing_remote = get_shared_endpoint(
|
|
config.service_provider, config.outgoing
|
|
);
|
|
|
|
Geary.Account account;
|
|
switch (config.service_provider) {
|
|
case ServiceProvider.GMAIL:
|
|
account = new ImapEngine.GmailAccount(
|
|
config, local, incoming_remote, outgoing_remote
|
|
);
|
|
break;
|
|
|
|
case ServiceProvider.YAHOO:
|
|
account = new ImapEngine.YahooAccount(
|
|
config, local, incoming_remote, outgoing_remote
|
|
);
|
|
break;
|
|
|
|
case ServiceProvider.OUTLOOK:
|
|
account = new ImapEngine.OutlookAccount(
|
|
config, local, incoming_remote, outgoing_remote
|
|
);
|
|
break;
|
|
|
|
case ServiceProvider.OTHER:
|
|
account = new ImapEngine.OtherAccount(
|
|
config, local, incoming_remote, outgoing_remote
|
|
);
|
|
break;
|
|
|
|
default:
|
|
assert_not_reached();
|
|
}
|
|
|
|
config.notify["ordinal"].connect(on_account_ordinal_changed);
|
|
this.accounts.add(account);
|
|
sort_accounts();
|
|
account_available(config);
|
|
}
|
|
|
|
/**
|
|
* Removes an account from the engine.
|
|
*
|
|
* The account must be closed before removing it.
|
|
*/
|
|
public void remove_account(AccountInformation config) throws GLib.Error {
|
|
check_opened();
|
|
|
|
Account account = get_account(config);
|
|
if (account.is_open()) {
|
|
throw new EngineError.CLOSE_REQUIRED(
|
|
"Account must be closed before removal"
|
|
);
|
|
}
|
|
|
|
config.notify["ordinal"].disconnect(on_account_ordinal_changed);
|
|
this.accounts.remove(account);
|
|
|
|
account_unavailable(config);
|
|
}
|
|
|
|
/**
|
|
* Determines if an account's IMAP service can be connected to.
|
|
*/
|
|
public async void validate_imap(AccountInformation account,
|
|
ServiceInformation service,
|
|
GLib.Cancellable? cancellable = null)
|
|
throws GLib.Error {
|
|
check_opened();
|
|
|
|
// Use a new endpoint since we use a different socket timeout
|
|
Endpoint endpoint = new_endpoint(
|
|
account.service_provider, service, VALIDATION_TIMEOUT
|
|
);
|
|
ulong untrusted_id = endpoint.untrusted_host.connect(
|
|
(security, cx) => account.untrusted_host(service, security, cx)
|
|
);
|
|
|
|
var client = new Imap.ClientSession(endpoint, new Imap.Quirks());
|
|
GLib.Error? imap_err = null;
|
|
try {
|
|
yield client.connect_async(
|
|
Imap.ClientSession.DEFAULT_GREETING_TIMEOUT_SEC, cancellable
|
|
);
|
|
} catch (GLib.Error err) {
|
|
imap_err = err;
|
|
}
|
|
|
|
if (imap_err == null) {
|
|
try {
|
|
yield client.initiate_session_async(
|
|
service.credentials, cancellable
|
|
);
|
|
} catch (GLib.Error err) {
|
|
imap_err = err;
|
|
}
|
|
|
|
try {
|
|
yield client.disconnect_async(cancellable);
|
|
} catch {
|
|
// Oh well
|
|
}
|
|
}
|
|
|
|
// This always needs to be disconnected, even when there's an
|
|
// error
|
|
endpoint.disconnect(untrusted_id);
|
|
|
|
if (imap_err != null) {
|
|
throw imap_err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if an account's SMTP service can be connected to.
|
|
*/
|
|
public async void validate_smtp(AccountInformation account,
|
|
ServiceInformation service,
|
|
Credentials? incoming_credentials,
|
|
GLib.Cancellable? cancellable = null)
|
|
throws GLib.Error {
|
|
check_opened();
|
|
|
|
// Use a new endpoint since we use a different socket timeout
|
|
Endpoint endpoint = new_endpoint(
|
|
account.service_provider, service, VALIDATION_TIMEOUT
|
|
);
|
|
ulong untrusted_id = endpoint.untrusted_host.connect(
|
|
(security, cx) => account.untrusted_host(service, security, cx)
|
|
);
|
|
|
|
Credentials? credentials = null;
|
|
switch (service.credentials_requirement) {
|
|
case USE_INCOMING:
|
|
credentials = incoming_credentials;
|
|
break;
|
|
case CUSTOM:
|
|
credentials = service.credentials;
|
|
break;
|
|
case NONE:
|
|
// no-op
|
|
break;
|
|
}
|
|
|
|
Geary.Smtp.ClientSession client = new Geary.Smtp.ClientSession(endpoint);
|
|
GLib.Error? login_err = null;
|
|
try {
|
|
yield client.login_async(credentials, cancellable);
|
|
} catch (GLib.Error err) {
|
|
login_err = err;
|
|
}
|
|
|
|
try {
|
|
yield client.logout_async(true, cancellable);
|
|
} catch {
|
|
// Oh well
|
|
}
|
|
|
|
// This always needs to be disconnected, even when there's an
|
|
// error
|
|
endpoint.disconnect(untrusted_id);
|
|
|
|
if (login_err != null) {
|
|
throw login_err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes the service configuration for an account.
|
|
*
|
|
* This updates an account's service configuration with the given
|
|
* configuration, by replacing the account's existing
|
|
* configuration for that service. The corresponding {@link
|
|
* Account.incoming} or {@link Account.outgoing} client service
|
|
* will also be updated so that the new configuration will start
|
|
* taking effect immediately.
|
|
*/
|
|
public async void update_account_service(AccountInformation config,
|
|
ServiceInformation updated,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
Account account = get_account(config);
|
|
|
|
ClientService? service = null;
|
|
switch (updated.protocol) {
|
|
case Protocol.IMAP:
|
|
config.incoming = updated;
|
|
service = account.incoming;
|
|
break;
|
|
|
|
case Protocol.SMTP:
|
|
config.outgoing = updated;
|
|
service = account.outgoing;
|
|
break;
|
|
}
|
|
|
|
Endpoint remote = get_shared_endpoint(config.service_provider, updated);
|
|
yield service.update_configuration(updated, remote, cancellable);
|
|
config.changed();
|
|
}
|
|
|
|
private Geary.Endpoint get_shared_endpoint(ServiceProvider provider,
|
|
ServiceInformation service) {
|
|
// Key includes TLS method since endpoints encapsulate
|
|
// TLS-specific state
|
|
string key = "%s:%u/%s".printf(
|
|
service.host,
|
|
service.port,
|
|
service.transport_security.to_value()
|
|
);
|
|
|
|
Endpoint? shared = null;
|
|
EndpointWeakRef? cached = this.shared_endpoints.get(key);
|
|
if (cached != null) {
|
|
shared = cached.get() as Endpoint;
|
|
}
|
|
if (shared == null) {
|
|
uint timeout = service.protocol == Protocol.IMAP
|
|
? Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC
|
|
: Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC;
|
|
shared = new_endpoint(provider, service, timeout);
|
|
this.shared_endpoints.set(key, new EndpointWeakRef(shared));
|
|
}
|
|
|
|
return shared;
|
|
}
|
|
|
|
private void check_opened() throws EngineError {
|
|
if (!is_open)
|
|
throw new EngineError.OPEN_REQUIRED("Geary.Engine instance not open");
|
|
}
|
|
|
|
private inline Geary.Endpoint new_endpoint(ServiceProvider provider,
|
|
ServiceInformation service,
|
|
uint timeout) {
|
|
return new Endpoint(
|
|
new NetworkAddress(service.host, service.port),
|
|
service.transport_security,
|
|
timeout
|
|
);
|
|
}
|
|
|
|
private void sort_accounts() {
|
|
this.accounts.sort((a, b) => {
|
|
return AccountInformation.compare_ascending(
|
|
a.information, b.information
|
|
);
|
|
});
|
|
}
|
|
|
|
private void on_account_ordinal_changed() {
|
|
sort_accounts();
|
|
}
|
|
|
|
}
|