Commit 80912869 authored by Jim Nelson's avatar Jim Nelson

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).
parent 0e5c3343
......@@ -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.
......
......@@ -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
......
......@@ -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
)
......
......@@ -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() {
......
......@@ -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) {
......
......@@ -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 {
......
/* Copyright 2014 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class CertificateWarningDialog {
public enum Result {
DONT_TRUST,
TRUST,
ALWAYS_TRUST
}
private const string BULLET = "• ";
private Gtk.Dialog dialog;
public CertificateWarningDialog(Gtk.Window? parent, Geary.AccountInformation account_information,
Geary.Service service, TlsCertificateFlags warnings, bool is_validation) {
Gtk.Builder builder = GearyApplication.instance.create_builder("certificate_warning_dialog.glade");
dialog = (Gtk.Dialog) builder.get_object("CertificateWarningDialog");
dialog.transient_for = parent;
dialog.modal = true;
Gtk.Label title_label = (Gtk.Label) builder.get_object("untrusted_connection_label");
Gtk.Label top_label = (Gtk.Label) builder.get_object("top_label");
Gtk.Label warnings_label = (Gtk.Label) builder.get_object("warnings_label");
Gtk.Label trust_label = (Gtk.Label) builder.get_object("trust_label");
Gtk.Label dont_trust_label = (Gtk.Label) builder.get_object("dont_trust_label");
Gtk.Label contact_label = (Gtk.Label) builder.get_object("contact_label");
title_label.label = _("Untrusted Connection: %s").printf(account_information.email);
Geary.Endpoint endpoint = account_information.get_endpoint_for_service(service);
top_label.label = _("The identity of the %s mail server at %s:%u could not be verified.").printf(
service.user_label(), endpoint.remote_address.hostname, endpoint.remote_address.port);
warnings_label.label = generate_warning_list(warnings);
warnings_label.use_markup = true;
trust_label.label =
"<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;
}
}
}
......@@ -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");
......
......@@ -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
......
......@@ -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) {
......
......@@ -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);
}
}
/* 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;
}
}
......@@ -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);