diff --git a/debian/control b/debian/control index 9896e113..894b5067 100644 --- a/debian/control +++ b/debian/control @@ -20,7 +20,8 @@ Build-Depends: debhelper (>= 8), intltool, libgirepository1.0-dev (>= 1.32.0), desktop-file-utils, - gnome-doc-utils + gnome-doc-utils, + libgcr-3-dev Standards-Version: 3.8.3 Homepage: http://www.yorba.org @@ -38,7 +39,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, libgmime-2.6-0 (>= 2.6.0), libsecret-1-0 (>= 0.11), libmessaging-menu0 (>= 12.10.2), - libunity9 (>= 5.12.0) + libunity9 (>= 5.12.0), + libgcr-3-1 Description: Email client Geary is an email client built for the GNOME desktop environment. It allows you to read and send email with a simple, modern interface. diff --git a/po/POTFILES.in b/po/POTFILES.in index 59830436..32ae9a41 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -48,6 +48,7 @@ src/client/conversation-viewer/conversation-viewer.vala src/client/conversation-viewer/conversation-web-view.vala src/client/dialogs/alert-dialog.vala src/client/dialogs/attachment-dialog.vala +src/client/dialogs/certificate-warning-dialog.vala src/client/dialogs/password-dialog.vala src/client/dialogs/preferences-dialog.vala src/client/dialogs/upgrade-dialog.vala @@ -114,6 +115,7 @@ src/engine/api/geary-named-flag.vala src/engine/api/geary-progress-monitor.vala src/engine/api/geary-search-folder.vala src/engine/api/geary-search-query.vala +src/engine/api/geary-service.vala src/engine/api/geary-service-provider.vala src/engine/api/geary-special-folder-type.vala src/engine/app/app-conversation-monitor.vala @@ -352,6 +354,7 @@ src/engine/util/util-trillian.vala [type: gettext/glade]ui/account_list.glade [type: gettext/glade]ui/account_spinner.glade [type: gettext/glade]ui/app_menu.interface +[type: gettext/glade]ui/certificate_warning_dialog.glade [type: gettext/glade]ui/composer_accelerators.ui [type: gettext/glade]ui/composer.glade [type: gettext/glade]ui/find_bar.glade diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e8a6e15..5a786618 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ engine/api/geary-named-flags.vala engine/api/geary-progress-monitor.vala engine/api/geary-search-folder.vala engine/api/geary-search-query.vala +engine/api/geary-service.vala engine/api/geary-service-provider.vala engine/api/geary-special-folder-type.vala @@ -346,6 +347,7 @@ client/conversation-viewer/conversation-web-view.vala client/dialogs/alert-dialog.vala client/dialogs/attachment-dialog.vala +client/dialogs/certificate-warning-dialog.vala client/dialogs/password-dialog.vala client/dialogs/preferences-dialog.vala client/dialogs/upgrade-dialog.vala @@ -530,6 +532,7 @@ pkg_check_modules(DEPS REQUIRED gmime-2.6>=2.6.0 libsecret-1>=0.11 libxml-2.0>=2.7.8 + gcr-3 ${EXTRA_CLIENT_PKG_CONFIG} ) @@ -540,7 +543,7 @@ set(ENGINE_PACKAGES # webkitgtk-3.0 is listed as a custom VAPI (below) to ensure it's treated as a dependency and # built before compilation set(CLIENT_PACKAGES - gtk+-3.0 libsecret-1 libsoup-2.4 libnotify libcanberra ${EXTRA_CLIENT_PACKAGES} + gtk+-3.0 libsecret-1 libsoup-2.4 libnotify libcanberra gcr-3 ${EXTRA_CLIENT_PACKAGES} ) set(CONSOLE_PACKAGES @@ -558,6 +561,7 @@ set(CFLAGS -D_GSETTINGS_DIR=\"${CMAKE_BINARY_DIR}/gsettings\" -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLANGUAGE_SUPPORT_DIRECTORY=\"${LANGUAGE_SUPPORT_DIRECTORY}\" + -DGCR_API_SUBJECT_TO_CHANGE -g ) diff --git a/src/client/accounts/account-dialog.vala b/src/client/accounts/account-dialog.vala index b64df131..192be9d4 100644 --- a/src/client/accounts/account-dialog.vala +++ b/src/client/accounts/account-dialog.vala @@ -92,8 +92,7 @@ public class AccountDialog : Gtk.Dialog { return; try { - yield account.get_passwords_async(Geary.CredentialsMediator.ServiceFlag.IMAP | - Geary.CredentialsMediator.ServiceFlag.SMTP); + yield account.get_passwords_async(Geary.ServiceFlag.IMAP | Geary.ServiceFlag.SMTP); } catch (Error err) { debug("Unable to fetch password(s) for account: %s", err.message); } @@ -166,22 +165,34 @@ public class AccountDialog : Gtk.Dialog { options |= Geary.Engine.ValidationOption.CHECK_CONNECTIONS; // Validate account. - GearyApplication.instance.controller.validate_async.begin(info, options, null, - on_save_add_or_edit_completed); + do_save_or_edit_async.begin(info, options); } - private void on_save_add_or_edit_completed(Object? source, AsyncResult result) { - Geary.Engine.ValidationResult validation_result = - GearyApplication.instance.controller.validate_async.end(result); - - // If account was successfully added return to the account list. Otherwise, go back to the - // account add page so the user can try again. - if (validation_result == Geary.Engine.ValidationResult.OK) { - account_list_pane.present(); - } else { - add_edit_pane.set_validation_result(validation_result); - add_edit_pane.present(); + private async void do_save_or_edit_async(Geary.AccountInformation account_information, + Geary.Engine.ValidationOption options) { + Geary.Engine.ValidationResult validation_result = Geary.Engine.ValidationResult.OK; + for (;;) { + validation_result = yield GearyApplication.instance.controller.validate_async( + account_information, options); + + // If account was successfully added return to the account list. + if (validation_result == Geary.Engine.ValidationResult.OK) { + account_list_pane.present(); + + return; + } + + // check for TLS warnings + bool retry_required; + validation_result = yield GearyApplication.instance.controller.validation_check_for_tls_warnings_async( + account_information, validation_result, out retry_required); + if (!retry_required) + break; } + + // Otherwise, go back to the account add page so the user can try again. + add_edit_pane.set_validation_result(validation_result); + add_edit_pane.present(); } private void on_cancel_back_to_list() { diff --git a/src/client/application/geary-args.vala b/src/client/application/geary-args.vala index a26a9c8b..70b91594 100644 --- a/src/client/application/geary-args.vala +++ b/src/client/application/geary-args.vala @@ -23,6 +23,7 @@ private const OptionEntry[] options = { /// "Normalization" can also be called "synchronization" { "log-folder-normalization", 0, 0, OptionArg.NONE, ref log_folder_normalization, N_("Log folder normalization"), null }, { "inspector", 'i', 0, OptionArg.NONE, ref inspector, N_("Allow inspection of WebView"), null }, + { "revoke-certs", 0, 0, OptionArg.NONE, ref revoke_certs, N_("Revoke all server certificates with TLS warnings"), null }, { "version", 'V', 0, OptionArg.NONE, ref version, N_("Display program version"), null }, { null } }; @@ -38,6 +39,7 @@ public bool log_periodic = false; public bool log_sql = false; public bool log_folder_normalization = false; public bool inspector = false; +public bool revoke_certs = false; public bool version = false; public bool parse(string[] args) { diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala index 2c4b4f8e..990b5482 100644 --- a/src/client/application/geary-controller.vala +++ b/src/client/application/geary-controller.vala @@ -4,6 +4,16 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +// Required because Gcr's VAPI is behind-the-times +// TODO: When bindings available, use async variants of these calls +extern const string GCR_PURPOSE_SERVER_AUTH; +extern bool gcr_trust_add_pinned_certificate(Gcr.Certificate cert, string purpose, string peer, + Cancellable? cancellable) throws Error; +extern bool gcr_trust_is_certificate_pinned(Gcr.Certificate cert, string purpose, string peer, + Cancellable? cancellable) throws Error; +extern bool gcr_trust_remove_pinned_certificate(Gcr.Certificate cert, string purpose, string peer, + Cancellable? cancellable) throws Error; + // Primary controller object for Geary. public class GearyController : Geary.BaseObject { // Named actions. @@ -111,6 +121,8 @@ public class GearyController : Geary.BaseObject { private LoginDialog? login_dialog = null; private UpgradeDialog upgrade_dialog; private Gee.List pending_mailtos = new Gee.ArrayList(); + private Geary.Nonblocking.Mutex untrusted_host_prompt_mutex = new Geary.Nonblocking.Mutex(); + private Gee.HashSet validating_endpoints = new Gee.HashSet(); // List of windows we're waiting to close before Geary closes. private Gee.List waiting_to_close = new Gee.ArrayList(); @@ -177,6 +189,7 @@ public class GearyController : Geary.BaseObject { Geary.Engine.instance.account_available.connect(on_account_available); Geary.Engine.instance.account_unavailable.connect(on_account_unavailable); + Geary.Engine.instance.untrusted_host.connect(on_untrusted_host); // Connect to various UI signals. main_window.conversation_list_view.conversations_selected.connect(on_conversations_selected); @@ -499,6 +512,135 @@ public class GearyController : Geary.BaseObject { close_account(get_account_instance(account_information)); } + private void on_untrusted_host(Geary.AccountInformation account_information, + Geary.Endpoint endpoint, Geary.Endpoint.SecurityType security, TlsConnection cx, + Geary.Service service) { + prompt_untrusted_host_async.begin(account_information, endpoint, security, cx, service); + } + + private async void prompt_untrusted_host_async(Geary.AccountInformation account_information, + Geary.Endpoint endpoint, Geary.Endpoint.SecurityType security, TlsConnection cx, + Geary.Service service) { + // use a mutex to prevent multiple dialogs popping up at the same time + int token = Geary.Nonblocking.Mutex.INVALID_TOKEN; + try { + token = yield untrusted_host_prompt_mutex.claim_async(); + } catch (Error err) { + message("Unable to lock mutex to prompt user about invalid certificate: %s", err.message); + + return; + } + + yield locked_prompt_untrusted_host_async(account_information, endpoint, security, cx, + service); + + try { + untrusted_host_prompt_mutex.release(ref token); + } catch (Error err) { + message("Unable to release mutex after prompting user about invalid certificate: %s", + err.message); + } + } + + private static void get_gcr_params(Geary.Endpoint endpoint, out Gcr.Certificate cert, + out string peer) { + cert = new Gcr.SimpleCertificate(endpoint.untrusted_certificate.certificate.data); + peer = "%s:%u".printf(endpoint.remote_address.hostname, endpoint.remote_address.port); + } + + private async void locked_prompt_untrusted_host_async(Geary.AccountInformation account_information, + Geary.Endpoint endpoint, Geary.Endpoint.SecurityType security, TlsConnection cx, + Geary.Service service) { + // possible while waiting on mutex that this endpoint became trusted or untrusted + if (endpoint.trust_untrusted_host != Geary.Trillian.UNKNOWN) + return; + + // get GCR parameters + Gcr.Certificate cert; + string peer; + get_gcr_params(endpoint, out cert, out peer); + + // Geary allows for user to auto-revoke all questionable server certificates without + // digging around in a keyring/pk manager + if (Args.revoke_certs) { + debug("Auto-revoking certificate for %s...", peer); + + try { + gcr_trust_remove_pinned_certificate(cert, GCR_PURPOSE_SERVER_AUTH, peer, null); + } catch (Error err) { + message("Unable to auto-revoke server certificate for %s: %s", peer, err.message); + + // drop through, not absolutely valid to do this (might also mean certificate + // was never pinned) + } + } + + // if pinned, the user has already made an exception for this server and its certificate, + // so go ahead w/o asking + try { + if (gcr_trust_is_certificate_pinned(cert, GCR_PURPOSE_SERVER_AUTH, peer, null)) { + debug("Certificate for %s is pinned, accepting connection...", peer); + + endpoint.trust_untrusted_host = Geary.Trillian.TRUE; + + return; + } + } catch (Error err) { + message("Unable to check if server certificate for %s is pinned, assuming not: %s", + peer, err.message); + } + + // if these are in validation, there are complex GTK and workflow issues from simply + // presenting the prompt now, so caller who connected will need to do it on their own dime + if (!validating_endpoints.contains(endpoint)) + prompt_for_untrusted_host(main_window, account_information, endpoint, service, false); + } + + private void prompt_for_untrusted_host(Gtk.Window? parent, Geary.AccountInformation account_information, + Geary.Endpoint endpoint, Geary.Service service, bool is_validation) { + CertificateWarningDialog dialog = new CertificateWarningDialog(parent, account_information, + service, endpoint.tls_validation_warnings, is_validation); + switch (dialog.run()) { + case CertificateWarningDialog.Result.TRUST: + endpoint.trust_untrusted_host = Geary.Trillian.TRUE; + break; + + case CertificateWarningDialog.Result.ALWAYS_TRUST: + endpoint.trust_untrusted_host = Geary.Trillian.TRUE; + + // get GCR parameters for pinning + Gcr.Certificate cert; + string peer; + get_gcr_params(endpoint, out cert, out peer); + + // pinning the certificate creates an exception for the next time a connection + // is attempted + debug("Pinning certificate for %s...", peer); + try { + gcr_trust_add_pinned_certificate(cert, GCR_PURPOSE_SERVER_AUTH, peer, null); + } catch (Error err) { + ErrorDialog error_dialog = new ErrorDialog(main_window, + _("Unable to store server trust exception"), err.message); + error_dialog.run(); + } + break; + + default: + endpoint.trust_untrusted_host = Geary.Trillian.FALSE; + + // close the account; can't go any further w/o offline mode + try { + if (Geary.Engine.instance.get_accounts().has_key(account_information.email)) { + Geary.Account account = Geary.Engine.instance.get_account_instance(account_information); + close_account(account); + } + } catch (Error err) { + message("Unable to close account due to user trust issues: %s", err.message); + } + break; + } + } + private void create_account() { Geary.AccountInformation? account_information = request_account_information(null); if (account_information != null) @@ -516,6 +658,82 @@ public class GearyController : Geary.BaseObject { login_dialog.hide(); } + // Returns possibly modified validation results + private Geary.Engine.ValidationResult validation_check_endpoint_for_tls_warnings( + Geary.AccountInformation account_information, Geary.Service service, + Geary.Engine.ValidationResult validation_result, out bool prompted, out bool retry_required) { + prompted = false; + retry_required = false; + + // use LoginDialog for parent only if available and visible + Gtk.Window? parent; + if (login_dialog != null && login_dialog.visible) + parent = login_dialog; + else + parent = main_window; + + Geary.Endpoint endpoint = account_information.get_endpoint_for_service(service); + + // If Endpoint had unresolved TLS issues, prompt user about them + if (endpoint.tls_validation_warnings != 0 && endpoint.trust_untrusted_host != Geary.Trillian.TRUE) { + prompt_for_untrusted_host(parent, account_information, endpoint, service, true); + prompted = true; + } + + // If there are still TLS connection issues that caused the connection to fail (happens on the + // first attempt), clear those errors and retry + if (endpoint.tls_validation_warnings != 0 && endpoint.trust_untrusted_host == Geary.Trillian.TRUE) { + Geary.Engine.ValidationResult flag = (service == Geary.Service.IMAP) + ? Geary.Engine.ValidationResult.IMAP_CONNECTION_FAILED + : Geary.Engine.ValidationResult.SMTP_CONNECTION_FAILED; + + if ((validation_result & flag) != 0) { + validation_result &= ~flag; + retry_required = true; + } + } + + return validation_result; + } + + // Use after validating to see if TLS warnings were handled by the user and need to retry the + // validation; this will also modify the validation results to better indicate issues to the user + // + // Returns possibly modified validation results + public async Geary.Engine.ValidationResult validation_check_for_tls_warnings_async( + Geary.AccountInformation account_information, Geary.Engine.ValidationResult validation_result, + out bool retry_required) { + retry_required = false; + + // Because TLS warnings need cycles to process, sleep and give 'em a chance to do their + // thing ... note that the signal handler does *not* invoke the user prompt dialog when the + // login dialog is in play, so this sleep does not need to worry about user input + yield Geary.Scheduler.sleep_ms_async(100); + + // check each service for problems, prompting user each time for verification + bool imap_prompted, imap_retry_required; + validation_result = validation_check_endpoint_for_tls_warnings(account_information, + Geary.Service.IMAP, validation_result, out imap_prompted, out imap_retry_required); + + bool smtp_prompted, smtp_retry_required; + validation_result = validation_check_endpoint_for_tls_warnings(account_information, + Geary.Service.SMTP, validation_result, out smtp_prompted, out smtp_retry_required); + + // if prompted for user acceptance of bad certificates and they agreed to both, try again + if (imap_prompted && smtp_prompted + && account_information.get_imap_endpoint().is_trusted_or_never_connected + && account_information.get_smtp_endpoint().is_trusted_or_never_connected) { + retry_required = true; + } else if (validation_result == Geary.Engine.ValidationResult.OK) { + retry_required = true; + } else { + // if prompt requires retry or otherwise detected it, retry + retry_required = imap_retry_required && smtp_retry_required; + } + + return validation_result; + } + // Returns null if we are done validating, or the revised account information if we should retry. private async Geary.AccountInformation? validate_or_retry_async(Geary.AccountInformation account_information, Cancellable? cancellable = null) { @@ -524,6 +742,16 @@ public class GearyController : Geary.BaseObject { if (result == Geary.Engine.ValidationResult.OK) return null; + // check Endpoints for trust (TLS) issues + bool retry_required; + result = yield validation_check_for_tls_warnings_async(account_information, result, + out retry_required); + + // return for retry if required; check can also change validation results, in which case + // revalidate entirely to have them written out + if (retry_required) + return account_information; + debug("Validation failed. Prompting user for revised account information"); Geary.AccountInformation? new_account_information = request_account_information(account_information, result); @@ -545,6 +773,10 @@ public class GearyController : Geary.BaseObject { public async Geary.Engine.ValidationResult validate_async( Geary.AccountInformation account_information, Geary.Engine.ValidationOption options, Cancellable? cancellable = null) { + // add Endpoints to set of validating endpoints to prevent the prompt from appearing + validating_endpoints.add(account_information.get_imap_endpoint()); + validating_endpoints.add(account_information.get_smtp_endpoint()); + Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK; try { result = yield Geary.Engine.instance.validate_account_information_async(account_information, @@ -556,6 +788,9 @@ public class GearyController : Geary.BaseObject { return result; } + validating_endpoints.remove(account_information.get_imap_endpoint()); + validating_endpoints.remove(account_information.get_smtp_endpoint()); + if (result == Geary.Engine.ValidationResult.OK) { Geary.AccountInformation real_account_information = account_information; if (account_information.is_copy()) { @@ -566,8 +801,8 @@ public class GearyController : Geary.BaseObject { } real_account_information.store_async.begin(cancellable); - do_update_stored_passwords_async.begin(Geary.CredentialsMediator.ServiceFlag.IMAP | - Geary.CredentialsMediator.ServiceFlag.SMTP, real_account_information); + do_update_stored_passwords_async.begin(Geary.ServiceFlag.IMAP | Geary.ServiceFlag.SMTP, + real_account_information); debug("Successfully validated account information"); } @@ -635,7 +870,7 @@ public class GearyController : Geary.BaseObject { return new_info; } - private async void do_update_stored_passwords_async(Geary.CredentialsMediator.ServiceFlag services, + private async void do_update_stored_passwords_async(Geary.ServiceFlag services, Geary.AccountInformation account_information) { try { yield account_information.update_stored_passwords_async(services); diff --git a/src/client/application/secret-mediator.vala b/src/client/application/secret-mediator.vala index b06224d3..e1ed1ae3 100644 --- a/src/client/application/secret-mediator.vala +++ b/src/client/application/secret-mediator.vala @@ -8,12 +8,12 @@ public class SecretMediator : Geary.CredentialsMediator, Object { private const string OLD_GEARY_USERNAME_PREFIX = "org.yorba.geary username:"; - private string get_key_name(Geary.CredentialsMediator.Service service, string user) { + private string get_key_name(Geary.Service service, string user) { switch (service) { - case Service.IMAP: + case Geary.Service.IMAP: return "org.yorba.geary imap_username:" + user; - case Service.SMTP: + case Geary.Service.SMTP: return "org.yorba.geary smtp_username:" + user; default: @@ -21,12 +21,12 @@ public class SecretMediator : Geary.CredentialsMediator, Object { } } - private Geary.Credentials get_credentials(Geary.CredentialsMediator.Service service, Geary.AccountInformation account_information) { + private Geary.Credentials get_credentials(Geary.Service service, Geary.AccountInformation account_information) { switch (service) { - case Service.IMAP: + case Geary.Service.IMAP: return account_information.imap_credentials; - case Service.SMTP: + case Geary.Service.SMTP: return account_information.smtp_credentials; default: @@ -49,7 +49,7 @@ public class SecretMediator : Geary.CredentialsMediator, Object { } public virtual async string? get_password_async( - Geary.CredentialsMediator.Service service, Geary.AccountInformation account_information, Cancellable? cancellable = null) + Geary.Service service, Geary.AccountInformation account_information, Cancellable? cancellable = null) throws Error { string key_name = get_key_name(service, account_information.email); string? password = yield Secret.password_lookup(Secret.SCHEMA_COMPAT_NETWORK, cancellable, @@ -77,7 +77,7 @@ public class SecretMediator : Geary.CredentialsMediator, Object { } public virtual async void set_password_async( - Geary.CredentialsMediator.Service service, Geary.AccountInformation account_information, + Geary.Service service, Geary.AccountInformation account_information, Cancellable? cancellable = null) throws Error { string key_name = get_key_name(service, account_information.email); Geary.Credentials credentials = get_credentials(service, account_information); @@ -89,7 +89,7 @@ public class SecretMediator : Geary.CredentialsMediator, Object { } public virtual async void clear_password_async( - Geary.CredentialsMediator.Service service, Geary.AccountInformation account_information, Cancellable? cancellable = null) + Geary.Service service, Geary.AccountInformation account_information, Cancellable? cancellable = null) throws Error { // delete new-style and old-style locations Geary.Credentials credentials = get_credentials(service, account_information); @@ -104,7 +104,7 @@ public class SecretMediator : Geary.CredentialsMediator, Object { OLD_GEARY_USERNAME_PREFIX + credentials.user); } - public virtual async bool prompt_passwords_async(Geary.CredentialsMediator.ServiceFlag services, + public virtual async bool prompt_passwords_async(Geary.ServiceFlag services, Geary.AccountInformation account_information, out string? imap_password, out string? smtp_password, out bool imap_remember_password, out bool smtp_remember_password) throws Error { diff --git a/src/client/dialogs/certificate-warning-dialog.vala b/src/client/dialogs/certificate-warning-dialog.vala new file mode 100644 index 00000000..d3d9f01e --- /dev/null +++ b/src/client/dialogs/certificate-warning-dialog.vala @@ -0,0 +1,114 @@ +/* Copyright 2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class CertificateWarningDialog { + public enum Result { + DONT_TRUST, + TRUST, + ALWAYS_TRUST + } + + private const string BULLET = "• "; + + private Gtk.Dialog dialog; + + public CertificateWarningDialog(Gtk.Window? parent, Geary.AccountInformation account_information, + Geary.Service service, TlsCertificateFlags warnings, bool is_validation) { + Gtk.Builder builder = GearyApplication.instance.create_builder("certificate_warning_dialog.glade"); + + dialog = (Gtk.Dialog) builder.get_object("CertificateWarningDialog"); + dialog.transient_for = parent; + dialog.modal = true; + + Gtk.Label title_label = (Gtk.Label) builder.get_object("untrusted_connection_label"); + Gtk.Label top_label = (Gtk.Label) builder.get_object("top_label"); + Gtk.Label warnings_label = (Gtk.Label) builder.get_object("warnings_label"); + Gtk.Label trust_label = (Gtk.Label) builder.get_object("trust_label"); + Gtk.Label dont_trust_label = (Gtk.Label) builder.get_object("dont_trust_label"); + Gtk.Label contact_label = (Gtk.Label) builder.get_object("contact_label"); + + title_label.label = _("Untrusted Connection: %s").printf(account_information.email); + + Geary.Endpoint endpoint = account_information.get_endpoint_for_service(service); + top_label.label = _("The identity of the %s mail server at %s:%u could not be verified.").printf( + service.user_label(), endpoint.remote_address.hostname, endpoint.remote_address.port); + + warnings_label.label = generate_warning_list(warnings); + warnings_label.use_markup = true; + + trust_label.label = + "" + +_("Selecting \"Trust This Server\" or \"Always Trust This Server\" may cause your username and password to be transmitted insecurely.") + + ""; + trust_label.use_markup = true; + + if (is_validation) { + // could be a new or existing account + dont_trust_label.label = + "" + + _("Selecting \"Don't Trust This Server\" will cause Geary not to access this server.") + + " " + + _("Geary will not add or update this email account."); + } else { + // a registered account + dont_trust_label.label = + "" + + _("Selecting \"Don't Trust This Server\" will cause Geary to stop accessing this account.") + + " " + + _("Geary will exit if you have no other open email accounts."); + } + dont_trust_label.use_markup = true; + + contact_label.label = + _("Contact your system administrator or email service provider if you have any question about these issues."); + } + + private static string generate_warning_list(TlsCertificateFlags warnings) { + StringBuilder builder = new StringBuilder(); + + if ((warnings & TlsCertificateFlags.UNKNOWN_CA) != 0) + builder.append(BULLET + _("The server's certificate is not signed by a known authority") + "\n"); + + if ((warnings & TlsCertificateFlags.BAD_IDENTITY) != 0) + builder.append(BULLET + _("The server's identity does not match the identity in the certificate") + "\n"); + + if ((warnings & TlsCertificateFlags.EXPIRED) != 0) + builder.append(BULLET + _("The server's certificate has expired") + "\n"); + + if ((warnings & TlsCertificateFlags.NOT_ACTIVATED) != 0) + builder.append(BULLET + _("The server's certificate has not been activated") + "\n"); + + if ((warnings & TlsCertificateFlags.REVOKED) != 0) + builder.append(BULLET + _("The server's certificate has been revoked and is now invalid") + "\n"); + + if ((warnings & TlsCertificateFlags.INSECURE) != 0) + builder.append(BULLET + _("The server's certificate is considered insecure") + "\n"); + + if ((warnings & TlsCertificateFlags.GENERIC_ERROR) != 0) + builder.append(BULLET + _("An error has occurred processing the server's certificate") + "\n"); + + return builder.str; + } + + public Result run() { + dialog.show_all(); + int response = dialog.run(); + dialog.destroy(); + + // these values are defined in the Glade file + switch (response) { + case 1: + return Result.TRUST; + + case 2: + return Result.ALWAYS_TRUST; + + default: + return Result.DONT_TRUST; + } + } +} + diff --git a/src/client/dialogs/password-dialog.vala b/src/client/dialogs/password-dialog.vala index 96296fdb..8ca620d7 100644 --- a/src/client/dialogs/password-dialog.vala +++ b/src/client/dialogs/password-dialog.vala @@ -24,7 +24,7 @@ public class PasswordDialog { public bool remember_password { get; private set; } public PasswordDialog(bool smtp, Geary.AccountInformation account_information, - Geary.CredentialsMediator.ServiceFlag password_flags) { + Geary.ServiceFlag password_flags) { Gtk.Builder builder = GearyApplication.instance.create_builder("password-dialog.glade"); dialog = (Gtk.Dialog) builder.get_object("PasswordDialog"); diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala index c00dbb8f..926dc272 100644 --- a/src/engine/api/geary-account-information.vala +++ b/src/engine/api/geary-account-information.vala @@ -50,6 +50,8 @@ public class Geary.AccountInformation : BaseObject { public static int default_ordinal = 0; + private static Gee.HashMap? known_endpoints = null; + internal File? settings_dir = null; internal File? file = null; @@ -105,6 +107,20 @@ public class Geary.AccountInformation : BaseObject { public bool save_drafts { get; set; default = true; } private bool _save_sent_mail = true; + private Endpoint? imap_endpoint = null; + private Endpoint? smtp_endpoint = null; + + /** + * Indicates the supplied {@link Endpoint} has reported TLS certificate warnings during + * connection. + * + * Since this {@link Endpoint} persists for the lifetime of the {@link AccountInformation}, + * marking it as trusted once will survive the application session. It is up to the caller to + * pin the certificate appropriately if the user does not want to receive these warnings in + * the future. + */ + public signal void untrusted_host(Endpoint endpoint, Endpoint.SecurityType security, + TlsConnection cx, Service service); // Used to create temporary AccountInformation objects. (Note that these cannot be saved.) public AccountInformation.temp_copy(AccountInformation copy) { @@ -179,6 +195,32 @@ public class Geary.AccountInformation : BaseObject { } } + ~AccountInformation() { + if (imap_endpoint != null) + imap_endpoint.untrusted_host.disconnect(on_imap_untrusted_host); + + if (smtp_endpoint != null) + smtp_endpoint.untrusted_host.disconnect(on_smtp_untrusted_host); + } + + internal static void init() { + known_endpoints = new Gee.HashMap(); + } + + private static Geary.Endpoint get_shared_endpoint(Service service, Endpoint endpoint) { + string key = "%s/%s:%u".printf(service.user_label(), endpoint.remote_address.hostname, + endpoint.remote_address.port); + + // if already known, prefer it over this one + if (known_endpoints.has_key(key)) + return known_endpoints.get(key); + + // save for future use and return this one + known_endpoints.set(key, endpoint); + + return endpoint; + } + // Copies all data from the "from" object into this one. public void copy_from(AccountInformation from) { real_name = from.real_name; @@ -287,19 +329,19 @@ public class Geary.AccountInformation : BaseObject { * * If force_request is set to true, a prompt will appear regardless. */ - public async bool fetch_passwords_async(CredentialsMediator.ServiceFlag services, + public async bool fetch_passwords_async(ServiceFlag services, bool force_request = false) throws Error { if (force_request) { // Delete the current password(s). if (services.has_imap()) { yield Geary.Engine.instance.authentication_mediator.clear_password_async( - CredentialsMediator.Service.IMAP, this); + Service.IMAP, this); if (imap_credentials != null) imap_credentials.pass = null; } else if (services.has_smtp()) { yield Geary.Engine.instance.authentication_mediator.clear_password_async( - CredentialsMediator.Service.SMTP, this); + Service.SMTP, this); if (smtp_credentials != null) smtp_credentials.pass = null; @@ -308,14 +350,14 @@ public class Geary.AccountInformation : BaseObject { // Only call get_passwords on anything that hasn't been set // (incorrectly) previously. - CredentialsMediator.ServiceFlag get_services = 0; + ServiceFlag get_services = 0; if (services.has_imap() && !imap_credentials.is_complete()) - get_services |= CredentialsMediator.ServiceFlag.IMAP; + get_services |= ServiceFlag.IMAP; if (services.has_smtp() && smtp_credentials != null && !smtp_credentials.is_complete()) - get_services |= CredentialsMediator.ServiceFlag.SMTP; + get_services |= ServiceFlag.SMTP; - CredentialsMediator.ServiceFlag unset_services = services; + ServiceFlag unset_services = services; if (get_services != 0) unset_services = yield get_passwords_async(get_services); else @@ -352,31 +394,28 @@ public class Geary.AccountInformation : BaseObject { * prompt_passwords_async() on the return value), or 0 if all were * retrieved. */ - public async CredentialsMediator.ServiceFlag get_passwords_async( - CredentialsMediator.ServiceFlag services) throws Error { + public async ServiceFlag get_passwords_async(ServiceFlag services) throws Error { check_mediator_instance(); CredentialsMediator mediator = Geary.Engine.instance.authentication_mediator; - CredentialsMediator.ServiceFlag failed_services = 0; + ServiceFlag failed_services = 0; if (services.has_imap()) { - string? imap_password = yield mediator.get_password_async( - CredentialsMediator.Service.IMAP, this); + string? imap_password = yield mediator.get_password_async(Service.IMAP, this); if (imap_password != null) set_imap_password(imap_password); else - failed_services |= CredentialsMediator.ServiceFlag.IMAP; + failed_services |= ServiceFlag.IMAP; } if (services.has_smtp() && smtp_credentials != null) { - string? smtp_password = yield mediator.get_password_async( - CredentialsMediator.Service.SMTP, this); + string? smtp_password = yield mediator.get_password_async(Service.SMTP, this); if (smtp_password != null) set_smtp_password(smtp_password); else - failed_services |= CredentialsMediator.ServiceFlag.SMTP; + failed_services |= ServiceFlag.SMTP; } return failed_services; @@ -390,15 +429,14 @@ public class Geary.AccountInformation : BaseObject { * whether the user proceeded normally (false if they tried to cancel the * prompt). */ - public async bool prompt_passwords_async( - CredentialsMediator.ServiceFlag services) throws Error { + public async bool prompt_passwords_async(ServiceFlag services) throws Error { check_mediator_instance(); string? imap_password, smtp_password; bool imap_remember_password, smtp_remember_password; if (smtp_credentials == null) - services &= ~CredentialsMediator.ServiceFlag.SMTP; + services &= ~ServiceFlag.SMTP; if (!yield Geary.Engine.instance.authentication_mediator.prompt_passwords_async( services, this, out imap_password, out smtp_password, @@ -424,43 +462,49 @@ public class Geary.AccountInformation : BaseObject { * Use the Engine's authentication mediator to set or clear the passwords * for the given services in the key store. */ - public async void update_stored_passwords_async( - CredentialsMediator.ServiceFlag services) throws Error { + public async void update_stored_passwords_async(ServiceFlag services) throws Error { check_mediator_instance(); CredentialsMediator mediator = Geary.Engine.instance.authentication_mediator; if (services.has_imap()) { - if (imap_remember_password) { - yield mediator.set_password_async( - CredentialsMediator.Service.IMAP, this); - } else { - yield mediator.clear_password_async( - CredentialsMediator.Service.IMAP, this); - } + if (imap_remember_password) + yield mediator.set_password_async(Service.IMAP, this); + else + yield mediator.clear_password_async(Service.IMAP, this); } if (services.has_smtp() && smtp_credentials != null) { - if (smtp_remember_password) { - yield mediator.set_password_async( - CredentialsMediator.Service.SMTP, this); - } else { - yield mediator.clear_password_async( - CredentialsMediator.Service.SMTP, this); - } + if (smtp_remember_password) + yield mediator.set_password_async(Service.SMTP, this); + else + yield mediator.clear_password_async(Service.SMTP, this); } } + /** + * Returns the {@link Endpoint} for the account's IMAP service. + * + * The Endpoint instance is guaranteed to be the same for the lifetime of the + * {@link AccountInformation} instance, which is in turn guaranteed to be the same for the + * duration of the application session. + */ public Endpoint get_imap_endpoint() { + if (imap_endpoint != null) + return imap_endpoint; + switch (service_provider) { case ServiceProvider.GMAIL: - return ImapEngine.GmailAccount.IMAP_ENDPOINT; + imap_endpoint = ImapEngine.GmailAccount.generate_imap_endpoint(); + break; case ServiceProvider.YAHOO: - return ImapEngine.YahooAccount.IMAP_ENDPOINT; + imap_endpoint = ImapEngine.YahooAccount.generate_imap_endpoint(); + break; case ServiceProvider.OUTLOOK: - return ImapEngine.OutlookAccount.IMAP_ENDPOINT; + imap_endpoint = ImapEngine.OutlookAccount.generate_imap_endpoint(); + break; case ServiceProvider.OTHER: Endpoint.Flags imap_flags = Endpoint.Flags.GRACEFUL_DISCONNECT; @@ -469,24 +513,52 @@ public class Geary.AccountInformation : BaseObject { if (default_imap_server_starttls) imap_flags |= Endpoint.Flags.STARTTLS; - return new Endpoint(default_imap_server_host, default_imap_server_port, + imap_endpoint = new Endpoint(default_imap_server_host, default_imap_server_port, imap_flags, Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); + break; default: assert_not_reached(); } + + // look for existing one in the global pool; want to use that because Endpoint is mutable + // and signalled in such a way that it's better to share them + imap_endpoint = get_shared_endpoint(Service.IMAP, imap_endpoint); + + // bind shared Endpoint signal to this AccountInformation's signal + imap_endpoint.untrusted_host.connect(on_imap_untrusted_host); + + return imap_endpoint; } - + + private void on_imap_untrusted_host(Endpoint endpoint, Endpoint.SecurityType security, + TlsConnection cx) { + untrusted_host(endpoint, security, cx, Service.IMAP); + } + + /** + * Returns the {@link Endpoint} for the account's SMTP service. + * + * The Endpoint instance is guaranteed to be the same for the lifetime of the + * {@link AccountInformation} instance, which is in turn guaranteed to be the same for the + * duration of the application session. + */ public Endpoint get_smtp_endpoint() { + if (smtp_endpoint != null) + return smtp_endpoint; + switch (service_provider) { case ServiceProvider.GMAIL: - return ImapEngine.GmailAccount.SMTP_ENDPOINT; + smtp_endpoint = ImapEngine.GmailAccount.generate_smtp_endpoint(); + break; case ServiceProvider.YAHOO: - return ImapEngine.YahooAccount.SMTP_ENDPOINT; + smtp_endpoint = ImapEngine.YahooAccount.generate_smtp_endpoint(); + break; case ServiceProvider.OUTLOOK: - return ImapEngine.OutlookAccount.SMTP_ENDPOINT; + smtp_endpoint = ImapEngine.OutlookAccount.generate_smtp_endpoint(); + break; case ServiceProvider.OTHER: Endpoint.Flags smtp_flags = Endpoint.Flags.GRACEFUL_DISCONNECT; @@ -495,8 +567,36 @@ public class Geary.AccountInformation : BaseObject { if (default_smtp_server_starttls) smtp_flags |= Endpoint.Flags.STARTTLS; - return new Endpoint(default_smtp_server_host, default_smtp_server_port, + smtp_endpoint = new Endpoint(default_smtp_server_host, default_smtp_server_port, smtp_flags, Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); + break; + + default: + assert_not_reached(); + } + + // look for existing one in the global pool; want to use that because Endpoint is mutable + // and signalled in such a way that it's better to share them + smtp_endpoint = get_shared_endpoint(Service.SMTP, smtp_endpoint); + + // bind shared Endpoint signal to this AccountInformation's signal + smtp_endpoint.untrusted_host.connect(on_smtp_untrusted_host); + + return smtp_endpoint; + } + + private void on_smtp_untrusted_host(Endpoint endpoint, Endpoint.SecurityType security, + TlsConnection cx) { + untrusted_host(endpoint, security, cx, Service.SMTP); + } + + public Geary.Endpoint get_endpoint_for_service(Geary.Service service) { + switch (service) { + case Service.IMAP: + return get_imap_endpoint(); + + case Service.SMTP: + return get_smtp_endpoint(); default: assert_not_reached(); @@ -646,26 +746,21 @@ public class Geary.AccountInformation : BaseObject { } } - public async void clear_stored_passwords_async( - CredentialsMediator.ServiceFlag services) throws Error { + public async void clear_stored_passwords_async(ServiceFlag services) throws Error { Error? return_error = null; check_mediator_instance(); CredentialsMediator mediator = Geary.Engine.instance.authentication_mediator; try { - if (services.has_imap()) { - yield mediator.clear_password_async( - CredentialsMediator.Service.IMAP, this); - } + if (services.has_imap()) + yield mediator.clear_password_async(Service.IMAP, this); } catch (Error e) { return_error = e; } try { - if (services.has_smtp() && smtp_credentials != null) { - yield mediator.clear_password_async( - CredentialsMediator.Service.SMTP, this); - } + if (services.has_smtp() && smtp_credentials != null) + yield mediator.clear_password_async(Service.SMTP, this); } catch (Error e) { return_error = e; } @@ -685,8 +780,7 @@ public class Geary.AccountInformation : BaseObject { } try { - yield clear_stored_passwords_async(CredentialsMediator.ServiceFlag.IMAP - | CredentialsMediator.ServiceFlag.SMTP); + yield clear_stored_passwords_async(ServiceFlag.IMAP | ServiceFlag.SMTP); } catch (Error e) { debug("Error clearing SMTP password: %s", e.message); } diff --git a/src/engine/api/geary-credentials-mediator.vala b/src/engine/api/geary-credentials-mediator.vala index fa6b683c..9d1cdc1d 100644 --- a/src/engine/api/geary-credentials-mediator.vala +++ b/src/engine/api/geary-credentials-mediator.vala @@ -5,25 +5,6 @@ */ public interface Geary.CredentialsMediator : Object { - public enum Service { - IMAP, - SMTP; - } - - [Flags] - public enum ServiceFlag { - IMAP, - SMTP; - - public bool has_imap() { - return (this & IMAP) == IMAP; - } - - public bool has_smtp() { - return (this & SMTP) == SMTP; - } - } - /** * Query the key store for the password of the given username for the given * service. Return null if the password wasn't in the key store, or the diff --git a/src/engine/api/geary-endpoint.vala b/src/engine/api/geary-endpoint.vala index bf0ab51e..adbd98ae 100644 --- a/src/engine/api/geary-endpoint.vala +++ b/src/engine/api/geary-endpoint.vala @@ -10,6 +10,8 @@ */ public class Geary.Endpoint : BaseObject { + public const string PROP_TRUST_UNTRUSTED_HOST = "trust-untrusted-host"; + [Flags] public enum Flags { NONE = 0, @@ -26,6 +28,12 @@ public class Geary.Endpoint : BaseObject { } } + public enum SecurityType { + NONE, + SSL, + STARTTLS + } + public enum AttemptStarttls { YES, NO, @@ -38,6 +46,52 @@ public class Geary.Endpoint : BaseObject { public TlsCertificateFlags tls_validation_flags { get; set; default = TlsCertificateFlags.VALIDATE_ALL; } public bool force_ssl3 { get; set; default = false; } + /** + * When set, TLS has reported certificate issues. + * + * @see trust_untrusted_host + * @see untrusted_host + */ + public TlsCertificateFlags tls_validation_warnings { get; private set; default = 0; } + + /** + * The TLS certificate for an invalid or untrusted connection. + */ + public TlsCertificate? untrusted_certificate { get; private set; default = null; } + + /** + * When set, indicates the user has acceded to trusting the host even though TLS has reported + * certificate issues. + * + * Initialized to {@link Trillian.UNKNOWN}, meaning the user must decide when warnings are + * detected. + * + * @see untrusted_host + * @see tls_validation_warnings + */ + public Trillian trust_untrusted_host { get; set; default = Trillian.UNKNOWN; } + + /** + * Returns true if (a) no TLS warnings have been detected or (b) user has explicitly acceded + * to ignoring them and continuing the connection. + * + * This returns true if no connection has been attempted or connected and STARTTLS has not + * been issued. It's only when a connection is attempted can the certificate be examined + * and this can accurately return false. This behavior allows for a single code path to + * first attempt a connection and thereafter only attempt connections when TLS issues have + * been resolved by the user. + * + * @see tls_validation_warnings + * @see trust_untrusted_host + */ + public bool is_trusted_or_never_connected { + get { + return (tls_validation_warnings != 0) + ? trust_untrusted_host.is_certain() + : trust_untrusted_host.is_possible(); + } + } + public bool is_ssl { get { return flags.is_all_set(Flags.SSL); } } @@ -48,36 +102,48 @@ public class Geary.Endpoint : BaseObject { private SocketClient? socket_client = null; + /** + * Fired when TLS certificate warnings are detected and the caller has not marked this + * {@link Endpoint} as trusted via {@link trust_untrusted_host}. + * + * The connection will be closed when this is fired. The caller should query the user about + * how to deal with the situation. If user wants to proceed, set {@link trust_untrusted_host} + * to {@link Trillian.TRUE} and retry connection. + * + * @see tls_validation_warnings + */ + public signal void untrusted_host(SecurityType security, TlsConnection cx); + public Endpoint(string host_specifier, uint16 default_port, Flags flags, uint timeout_sec) { this.remote_address = new NetworkAddress(host_specifier, default_port); this.flags = flags; this.timeout_sec = timeout_sec; } - public SocketClient get_socket_client() { + private SocketClient get_socket_client() { if (socket_client != null) return socket_client; - + socket_client = new SocketClient(); - + if (is_ssl) { socket_client.set_tls(true); socket_client.set_tls_validation_flags(tls_validation_flags); socket_client.event.connect(on_socket_client_event); } - + socket_client.set_timeout(timeout_sec); - + return socket_client; } public async SocketConnection connect_async(Cancellable? cancellable = null) throws Error { SocketConnection cx = yield get_socket_client().connect_async(remote_address, cancellable); - + TcpConnection? tcp = cx as TcpConnection; if (tcp != null) tcp.set_graceful_disconnect(flags.is_all_set(Flags.GRACEFUL_DISCONNECT)); - + return cx; } @@ -110,20 +176,31 @@ public class Geary.Endpoint : BaseObject { } private bool on_accept_starttls_certificate(TlsConnection cx, TlsCertificate cert, TlsCertificateFlags flags) { - return report_tls_warnings("STARTTLS", flags); + return report_tls_warnings(SecurityType.STARTTLS, cx, cert, flags); } private bool on_accept_ssl_certificate(TlsConnection cx, TlsCertificate cert, TlsCertificateFlags flags) { - return report_tls_warnings("SSL", flags); + return report_tls_warnings(SecurityType.SSL, cx, cert, flags); } - private bool report_tls_warnings(string cx_type, TlsCertificateFlags warnings) { + private bool report_tls_warnings(SecurityType security, TlsConnection cx, TlsCertificate cert, + TlsCertificateFlags warnings) { // TODO: Report or verify flags with user, but for now merely log for informational/debugging // reasons and accede - message("%s TLS warnings connecting to %s: %Xh (%s)", cx_type, to_string(), warnings, + message("%s TLS warnings connecting to %s: %Xh (%s)", security.to_string(), to_string(), warnings, tls_flags_to_string(warnings)); - return true; + tls_validation_warnings = warnings; + untrusted_certificate = cert; + + // if user has marked this untrusted host as trusted already, accept warnings and move on + if (trust_untrusted_host == Trillian.TRUE) + return true; + + // signal an issue has been detected and return false to deny the connection + untrusted_host(security, cx); + + return false; } private string tls_flags_to_string(TlsCertificateFlags flags) { diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index 0491af62..58ce1764 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -80,7 +80,17 @@ public class Geary.Engine : BaseObject { * Fired when an account is deleted. */ public signal void account_removed(AccountInformation account); - + + /** + * Fired when an {@link Endpoint} associated with the {@link AccountInformation} reports + * TLS certificate warnings during connection. + * + * This may be fired during normal operation or while validating the AccountInformation, in + * which case there is no {@link Account} associated with it. + */ + public signal void untrusted_host(Geary.AccountInformation account_information, + Endpoint endpoint, Endpoint.SecurityType security, TlsConnection cx, Service service); + private Engine() { } @@ -101,6 +111,7 @@ public class Geary.Engine : BaseObject { is_initialized = true; + AccountInformation.init(); Logging.init(); RFC822.init(); ImapEngine.init(); @@ -239,6 +250,8 @@ public class Geary.Engine : BaseObject { if (!options.is_all_set(ValidationOption.CHECK_CONNECTIONS)) return error_code; + account.untrusted_host.connect(on_untrusted_host); + // validate IMAP, which requires logging in and establishing an AUTHORIZED cx state Geary.Imap.ClientSession? imap_session = new Imap.ClientSession(account.get_imap_endpoint()); try { @@ -286,13 +299,15 @@ public class Geary.Engine : BaseObject { } try { - yield smtp_session.logout_async(cancellable); + yield smtp_session.logout_async(true, cancellable); } catch (Error err) { // ignored } finally { smtp_session = null; } + account.untrusted_host.disconnect(on_untrusted_host); + return error_code; } @@ -352,8 +367,11 @@ public class Geary.Engine : BaseObject { accounts.set(account.email, account); if (!already_added) { + account.untrusted_host.connect(on_untrusted_host); + if (created) account_added(account); + account_available(account); } } @@ -372,6 +390,8 @@ public class Geary.Engine : BaseObject { } if (accounts.unset(account.email)) { + account.untrusted_host.disconnect(on_untrusted_host); + // Removal *MUST* be done in the following order: // 1. Send the account-unavailable signal. account_unavailable(account); @@ -386,5 +406,10 @@ public class Geary.Engine : BaseObject { account_instances.unset(account.email); } } + + private void on_untrusted_host(AccountInformation account_information, Endpoint endpoint, + Endpoint.SecurityType security, TlsConnection cx, Service service) { + untrusted_host(account_information, endpoint, security, cx, service); + } } diff --git a/src/engine/api/geary-service.vala b/src/engine/api/geary-service.vala new file mode 100644 index 00000000..77452938 --- /dev/null +++ b/src/engine/api/geary-service.vala @@ -0,0 +1,47 @@ +/* Copyright 2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * The type of mail service provided by a particular destination. + */ +public enum Geary.Service { + IMAP, + SMTP; + + /** + * Returns a user-visible label for the {@link Service}. + */ + public string user_label() { + switch (this) { + case IMAP: + return _("IMAP"); + + case SMTP: + return _("SMTP"); + + default: + assert_not_reached(); + } + } +} + +/** + * A bitfield of {@link Service}s. + */ +[Flags] +public enum Geary.ServiceFlag { + IMAP, + SMTP; + + public bool has_imap() { + return (this & IMAP) == IMAP; + } + + public bool has_smtp() { + return (this & SMTP) == SMTP; + } +} + diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index 22c82f4b..745967b4 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -197,8 +197,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu if (_account.information.smtp_credentials != null && !_account.information.smtp_credentials.is_complete()) { try { - yield _account.information.get_passwords_async( - CredentialsMediator.ServiceFlag.SMTP); + yield _account.information.get_passwords_async(ServiceFlag.SMTP); } catch (Error e) { debug("SMTP password fetch error: %s", e.message); } @@ -216,16 +215,26 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu } // Send the message, but only remove from database once sent + bool should_nap = false; + bool mail_sent = false; try { - debug("Outbox postman: Sending \"%s\" (ID:%s)...", message_subject(message), - row.outbox_id.to_string()); - yield send_email_async(message, null); + // only try if (a) no TLS issues or (b) user has acknowledged them and says to + // continue + if (_account.information.get_smtp_endpoint().is_trusted_or_never_connected) { + debug("Outbox postman: Sending \"%s\" (ID:%s)...", message_subject(message), + row.outbox_id.to_string()); + yield send_email_async(message, null); + mail_sent = true; + } else { + // user was warned via Geary.Engine signal, need to wait for that to be cleared + // befor sending + outbox_queue.send(row); + should_nap = true; + } } catch (Error send_err) { debug("Outbox postman send error, retrying: %s", send_err.message); - outbox_queue.send(row); - - bool should_nap = true; + should_nap = true; if (send_err is SmtpError.AUTHENTICATION_FAILED) { bool report = true; @@ -236,8 +245,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu // At this point we may already have a password in memory -- but it's incorrect. // Delete the current password, prompt the user for a new one, and try again. try { - if (yield _account.information.fetch_passwords_async( - CredentialsMediator.ServiceFlag.SMTP, true)) + if (yield _account.information.fetch_passwords_async(ServiceFlag.SMTP, true)) report = false; } catch (Error e) { debug("Error prompting for SMTP password: %s", e.message); @@ -245,16 +253,27 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu if (report) report_problem(Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED, send_err); + } else if (send_err is TlsError) { + // up to application to be aware of problem via Geary.Engine, but do nap and + // try later + debug("TLS connection warnings connecting to %s, user must confirm connection to continue", + _account.information.get_smtp_endpoint().to_string()); } else { report_problem(Geary.Account.Problem.EMAIL_DELIVERY_FAILURE, send_err); } + } + + if (should_nap) { + debug("Outbox napping for %u seconds...", send_retry_seconds); - if (should_nap) { - // Take a brief nap before continuing to allow connection problems to resolve. - yield Geary.Scheduler.sleep_async(send_retry_seconds); - send_retry_seconds *= 2; - send_retry_seconds = Geary.Numeric.uint_ceiling(send_retry_seconds, MAX_SEND_RETRY_INTERVAL_SEC); - } + // Take a brief nap before continuing to allow connection problems to resolve. + yield Geary.Scheduler.sleep_async(send_retry_seconds); + send_retry_seconds = Geary.Numeric.uint_ceiling(send_retry_seconds * 2, MAX_SEND_RETRY_INTERVAL_SEC); + } + + if (!mail_sent) { + // don't drop row until it's sent + outbox_queue.send(row); continue; } @@ -625,7 +644,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu // always logout try { - yield smtp.logout_async(cancellable); + yield smtp.logout_async(false, cancellable); } catch (Error err) { debug("Unable to disconnect from SMTP server %s: %s", smtp.to_string(), err.message); } diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala index 9ad9a29a..d409efc5 100644 --- a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala +++ b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala @@ -5,31 +5,21 @@ */ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount { - private static Geary.Endpoint? _imap_endpoint = null; - public static Geary.Endpoint IMAP_ENDPOINT { get { - if (_imap_endpoint == null) { - _imap_endpoint = new Geary.Endpoint( - "imap.gmail.com", - Imap.ClientConnection.DEFAULT_PORT_SSL, - Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); - } - - return _imap_endpoint; - } } + public static Geary.Endpoint generate_imap_endpoint() { + return new Geary.Endpoint( + "imap.gmail.com", + Imap.ClientConnection.DEFAULT_PORT_SSL, + Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); + } - private static Geary.Endpoint? _smtp_endpoint = null; - public static Geary.Endpoint SMTP_ENDPOINT { get { - if (_smtp_endpoint == null) { - _smtp_endpoint = new Geary.Endpoint( - "smtp.gmail.com", - Smtp.ClientConnection.DEFAULT_PORT_SSL, - Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); - } - - return _smtp_endpoint; - } } + public static Geary.Endpoint generate_smtp_endpoint() { + return new Geary.Endpoint( + "smtp.gmail.com", + Smtp.ClientConnection.DEFAULT_PORT_SSL, + Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); + } public GmailAccount(string name, Geary.AccountInformation account_information, Imap.Account remote, ImapDB.Account local) { diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 4ffd2688..92fef951 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -122,7 +122,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { // IMAP password before attempting a connection. This might have to be // reworked when we allow passwordless logins. if (!information.imap_credentials.is_complete()) - yield information.fetch_passwords_async(Geary.CredentialsMediator.ServiceFlag.IMAP); + yield information.fetch_passwords_async(ServiceFlag.IMAP); try { yield local.open_async(information.settings_dir, Engine.instance.resource_dir.get_child("sql"), @@ -853,7 +853,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { private async void do_login_failed_async(Geary.Credentials? credentials) { try { - if (yield information.fetch_passwords_async(CredentialsMediator.ServiceFlag.IMAP, true)) + if (yield information.fetch_passwords_async(ServiceFlag.IMAP, true)) return; } catch (Error e) { debug("Error prompting for IMAP password: %s", e.message); diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala index c40d0ea7..d8ef8d63 100644 --- a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala +++ b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala @@ -5,31 +5,21 @@ */ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount { - private static Geary.Endpoint? _imap_endpoint = null; - public static Geary.Endpoint IMAP_ENDPOINT { get { - if (_imap_endpoint == null) { - _imap_endpoint = new Geary.Endpoint( - "imap-mail.outlook.com", - Imap.ClientConnection.DEFAULT_PORT_SSL, - Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); - } - - return _imap_endpoint; - } } + public static Geary.Endpoint generate_imap_endpoint() { + return new Geary.Endpoint( + "imap-mail.outlook.com", + Imap.ClientConnection.DEFAULT_PORT_SSL, + Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); + } - private static Geary.Endpoint? _smtp_endpoint = null; - public static Geary.Endpoint SMTP_ENDPOINT { get { - if (_smtp_endpoint == null) { - _smtp_endpoint = new Geary.Endpoint( - "smtp-mail.outlook.com", - Smtp.ClientConnection.DEFAULT_PORT_STARTTLS, - Geary.Endpoint.Flags.STARTTLS | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); - } - - return _smtp_endpoint; - } } + public static Geary.Endpoint generate_smtp_endpoint() { + return new Geary.Endpoint( + "smtp-mail.outlook.com", + Smtp.ClientConnection.DEFAULT_PORT_STARTTLS, + Geary.Endpoint.Flags.STARTTLS | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); + } public OutlookAccount(string name, AccountInformation account_information, Imap.Account remote, ImapDB.Account local) { diff --git a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala index 01a3086b..dafc9933 100644 --- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala +++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala @@ -5,31 +5,21 @@ */ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount { - private static Geary.Endpoint? _imap_endpoint = null; - public static Geary.Endpoint IMAP_ENDPOINT { get { - if (_imap_endpoint == null) { - _imap_endpoint = new Geary.Endpoint( - "imap.mail.yahoo.com", - Imap.ClientConnection.DEFAULT_PORT_SSL, - Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); - } - - return _imap_endpoint; - } } + public static Geary.Endpoint generate_imap_endpoint() { + return new Geary.Endpoint( + "imap.mail.yahoo.com", + Imap.ClientConnection.DEFAULT_PORT_SSL, + Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC); + } - private static Geary.Endpoint? _smtp_endpoint = null; - public static Geary.Endpoint SMTP_ENDPOINT { get { - if (_smtp_endpoint == null) { - _smtp_endpoint = new Geary.Endpoint( - "smtp.mail.yahoo.com", - Smtp.ClientConnection.DEFAULT_PORT_SSL, - Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, - Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); - } - - return _smtp_endpoint; - } } + public static Geary.Endpoint generate_smtp_endpoint() { + return new Geary.Endpoint( + "smtp.mail.yahoo.com", + Smtp.ClientConnection.DEFAULT_PORT_SSL, + Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT, + Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); + } private static Gee.HashMap? special_map = null; diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index 95528e71..c821d643 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -50,6 +50,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject { private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex(); private Gee.HashSet reserved_sessions = new Gee.HashSet(); private bool authentication_failed = false; + private bool untrusted_host = false; private uint authorized_session_error_retry_timeout_id = 0; public signal void login_failed(); @@ -58,11 +59,19 @@ public class Geary.Imap.ClientSessionManager : BaseObject { this.account_information = account_information; account_information.notify["imap-credentials"].connect(on_imap_credentials_notified); + account_information.get_imap_endpoint().untrusted_host.connect(on_imap_untrusted_host); + account_information.get_imap_endpoint().notify[Endpoint.PROP_TRUST_UNTRUSTED_HOST].connect( + on_imap_trust_untrusted_host); } ~ClientSessionManager() { if (is_open) warning("Destroying opened ClientSessionManager"); + + account_information.notify["imap-credentials"].disconnect(on_imap_credentials_notified); + account_information.get_imap_endpoint().untrusted_host.disconnect(on_imap_untrusted_host); + account_information.get_imap_endpoint().notify[Endpoint.PROP_TRUST_UNTRUSTED_HOST].disconnect( + on_imap_trust_untrusted_host); } public async void open_async(Cancellable? cancellable) throws Error { @@ -136,7 +145,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject { return; } - while ((sessions.size + pending_sessions) < min_pool_size && !authentication_failed && is_open) + while ((sessions.size + pending_sessions) < min_pool_size && !authentication_failed && is_open && !untrusted_host) schedule_new_authorized_session(); try { @@ -184,6 +193,9 @@ public class Geary.Imap.ClientSessionManager : BaseObject { if (authentication_failed) throw new ImapError.UNAUTHENTICATED("Invalid ClientSessionManager credentials"); + if (untrusted_host) + throw new ImapError.UNAUTHENTICATED("Untrusted host %s", account_information.get_imap_endpoint().to_string()); + ClientSession new_session = new ClientSession(account_information.get_imap_endpoint()); // add session to pool before launching all the connect activity so error cases can properly @@ -427,6 +439,23 @@ public class Geary.Imap.ClientSessionManager : BaseObject { return removed; } + private void on_imap_untrusted_host() { + // this is called any time trust issues are detected, so immediately clutch in to stop + // retries + untrusted_host = true; + } + + private void on_imap_trust_untrusted_host() { + // fired when the trust_untrusted_host property changes, indicating if the user has agreed + // to ignore the trust problems and continue connecting + if (untrusted_host && account_information.get_imap_endpoint().trust_untrusted_host == Trillian.TRUE) { + untrusted_host = false; + + if (is_open) + adjust_session_pool.begin(); + } + } + /** * Use only for debugging and logging. */ diff --git a/src/engine/smtp/smtp-client-connection.vala b/src/engine/smtp/smtp-client-connection.vala index f629a641..6d4bf1cc 100644 --- a/src/engine/smtp/smtp-client-connection.vala +++ b/src/engine/smtp/smtp-client-connection.vala @@ -9,7 +9,7 @@ public class Geary.Smtp.ClientConnection { public const uint16 DEFAULT_PORT_SSL = 465; public const uint16 DEFAULT_PORT_STARTTLS = 587; - public const uint DEFAULT_TIMEOUT_SEC = 60; + public const uint DEFAULT_TIMEOUT_SEC = 20; public Geary.Smtp.Capabilities? capabilities { get; private set; default = null; } @@ -48,9 +48,18 @@ public class Geary.Smtp.ClientConnection { if (cx == null) return false; - yield cx.close_async(Priority.DEFAULT, cancellable); + Error? disconnect_error = null; + try { + yield cx.close_async(Priority.DEFAULT, cancellable); + } catch (Error err) { + disconnect_error = err; + } + cx = null; + if (disconnect_error != null) + throw disconnect_error; + return true; } diff --git a/src/engine/smtp/smtp-client-session.vala b/src/engine/smtp/smtp-client-session.vala index a3b96a1d..db950f61 100644 --- a/src/engine/smtp/smtp-client-session.vala +++ b/src/engine/smtp/smtp-client-session.vala @@ -109,10 +109,11 @@ public class Geary.Smtp.ClientSession { throw new SmtpError.AUTHENTICATION_FAILED("Unable to authenticate with %s", to_string()); } - public async Response? logout_async(Cancellable? cancellable = null) throws Error { + public async Response? logout_async(bool force, Cancellable? cancellable = null) throws Error { Response? response = null; try { - response = yield cx.quit_async(cancellable); + if (!force) + response = yield cx.quit_async(cancellable); } catch (Error err) { // catch because although error occurred, still attempt to close the connection message("Unable to QUIT: %s", err.message); diff --git a/src/mailer/main.vala b/src/mailer/main.vala index 50daab21..8fac48ff 100644 --- a/src/mailer/main.vala +++ b/src/mailer/main.vala @@ -53,7 +53,7 @@ async void main_async() throws Error { stdout.printf("Sent email #%d\n", ctr); } - Geary.Smtp.Response? logout = yield session.logout_async(); + Geary.Smtp.Response? logout = yield session.logout_async(false); stdout.printf("%s\n", logout.to_string()); } diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index b04dbe25..cdf0f7b4 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -5,6 +5,7 @@ install(FILES account_list.glade DESTINATION ${UI_DEST}) install(FILES account_cannot_remove.glade DESTINATION ${UI_DEST}) install(FILES account_spinner.glade DESTINATION ${UI_DEST}) install(FILES app_menu.interface DESTINATION ${UI_DEST}) +install(FILES certificate_warning_dialog.glade DESTINATION ${UI_DEST}) install(FILES composer.glade DESTINATION ${UI_DEST}) install(FILES composer_accelerators.ui DESTINATION ${UI_DEST}) install(FILES find_bar.glade DESTINATION ${UI_DEST}) diff --git a/ui/certificate_warning_dialog.glade b/ui/certificate_warning_dialog.glade new file mode 100644 index 00000000..e6ff2d33 --- /dev/null +++ b/ui/certificate_warning_dialog.glade @@ -0,0 +1,216 @@ + + + + + + False + Untrusted Connection + True + True + dialog + True + + + False + 12 + 12 + 12 + 12 + vertical + 2 + + + False + end + 8 + end + + + _Always Trust This Server + True + True + True + True + + + False + True + 0 + + + + + _Trust This Server + True + True + True + True + + + False + True + 1 + + + + + _Don't Trust This Server + True + True + True + True + + + False + True + 2 + + + + + False + True + end + 0 + + + + + True + False + vertical + 8 + + + True + False + 6 + + + True + False + security-high-symbolic + 6 + + + False + True + 0 + + + + + True + False + (empty) + + + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 8 + 0 + (empty) + True + + + False + True + 2 + + + + + True + False + 16 + 0 + (empty) + True + 80 + + + False + True + 3 + + + + + True + False + 0 + (empty) + True + + + False + True + end + 4 + + + + + True + False + end + 0 + (empty) + True + True + + + False + True + end + 5 + + + + + True + False + 0 + (empty) + True + + + False + True + end + 6 + + + + + True + True + 1 + + + + + + always_trust_button + trust_button + dont_trust_button + + +