[geary] Present user with dialog when TLS cert warnings detected: Bug #713247



commit 809128692cd23edeaaae827623f0bfe22b808218
Author: Jim Nelson <jim yorba org>
Date:   Thu Aug 28 17:27:50 2014 -0700

    Present user with dialog when TLS cert warnings detected: Bug #713247
    
    When a TLS certificate warning is detected, the user will now be
    presented with a warning dialog presenting them with three options:
    Trust This Server, Always Trust This Server, and Don't Trust This
    Server (the default).  The user must select one of the first two
    buttons for Geary to continue connecting to the server, otherwise it
    will close the Account object for the duration of the application
    session.
    
    This patch introduces a dependency on gcr-3, which is used to pin TLS
    certificates (i.e. persist the user's choice of ignoring the TLS
    warnings).

 debian/control                                     |    6 +-
 po/POTFILES.in                                     |    3 +
 src/CMakeLists.txt                                 |    6 +-
 src/client/accounts/account-dialog.vala            |   41 ++--
 src/client/application/geary-args.vala             |    2 +
 src/client/application/geary-controller.vala       |  241 +++++++++++++++++++-
 src/client/application/secret-mediator.vala        |   20 +-
 src/client/dialogs/certificate-warning-dialog.vala |  114 +++++++++
 src/client/dialogs/password-dialog.vala            |    2 +-
 src/engine/api/geary-account-information.vala      |  206 ++++++++++++-----
 src/engine/api/geary-credentials-mediator.vala     |   19 --
 src/engine/api/geary-endpoint.vala                 |  101 +++++++-
 src/engine/api/geary-engine.vala                   |   29 +++-
 src/engine/api/geary-service.vala                  |   47 ++++
 src/engine/imap-db/outbox/smtp-outbox-folder.vala  |   53 +++--
 .../gmail/imap-engine-gmail-account.vala           |   38 ++--
 .../imap-engine/imap-engine-generic-account.vala   |    4 +-
 .../outlook/imap-engine-outlook-account.vala       |   38 ++--
 .../yahoo/imap-engine-yahoo-account.vala           |   38 ++--
 .../transport/imap-client-session-manager.vala     |   31 +++-
 src/engine/smtp/smtp-client-connection.vala        |   13 +-
 src/engine/smtp/smtp-client-session.vala           |    5 +-
 src/mailer/main.vala                               |    2 +-
 ui/CMakeLists.txt                                  |    1 +
 ui/certificate_warning_dialog.glade                |  216 ++++++++++++++++++
 25 files changed, 1058 insertions(+), 218 deletions(-)
---
diff --git a/debian/control b/debian/control
index 9896e11..894b506 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 5983043..32ae9a4 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 7e8a6e1..5a78661 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 b64df13..192be9d 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 a26a9c8..70b9159 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 2c4b4f8..990b548 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<string> pending_mailtos = new Gee.ArrayList<string>();
+    private Geary.Nonblocking.Mutex untrusted_host_prompt_mutex = new Geary.Nonblocking.Mutex();
+    private Gee.HashSet<Geary.Endpoint> validating_endpoints = new Gee.HashSet<Geary.Endpoint>();
     
     // List of windows we're waiting to close before Geary closes.
     private Gee.List<ComposerWidget> waiting_to_close = new Gee.ArrayList<ComposerWidget>();
@@ -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 b06224d..e1ed1ae 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 0000000..d3d9f01
--- /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 = "&#8226; ";
+    
+    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 =
+            "<b>"
+            +_("Selecting \"Trust This Server\" or \"Always Trust This Server\" may cause your username and 
password to be transmitted insecurely.")
+            + "</b>";
+        trust_label.use_markup = true;
+        
+        if (is_validation) {
+            // could be a new or existing account
+            dont_trust_label.label =
+                "<b>"
+                + _("Selecting \"Don't Trust This Server\" will cause Geary not to access this server.")
+                + "</b> "
+                + _("Geary will not add or update this email account.");
+        } else {
+            // a registered account
+            dont_trust_label.label =
+                "<b>"
+                + _("Selecting \"Don't Trust This Server\" will cause Geary to stop accessing this account.")
+                + "</b> "
+                + _("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 96296fd..8ca620d 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 c00dbb8..926dc27 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<string, Geary.Endpoint>? 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<string, Geary.Endpoint>();
+    }
+    
+    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 fa6b683..9d1cdc1 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 bf0ab51..adbd98a 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 0491af6..58ce176 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 0000000..7745293
--- /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 22c82f4..745967b 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 9ad9a29..d409efc 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 4ffd268..92fef95 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 c40d0ea..d8ef8d6 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 01a3086..dafc993 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<Geary.FolderPath, Geary.SpecialFolderType>? 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 95528e7..c821d64 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<ClientSession> reserved_sessions = new Gee.HashSet<ClientSession>();
     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 f629a64..6d4bf1c 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 a3b96a1..db950f6 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 50daab2..8fac48f 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 b04dbe2..cdf0f7b 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 0000000..e6ff2d3
--- /dev/null
+++ b/ui/certificate_warning_dialog.glade
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <object class="GtkDialog" id="CertificateWarningDialog">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Untrusted Connection</property>
+    <property name="modal">True</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="urgency_hint">True</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox1">
+        <property name="can_focus">False</property>
+        <property name="margin_left">12</property>
+        <property name="margin_right">12</property>
+        <property name="margin_top">12</property>
+        <property name="margin_bottom">12</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+            <property name="valign">end</property>
+            <property name="margin_top">8</property>
+            <property name="layout_style">end</property>
+            <child>
+              <object class="GtkButton" id="always_trust_button">
+                <property name="label" translatable="yes">_Always Trust This Server</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="trust_button">
+                <property name="label" translatable="yes">_Trust This Server</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="dont_trust_button">
+                <property name="label" translatable="yes">_Don't Trust This Server</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="box1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">8</property>
+            <child>
+              <object class="GtkBox" id="box2">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkImage" id="image1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="icon_name">security-high-symbolic</property>
+                    <property name="icon_size">6</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="untrusted_connection_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label">(empty)</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="top_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">8</property>
+                <property name="xalign">0</property>
+                <property name="label">(empty)</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="warnings_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">16</property>
+                <property name="xalign">0</property>
+                <property name="label">(empty)</property>
+                <property name="wrap">True</property>
+                <property name="max_width_chars">80</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">3</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="contact_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="label">(empty)</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="pack_type">end</property>
+                <property name="position">4</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="dont_trust_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="valign">end</property>
+                <property name="xalign">0</property>
+                <property name="label">(empty)</property>
+                <property name="use_markup">True</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="pack_type">end</property>
+                <property name="position">5</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="trust_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="label">(empty)</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="pack_type">end</property>
+                <property name="position">6</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="2">always_trust_button</action-widget>
+      <action-widget response="1">trust_button</action-widget>
+      <action-widget response="0">dont_trust_button</action-widget>
+    </action-widgets>
+  </object>
+</interface>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]