diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 285d0ed5..eea8e1b8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,7 +31,6 @@ engine/api/geary-email-identifier.vala engine/api/geary-email-properties.vala engine/api/geary-email.vala engine/api/geary-endpoint.vala -engine/api/geary-engine-account.vala engine/api/geary-engine-error.vala engine/api/geary-engine.vala engine/api/geary-folder.vala diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index 6157e9a6..29d5aa13 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -70,7 +70,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., private static GearyApplication _instance = null; private GearyController? controller = null; - private Geary.EngineAccount? account = null; + private Geary.Account? account = null; private File exec_dir; @@ -177,7 +177,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., return; } - private void set_account(Geary.EngineAccount? account) { + private void set_account(Geary.Account? account) { if (this.account == account) return; @@ -193,19 +193,22 @@ along with Geary; if not, write to the Free Software Foundation, Inc., } private void initialize_account() { - string? username = get_username(); - if (username == null) - create_account(username); + Geary.AccountInformation? account_information = get_account(); + if (account_information == null) + create_account(null); else - open_account(username); + open_account(account_information.email, null, null, PasswordTypeFlag.IMAP | PasswordTypeFlag.SMTP, null); } - private void create_account(string? username) { + private void create_account(string? email) { Geary.AccountInformation? old_account_information = null; - if (username != null) { - Geary.Credentials credentials = new Geary.Credentials(username, null); - old_account_information = new Geary.AccountInformation(credentials); - old_account_information.load_info_from_file(); + if (email != null) { + try { + old_account_information = Geary.Engine.get_account_for_email(email); + } catch (Error err) { + debug("Unable to open account information for %s, creating instead: %s", email, + err.message); + } } Geary.AccountInformation account_information = @@ -262,23 +265,31 @@ along with Geary; if not, write to the Free Software Foundation, Inc., return new_account_information; } - private void open_account(string username, string? old_password = null, Cancellable? cancellable = null) { - Geary.Credentials credentials = new Geary.Credentials(username, null); - Geary.AccountInformation account_information = new Geary.AccountInformation(credentials); - account_information.load_info_from_file(); + private void open_account(string email, string? old_imap_password, string? old_smtp_password, + PasswordTypeFlag password_flags, Cancellable? cancellable) { + Geary.AccountInformation account_information; + try { + account_information = Geary.Engine.get_account_for_email(email); + } catch (Error err) { + error("Unable to open account information for label %s: %s", email, err.message); + } - bool remember_password = account_information.remember_password; - string? password = get_password(account_information.credentials.user, old_password, ref remember_password); - // If there was no saved password and the user refused to enter a password. - if (password == null) { + bool imap_remember_password = account_information.imap_remember_password; + bool smtp_remember_password = account_information.smtp_remember_password; + string? imap_password, smtp_password; + get_passwords(account_information, password_flags, ref imap_remember_password, + ref smtp_remember_password, out imap_password, out smtp_password); + if (imap_password == null || smtp_password == null) { set_account(null); return; } - account_information.remember_password = remember_password; + account_information.imap_remember_password = imap_remember_password; + account_information.smtp_remember_password = smtp_remember_password; account_information.store_async.begin(cancellable); - account_information.credentials.pass = password; + account_information.imap_credentials.pass = imap_password; + account_information.smtp_credentials.pass = smtp_password; try { set_account(account_information.get_account()); @@ -290,29 +301,34 @@ along with Geary; if not, write to the Free Software Foundation, Inc., } } - private string? get_username() { + private Geary.AccountInformation? get_account() { try { - Gee.List usernames = Geary.Engine.get_usernames(); - if (usernames.size > 0) { - string username = usernames.get(0); - return Geary.String.is_empty(username) ? null : username; - } + Gee.List accounts = Geary.Engine.get_accounts(); + if (accounts.size > 0) + return accounts[0]; } catch (Error e) { - debug("Unable to fetch accounts. Error: %s", e.message); + debug("Unable to fetch account labels: %s", e.message); } return null; } - private string? get_password(string username, string? old_password, ref bool remember_password) { - string? password = null; - if (old_password == null && remember_password) - password = keyring_get_password(username); + private void get_passwords(Geary.AccountInformation old_account_information, + PasswordTypeFlag password_flags, ref bool imap_remember_password, + ref bool smtp_remember_password, out string? imap_password, out string? smtp_password) { + imap_password = null; + if (!old_account_information.imap_credentials.is_complete() && imap_remember_password) + imap_password = keyring_get_password(old_account_information.imap_credentials.user, PasswordType.IMAP); - if (Geary.String.is_null_or_whitespace(password)) - password = request_password(username, old_password, out remember_password); + smtp_password = null; + if (!old_account_information.smtp_credentials.is_complete() && smtp_remember_password) + smtp_password = keyring_get_password(old_account_information.smtp_credentials.user, PasswordType.SMTP); - return password; + if (Geary.String.is_empty_or_whitespace(imap_password) || + Geary.String.is_empty_or_whitespace(smtp_password)) { + request_passwords(old_account_information, password_flags, out imap_password, + out smtp_password, out imap_remember_password, out smtp_remember_password); + } } private string get_default_real_name() { @@ -320,30 +336,53 @@ along with Geary; if not, write to the Free Software Foundation, Inc., return real_name == "Unknown" ? "" : real_name; } - private string? request_password(string username, string? old_password, out bool remember_password) { - Geary.Credentials credentials = new Geary.Credentials(username, old_password); + private void request_passwords(Geary.AccountInformation old_account_information, + PasswordTypeFlag password_flags, out string? imap_password, out string? smtp_password, + out bool imap_remember_password, out bool smtp_remember_password) { + Geary.AccountInformation temp_account_information; + try { + temp_account_information = Geary.Engine.get_account_for_email(old_account_information.email); + } catch (Error err) { + error("Unable to open account information for %s: %s", old_account_information.email, + err.message); + } - Geary.AccountInformation old_account_information = new Geary.AccountInformation(credentials); - old_account_information.load_info_from_file(); + temp_account_information.imap_credentials = old_account_information.imap_credentials.copy(); + temp_account_information.smtp_credentials = old_account_information.smtp_credentials.copy(); - PasswordDialog password_dialog = new PasswordDialog(old_account_information, old_password == null); + bool first_try = !temp_account_information.imap_credentials.is_complete() || + !temp_account_information.smtp_credentials.is_complete(); + PasswordDialog password_dialog = new PasswordDialog(temp_account_information, first_try, + password_flags); if (!password_dialog.run()) { exit(1); - remember_password = false; - return null; + imap_password = null; + smtp_password = null; + imap_remember_password = false; + smtp_remember_password = false; + return; } // password_dialog.password should never be null at this point. It will only be null when // password_dialog.run() returns false, in which case we have already exited/returned. - string? password = password_dialog.password; - remember_password = password_dialog.remember_password; + imap_password = password_dialog.imap_password; + smtp_password = password_dialog.smtp_password; + imap_remember_password = password_dialog.imap_remember_password; + smtp_remember_password = password_dialog.smtp_remember_password; - if (remember_password) - keyring_save_password(new Geary.Credentials(username, password)); - else - keyring_delete_password(username); + if (imap_remember_password) { + keyring_save_password(new Geary.Credentials(temp_account_information.imap_credentials.user, + imap_password), PasswordType.IMAP); + } else { + keyring_delete_password(temp_account_information.imap_credentials.user, PasswordType.IMAP); + } - return password; + if (smtp_remember_password) { + keyring_save_password(new Geary.Credentials(temp_account_information.smtp_credentials.user, + smtp_password), PasswordType.SMTP); + } else { + keyring_delete_password(temp_account_information.smtp_credentials.user, PasswordType.SMTP); + } } // Prompt the user for a service, real name, username, and password, and try to start Geary. @@ -359,15 +398,26 @@ along with Geary; if not, write to the Free Software Foundation, Inc., return null; } - if (login_dialog.account_information.remember_password) - keyring_save_password(login_dialog.account_information.credentials); - else - keyring_delete_password(login_dialog.account_information.credentials.user); - + if (login_dialog.account_information.imap_remember_password) { + keyring_save_password(login_dialog.account_information.imap_credentials, + PasswordType.IMAP); + } else { + keyring_delete_password(login_dialog.account_information.imap_credentials.user, + PasswordType.IMAP); + } + + if (login_dialog.account_information.smtp_remember_password) { + keyring_save_password(login_dialog.account_information.smtp_credentials, + PasswordType.SMTP); + } else { + keyring_delete_password(login_dialog.account_information.smtp_credentials.user, + PasswordType.SMTP); + } + return login_dialog.account_information; } - private void on_report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, + private void on_report_problem(Geary.Account.Problem problem, Geary.AccountSettings settings, Error? err) { debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)"); switch (problem) { @@ -377,11 +427,18 @@ along with Geary; if not, write to the Free Software Foundation, Inc., // TODO break; - case Geary.Account.Problem.LOGIN_FAILED: - debug("Login failed."); - Geary.Credentials old_credentials = account.settings.credentials; + // TODO: Different password dialog prompt for SMTP or IMAP login. + case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED: account.report_problem.disconnect(on_report_problem); - open_account(old_credentials.user, old_credentials.pass); + open_account(account.settings.email.address, account.settings.imap_credentials.pass, + account.settings.smtp_credentials.pass, PasswordTypeFlag.IMAP, null); + break; + + // TODO: Different password dialog prompt for SMTP or IMAP login. + case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED: + account.report_problem.disconnect(on_report_problem); + open_account(account.settings.email.address, account.settings.imap_credentials.pass, + account.settings.smtp_credentials.pass, PasswordTypeFlag.SMTP, null); break; default: diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 6d27fee2..561cf210 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -67,7 +67,7 @@ public class GearyController { public MainWindow main_window { get; private set; } - private Geary.EngineAccount? account = null; + private Geary.Account? account = null; private Cancellable cancellable_folder = new Cancellable(); private Cancellable cancellable_inbox = new Cancellable(); private Cancellable cancellable_message = new Cancellable(); @@ -246,7 +246,7 @@ public class GearyController { return entries; } - public async void connect_account_async(Geary.EngineAccount? new_account, Cancellable? cancellable) { + public async void connect_account_async(Geary.Account? new_account, Cancellable? cancellable) { if (account == new_account) return; @@ -297,8 +297,7 @@ public class GearyController { if (account.settings.service_provider == Geary.ServiceProvider.YAHOO) main_window.title = GearyApplication.NAME + "!"; - // TODO: Change this when Geary no longer assumes username is email addresss. - main_window.message_list_store.account_owner_email = account.settings.credentials.user; + main_window.message_list_store.account_owner_email = account.settings.email.address; main_window.folder_list.set_user_folders_root_name(_("Labels")); load_folders.begin(cancellable_folder); @@ -538,7 +537,7 @@ public class GearyController { // Clear view before we yield, to make sure it happens if (clear_view) { - main_window.message_viewer.clear(current_folder); + main_window.message_viewer.clear(current_folder, account.settings); main_window.message_viewer.scroll_reset(); main_window.message_viewer.external_images_info_bar.hide(); } @@ -1139,17 +1138,9 @@ public class GearyController { } private Geary.RFC822.MailboxAddress get_sender() { - string username; - try { - // TODO: Multiple accounts. - username = Geary.Engine.get_usernames().get(0); - } catch (Error e) { - error("Unable to get username. Error: %s", e.message); - } - - return new Geary.RFC822.MailboxAddress(account.settings.real_name, username); + return account.settings.email; } - + private Geary.RFC822.MailboxAddresses get_from() { return new Geary.RFC822.MailboxAddresses.single(get_sender()); } diff --git a/src/client/ui/contact-entry-completion.vala b/src/client/ui/contact-entry-completion.vala index b29b2a3f..69da00e8 100644 --- a/src/client/ui/contact-entry-completion.vala +++ b/src/client/ui/contact-entry-completion.vala @@ -97,7 +97,7 @@ public class ContactEntryCompletion : Gtk.EntryCompletion { Gee.List addresses = get_addresses(sender, out current_address_index, null, out current_address_remainder); addresses[current_address_index] = full_address; - if (!Geary.String.is_null_or_whitespace(current_address_remainder)) + if (!Geary.String.is_empty_or_whitespace(current_address_remainder)) addresses.insert(current_address_index + 1, current_address_remainder); string delimiter = ", "; entry.text = concat_strings(addresses, delimiter); diff --git a/src/client/ui/geary-login.vala b/src/client/ui/geary-login.vala index 6e5f57d3..97456b9e 100644 --- a/src/client/ui/geary-login.vala +++ b/src/client/ui/geary-login.vala @@ -7,23 +7,35 @@ // Displays a dialog for collecting the user's login data. public class LoginDialog { private Gtk.Dialog dialog; - private Gtk.Entry entry_username; + private Gtk.Entry entry_email; + private Gtk.Label label_password; private Gtk.Entry entry_password; private Gtk.Entry entry_real_name; private Gtk.ComboBoxText combo_service; private Gtk.CheckButton check_remember_password; private Gtk.Alignment other_info; + + // IMAP info widgets private Gtk.Entry entry_imap_host; private Gtk.Entry entry_imap_port; + private Gtk.Entry entry_imap_username; + private Gtk.Entry entry_imap_password; + private Gtk.CheckButton check_imap_remember_password; private Gtk.RadioButton radio_imap_none; private Gtk.RadioButton radio_imap_ssl; private Gtk.RadioButton radio_imap_starttls; + + // SMTP info widgets private Gtk.Entry entry_smtp_host; private Gtk.Entry entry_smtp_port; + private Gtk.Entry entry_smtp_username; + private Gtk.Entry entry_smtp_password; + private Gtk.CheckButton check_smtp_remember_password; private Gtk.RadioButton radio_smtp_none; private Gtk.RadioButton radio_smtp_ssl; private Gtk.RadioButton radio_smtp_starttls; + private Gtk.Button ok_button; private bool edited_imap_port = false; @@ -31,49 +43,80 @@ public class LoginDialog { public Geary.AccountInformation account_information { get; private set; } + // TODO: Update the login dialog to use email, imap_credentials, smtp_credentials, + // imap_remember_password, and smtp_remember_password. public LoginDialog.from_account_information(Geary.AccountInformation initial_account_information) { - this(initial_account_information.real_name, initial_account_information.credentials.user, - initial_account_information.credentials.pass, initial_account_information.remember_password, - initial_account_information.service_provider, initial_account_information.default_imap_server_host, - initial_account_information.default_imap_server_port, initial_account_information.default_imap_server_ssl, - initial_account_information.default_imap_server_starttls, initial_account_information.default_smtp_server_host, - initial_account_information.default_smtp_server_port, initial_account_information.default_smtp_server_ssl, + this(initial_account_information.real_name, + initial_account_information.email, + initial_account_information.imap_credentials.user, + initial_account_information.imap_credentials.pass, + initial_account_information.imap_remember_password, + initial_account_information.smtp_credentials.user, + initial_account_information.smtp_credentials.pass, + initial_account_information.smtp_remember_password, + initial_account_information.service_provider, + initial_account_information.default_imap_server_host, + initial_account_information.default_imap_server_port, + initial_account_information.default_imap_server_ssl, + initial_account_information.default_imap_server_starttls, + initial_account_information.default_smtp_server_host, + initial_account_information.default_smtp_server_port, + initial_account_information.default_smtp_server_ssl, initial_account_information.default_smtp_server_starttls); } - public LoginDialog(string? initial_real_name = null, string? initial_username = null, - string? initial_password = null, bool initial_remember_password = true, - int initial_service_provider = -1, string? initial_default_imap_host = null, + public LoginDialog( + string? initial_real_name = null, + string? initial_email = null, + string? initial_imap_username = null, + string? initial_imap_password = null, + bool initial_imap_remember_password = true, + string? initial_smtp_username = null, + string? initial_smtp_password = null, + bool initial_smtp_remember_password = true, + int initial_service_provider = -1, + string? initial_default_imap_host = null, uint16 initial_default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL, - bool initial_default_imap_ssl = true, bool initial_default_imap_starttls = false, + bool initial_default_imap_ssl = true, + bool initial_default_imap_starttls = false, string? initial_default_smtp_host = null, uint16 initial_default_smtp_port = Geary.Smtp.ClientConnection.DEFAULT_PORT_SSL, - bool initial_default_smtp_ssl = true, bool initial_default_smtp_starttls = false) { + bool initial_default_smtp_ssl = true, + bool initial_default_smtp_starttls = false) { Gtk.Builder builder = GearyApplication.instance.create_builder("login.glade"); dialog = builder.get_object("LoginDialog") as Gtk.Dialog; dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG); dialog.set_default_response(Gtk.ResponseType.OK); - entry_real_name = builder.get_object("real_name") as Gtk.Entry; - combo_service = builder.get_object("service") as Gtk.ComboBoxText; - entry_username = builder.get_object("username") as Gtk.Entry; - entry_password = builder.get_object("password") as Gtk.Entry; - check_remember_password = builder.get_object("remember_password") as Gtk.CheckButton; + entry_real_name = builder.get_object("entry: real_name") as Gtk.Entry; + combo_service = builder.get_object("combo: service") as Gtk.ComboBoxText; + entry_email = builder.get_object("entry: email") as Gtk.Entry; + label_password = builder.get_object("label: password") as Gtk.Label; + entry_password = builder.get_object("entry: password") as Gtk.Entry; + check_remember_password = builder.get_object("check: remember_password") as Gtk.CheckButton; - other_info = builder.get_object("other_info") as Gtk.Alignment; + other_info = builder.get_object("container: other_info") as Gtk.Alignment; - entry_imap_host = builder.get_object("imap host") as Gtk.Entry; - entry_imap_port = builder.get_object("imap port") as Gtk.Entry; - radio_imap_none = builder.get_object("imap none") as Gtk.RadioButton; - radio_imap_ssl = builder.get_object("imap ssl") as Gtk.RadioButton; - radio_imap_starttls = builder.get_object("imap starttls") as Gtk.RadioButton; + // IMAP info widgets. + entry_imap_host = builder.get_object("entry: imap host") as Gtk.Entry; + entry_imap_port = builder.get_object("entry: imap port") as Gtk.Entry; + entry_imap_username = builder.get_object("entry: imap username") as Gtk.Entry; + entry_imap_password = builder.get_object("entry: imap password") as Gtk.Entry; + check_imap_remember_password = builder.get_object("check: imap remember_password") as Gtk.CheckButton; + radio_imap_none = builder.get_object("radio: imap none") as Gtk.RadioButton; + radio_imap_ssl = builder.get_object("radio: imap ssl") as Gtk.RadioButton; + radio_imap_starttls = builder.get_object("radio: imap starttls") as Gtk.RadioButton; - entry_smtp_host = builder.get_object("smtp host") as Gtk.Entry; - entry_smtp_port = builder.get_object("smtp port") as Gtk.Entry; - radio_smtp_none = builder.get_object("smtp none") as Gtk.RadioButton; - radio_smtp_ssl = builder.get_object("smtp ssl") as Gtk.RadioButton; - radio_smtp_starttls = builder.get_object("smtp starttls") as Gtk.RadioButton; + // SMTP info widgets. + entry_smtp_host = builder.get_object("entry: smtp host") as Gtk.Entry; + entry_smtp_port = builder.get_object("entry: smtp port") as Gtk.Entry; + entry_smtp_username = builder.get_object("entry: smtp username") as Gtk.Entry; + entry_smtp_password = builder.get_object("entry: smtp password") as Gtk.Entry; + check_smtp_remember_password = builder.get_object("check: smtp remember_password") as Gtk.CheckButton; + radio_smtp_none = builder.get_object("radio: smtp none") as Gtk.RadioButton; + radio_smtp_ssl = builder.get_object("radio: smtp ssl") as Gtk.RadioButton; + radio_smtp_starttls = builder.get_object("radio: smtp starttls") as Gtk.RadioButton; combo_service.changed.connect(on_service_changed); @@ -88,18 +131,28 @@ public class LoginDialog { // Set defaults (other than service provider, which is set above) entry_real_name.set_text(initial_real_name ?? ""); - entry_username.set_text(initial_username ?? ""); - entry_password.set_text(initial_password ?? ""); - check_remember_password.active = initial_remember_password; + entry_email.set_text(initial_email ?? ""); + bool use_imap_password = initial_imap_password == initial_smtp_password && + initial_imap_password != null; + entry_password.set_text(use_imap_password ? initial_imap_password : ""); + check_remember_password.active = initial_imap_remember_password && initial_smtp_remember_password; + // Set defaults for IMAP info entry_imap_host.set_text(initial_default_imap_host ?? ""); entry_imap_port.set_text(initial_default_imap_port.to_string()); + entry_imap_username.set_text(initial_imap_username ?? ""); + entry_imap_password.set_text(initial_imap_password ?? ""); + check_imap_remember_password.active = initial_imap_remember_password; radio_imap_none.active = true; radio_imap_ssl.active = initial_default_imap_ssl; radio_imap_starttls.active = initial_default_imap_starttls; + // Set defaults for SMTP info entry_smtp_host.set_text(initial_default_smtp_host ?? ""); entry_smtp_port.set_text(initial_default_smtp_port.to_string()); + entry_smtp_username.set_text(initial_smtp_username ?? ""); + entry_smtp_password.set_text(initial_smtp_password ?? ""); + check_smtp_remember_password.active = initial_smtp_remember_password; radio_smtp_none.active = true; radio_smtp_ssl.active = initial_default_smtp_ssl; radio_smtp_starttls.active = initial_default_smtp_starttls; @@ -107,17 +160,25 @@ public class LoginDialog { if (Geary.String.is_empty(entry_real_name.text)) entry_real_name.grab_focus(); else - entry_username.grab_focus(); + entry_email.grab_focus(); - entry_username.changed.connect(on_changed); + entry_email.changed.connect(on_changed); entry_password.changed.connect(on_changed); entry_real_name.changed.connect(on_changed); check_remember_password.toggled.connect(on_changed); combo_service.changed.connect(on_changed); entry_imap_host.changed.connect(on_changed); entry_imap_port.changed.connect(on_changed); + entry_imap_username.changed.connect(on_changed); + entry_imap_password.changed.connect(on_changed); entry_smtp_host.changed.connect(on_changed); entry_smtp_port.changed.connect(on_changed); + entry_smtp_username.changed.connect(on_changed); + entry_smtp_password.changed.connect(on_changed); + + entry_email.changed.connect(on_email_changed); + entry_password.changed.connect(on_password_changed); + check_remember_password.toggled.connect(on_remember_password_toggled); radio_imap_none.toggled.connect(on_radio_imap_toggled); radio_imap_ssl.toggled.connect(on_radio_imap_toggled); @@ -133,7 +194,7 @@ public class LoginDialog { dialog.add_action_widget(new Gtk.Button.from_stock(Gtk.Stock.CANCEL), Gtk.ResponseType.CANCEL); ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK); ok_button.can_default = true; - ok_button.sensitive = false; + ok_button.sensitive = is_complete(); dialog.add_action_widget(ok_button, Gtk.ResponseType.OK); dialog.set_default_response(Gtk.ResponseType.OK); } @@ -148,12 +209,34 @@ public class LoginDialog { if (response != Gtk.ResponseType.OK) return false; - Geary.Credentials credentials = new Geary.Credentials(entry_username.text.strip(), - entry_password.text.strip()); - account_information = new Geary.AccountInformation(credentials); + bool use_extra_info = get_service_provider() == Geary.ServiceProvider.OTHER; + + string email = entry_email.text.strip(); + string imap_username = use_extra_info ? entry_imap_username.text.strip() : email; + string imap_password = (use_extra_info ? entry_imap_password : entry_password).text.strip(); + bool imap_remember_password = use_extra_info ? check_imap_remember_password.active : + check_remember_password.active; + Geary.Credentials imap_credentials = new Geary.Credentials(imap_username, imap_password); + + string smtp_username = use_extra_info ? entry_smtp_username.text.strip() : email; + string smtp_password = (use_extra_info ? entry_smtp_password : entry_password).text.strip(); + bool smtp_remember_password = use_extra_info ? check_smtp_remember_password.active : + check_remember_password.active; + Geary.Credentials smtp_credentials = new Geary.Credentials(smtp_username, smtp_password); + + try { + account_information = Geary.Engine.get_account_for_email(email); + } catch (Error err) { + debug("Unable to open account information for %s: %s", email, err.message); + + return false; + } account_information.real_name = entry_real_name.text.strip(); - account_information.remember_password = check_remember_password.active; + account_information.imap_credentials = imap_credentials; + account_information.smtp_credentials = smtp_credentials; + account_information.imap_remember_password = imap_remember_password; + account_information.smtp_remember_password = smtp_remember_password; account_information.service_provider = get_service_provider(); account_information.default_imap_server_host = entry_imap_host.text.strip(); account_information.default_imap_server_port = (uint16) int.parse(entry_imap_port.text.strip()); @@ -170,10 +253,34 @@ public class LoginDialog { return true; } + // TODO: Only reset if not manually set by user. + private void on_email_changed() { + entry_imap_username.text = entry_email.text; + entry_smtp_username.text = entry_email.text; + } + + // TODO: Only reset if not manually set by user. + private void on_password_changed() { + entry_imap_password.text = entry_password.text; + entry_smtp_password.text = entry_password.text; + } + + // TODO: Only reset if not manually set by user. + private void on_remember_password_toggled() { + check_imap_remember_password.active = check_remember_password.active; + check_smtp_remember_password.active = check_remember_password.active; + } + private void on_service_changed() { if (get_service_provider() == Geary.ServiceProvider.OTHER) { + label_password.hide(); + entry_password.hide(); + check_remember_password.hide(); other_info.show(); } else { + label_password.show(); + entry_password.show(); + check_remember_password.show(); other_info.hide(); dialog.resize(1, 1); } @@ -233,22 +340,40 @@ public class LoginDialog { return (Geary.ServiceProvider) combo_service.get_active(); } - private bool is_complete() { - if (Geary.String.is_null_or_whitespace(entry_username.text.strip()) || - Geary.String.is_null_or_whitespace(entry_password.text.strip())) + private bool is_valid_port(string text) { + uint64 port; + if (!uint64.try_parse(text, out port)) return false; - // For "other" providers, check server settings. - if (get_service_provider() == Geary.ServiceProvider.OTHER) { - if (Geary.String.is_empty(entry_imap_host.text.strip()) || - Geary.String.is_empty(entry_imap_port.text) || - Geary.String.is_empty(entry_smtp_host.text.strip()) || - Geary.String.is_empty(entry_smtp_port.text) || - int.parse(entry_imap_port.text) > uint16.MAX || - int.parse(entry_smtp_port.text) > uint16.MAX) - return false; + return (port <= uint16.MAX); + } + + private bool is_complete() { + switch (get_service_provider()) { + case Geary.ServiceProvider.OTHER: + if (Geary.String.is_empty_or_whitespace(entry_email.text) || + Geary.String.is_empty_or_whitespace(entry_imap_host.text) || + Geary.String.is_empty_or_whitespace(entry_imap_port.text) || + Geary.String.is_empty_or_whitespace(entry_imap_username.text) || + Geary.String.is_empty_or_whitespace(entry_imap_password.text) || + Geary.String.is_empty_or_whitespace(entry_smtp_host.text) || + Geary.String.is_empty_or_whitespace(entry_smtp_port.text) || + Geary.String.is_empty_or_whitespace(entry_smtp_username.text) || + Geary.String.is_empty_or_whitespace(entry_smtp_password.text) || + !is_valid_port(entry_imap_port.text) || + !is_valid_port(entry_smtp_port.text)) + return false; + break; + + // GMAIL and YAHOO + default: + if (Geary.String.is_empty_or_whitespace(entry_email.text) || + Geary.String.is_empty_or_whitespace(entry_password.text)) + return false; + break; } return true; } } + diff --git a/src/client/ui/message-list-cell-renderer.vala b/src/client/ui/message-list-cell-renderer.vala index f3da618e..868e3a5a 100644 --- a/src/client/ui/message-list-cell-renderer.vala +++ b/src/client/ui/message-list-cell-renderer.vala @@ -92,7 +92,7 @@ public class FormattedMessageData : Object { // TODO: Should we use a more sophisticated algorithm than "first word" to get the // first name? string first_name = tokens[0].strip(); - if (Geary.String.is_null_or_whitespace(first_name)) + if (Geary.String.is_empty_or_whitespace(first_name)) continue; if (authors_builder.len > 0) diff --git a/src/client/ui/message-viewer.vala b/src/client/ui/message-viewer.vala index 0a2806dc..440f039a 100644 --- a/src/client/ui/message-viewer.vala +++ b/src/client/ui/message-viewer.vala @@ -80,6 +80,7 @@ public class MessageViewer : Object { private Gtk.Menu? attachment_menu = null; private FileMonitor? user_style_monitor = null; private weak Geary.Folder? current_folder = null; + private Geary.AccountSettings? current_settings = null; private bool load_external_images = false; public MessageViewer() { @@ -235,7 +236,7 @@ public class MessageViewer : Object { continue; string src = element.get_attribute("src"); - if (Geary.String.is_null_or_whitespace(src) || is_always_loaded(src)) + if (Geary.String.is_empty_or_whitespace(src) || is_always_loaded(src)) continue; // Refresh the image source. Requests are denied when load_external_images @@ -364,7 +365,7 @@ public class MessageViewer : Object { } // Removes all displayed e-mails from the view. - public void clear(Geary.Folder? new_folder) { + public void clear(Geary.Folder? new_folder, Geary.AccountSettings? settings) { // Remove all messages from DOM. try { foreach (WebKit.DOM.HTMLElement element in email_to_element.values) { @@ -378,6 +379,7 @@ public class MessageViewer : Object { messages.clear(); current_folder = new_folder; + current_settings = settings; } // Converts an email ID into HTML ID used by the
for the email. @@ -395,7 +397,7 @@ public class MessageViewer : Object { public void show_multiple_selected(uint selected_count) { // Remove any messages and hide the message container, then show the counter. - clear(current_folder); + clear(current_folder, current_settings); try { hide_element_by_id(MESSAGE_CONTAINER_ID); show_element_by_id(SELECTION_COUNTER_ID); @@ -478,21 +480,13 @@ public class MessageViewer : Object { email_to_element.set(email.id, div_message); - string username; - try { - // TODO: Multiple accounts. - username = Geary.Engine.get_usernames().get(0); - } catch (Error e) { - error("Unable to get username. Error: %s", e.message); - } - insert_header_address(ref header, _("From:"), email.from != null ? email.from : email.sender, true); // Only include to string if it's not just this account. // TODO: multiple accounts. - if (email.to != null) { - if (!(email.to.get_all().size == 1 && email.to.get_all().get(0).address == username)) + if (email.to != null && current_settings != null) { + if (!(email.to.get_all().size == 1 && email.to.get_all().get(0).address == current_settings.email.address)) insert_header_address(ref header, _("To:"), email.to); } @@ -1372,7 +1366,7 @@ public class MessageViewer : Object { foreach (Geary.Attachment attachment in attachments) { // Generate the attachment table. WebKit.DOM.HTMLElement attachment_table = Util.DOM.clone_node(attachment_template); - string filename = Geary.String.is_null_or_whitespace(attachment.filename) ? + string filename = Geary.String.is_empty_or_whitespace(attachment.filename) ? _("none") : attachment.filename; Util.DOM.select(attachment_table, ".info .filename") .set_inner_text(filename); diff --git a/src/client/ui/password-dialog.vala b/src/client/ui/password-dialog.vala index 54d2dda7..97b498ad 100644 --- a/src/client/ui/password-dialog.vala +++ b/src/client/ui/password-dialog.vala @@ -17,15 +17,23 @@ public class PasswordDialog { private const string PRIMARY_TEXT_REPEATED_TRY = _("Unable to login to email server"); private Gtk.Dialog dialog; - private Gtk.Entry password_entry; - private Gtk.CheckButton remember_password_checkbutton; + private Gtk.Entry entry_imap_password; + private Gtk.CheckButton check_imap_remember_password; + private Gtk.Entry entry_smtp_password; + private Gtk.CheckButton check_smtp_remember_password; private Gtk.Button ok_button; + private Gtk.Grid grid_imap; + private Gtk.Grid grid_smtp; + private PasswordTypeFlag password_flags; - public string password { get; private set; default = ""; } + public string imap_password { get; private set; default = ""; } + public string smtp_password { get; private set; default = ""; } + public bool imap_remember_password { get; private set; } + public bool smtp_remember_password { get; private set; } - public bool remember_password { get; private set; } - - public PasswordDialog(Geary.AccountInformation account_information, bool first_try) { + public PasswordDialog(Geary.AccountInformation account_information, bool first_try, + PasswordTypeFlag password_flags) { + this.password_flags = password_flags; Gtk.Builder builder = GearyApplication.instance.create_builder("password-dialog.glade"); // Load dialog @@ -34,19 +42,26 @@ public class PasswordDialog { dialog.set_default_response(Gtk.ResponseType.OK); // Load editable widgets - password_entry = (Gtk.Entry) builder.get_object("password_entry"); - remember_password_checkbutton = (Gtk.CheckButton) builder.get_object("remember_password_checkbutton"); + entry_imap_password = (Gtk.Entry) builder.get_object("entry: imap password"); + check_imap_remember_password = (Gtk.CheckButton) builder.get_object("check: imap remember_password"); + entry_smtp_password = (Gtk.Entry) builder.get_object("entry: smtp password"); + check_smtp_remember_password = (Gtk.CheckButton) builder.get_object("check: smtp remember_password"); // Load non-editable widgets - Gtk.Label email_label = (Gtk.Label) builder.get_object("email_label"); - Gtk.Label real_name_label = (Gtk.Label) builder.get_object("real_name_label"); - Gtk.Label service_label = (Gtk.Label) builder.get_object("service_label"); - Gtk.Label imap_server_label = (Gtk.Label) builder.get_object("imap_server_label"); - Gtk.Label imap_port_label = (Gtk.Label) builder.get_object("imap_port_label"); - Gtk.Label imap_encryption_label = (Gtk.Label) builder.get_object("imap_encryption_label"); - Gtk.Label smtp_server_label = (Gtk.Label) builder.get_object("smtp_server_label"); - Gtk.Label smtp_port_label = (Gtk.Label) builder.get_object("smtp_port_label"); - Gtk.Label smtp_encryption_label = (Gtk.Label) builder.get_object("smtp_encryption_label"); + Gtk.Label label_real_name = (Gtk.Label) builder.get_object("label: real_name"); + Gtk.Label label_service = (Gtk.Label) builder.get_object("label: service"); + + grid_imap = (Gtk.Grid) builder.get_object("grid: imap"); + Gtk.Label label_imap_username = (Gtk.Label) builder.get_object("label: imap username"); + Gtk.Label label_imap_server = (Gtk.Label) builder.get_object("label: imap server"); + Gtk.Label label_imap_port = (Gtk.Label) builder.get_object("label: imap port"); + Gtk.Label label_imap_encryption = (Gtk.Label) builder.get_object("label: imap encryption"); + + grid_smtp = (Gtk.Grid) builder.get_object("grid: smtp"); + Gtk.Label label_smtp_username = (Gtk.Label) builder.get_object("label: smtp username"); + Gtk.Label label_smtp_server = (Gtk.Label) builder.get_object("label: smtp server"); + Gtk.Label label_smtp_port = (Gtk.Label) builder.get_object("label: smtp port"); + Gtk.Label label_smtp_encryption = (Gtk.Label) builder.get_object("label: smtp encryption"); // Load translated text for labels with markup unsupported by glade. Gtk.Label primary_text_label = (Gtk.Label) builder.get_object("primary_text_label"); @@ -70,17 +85,23 @@ public class PasswordDialog { bool smtp_server_ssl= (smtp_endpoint.flags & Geary.Endpoint.Flags.SSL) != 0; // Load initial values - email_label.set_text(account_information.credentials.user ?? ""); - password_entry.set_text(account_information.credentials.pass ?? ""); - remember_password_checkbutton.active = account_information.remember_password; - real_name_label.set_text(account_information.real_name ?? ""); - service_label.set_text(account_information.service_provider.display_name() ?? ""); - imap_server_label.set_text(imap_server_host); - imap_port_label.set_text(imap_server_port.to_string()); - imap_encryption_label.set_text(imap_server_ssl ? "on" : "off"); - smtp_server_label.set_text(smtp_server_host); - smtp_port_label.set_text(smtp_server_port.to_string()); - smtp_encryption_label.set_text(smtp_server_ssl ? "on" : "off"); + // TODO: Use email, imap/smtp credentials, imap/smtp remember. + label_real_name.set_text(account_information.real_name ?? ""); + label_service.set_text(account_information.service_provider.display_name() ?? ""); + + label_imap_username.set_text(account_information.imap_credentials.user ?? ""); + entry_imap_password.set_text(account_information.imap_credentials.pass ?? ""); + check_imap_remember_password.active = account_information.imap_remember_password; + label_imap_server.set_text(imap_server_host); + label_imap_port.set_text(imap_server_port.to_string()); + label_imap_encryption.set_text(imap_server_ssl ? "on" : "off"); + + label_smtp_username.set_text(account_information.smtp_credentials.user ?? ""); + entry_smtp_password.set_text(account_information.smtp_credentials.pass ?? ""); + check_smtp_remember_password.active = account_information.smtp_remember_password; + label_smtp_server.set_text(smtp_server_host); + label_smtp_port.set_text(smtp_server_port.to_string()); + label_smtp_encryption.set_text(smtp_server_ssl ? "on" : "off"); // Add action buttons Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL); @@ -92,7 +113,8 @@ public class PasswordDialog { // Setup listeners refresh_ok_button_sensitivity(); - password_entry.changed.connect(refresh_ok_button_sensitivity); + entry_imap_password.changed.connect(refresh_ok_button_sensitivity); + entry_smtp_password.changed.connect(refresh_ok_button_sensitivity); } private string get_primary_text_markup(bool first_try) { @@ -100,24 +122,31 @@ public class PasswordDialog { } private void refresh_ok_button_sensitivity() { - ok_button.sensitive = !Geary.String.is_null_or_whitespace(password_entry.get_text()); + ok_button.sensitive = !Geary.String.is_empty_or_whitespace(entry_imap_password.get_text()) || + !Geary.String.is_empty_or_whitespace(entry_smtp_password.get_text()); } public bool run() { dialog.show(); dialog.get_action_area().show_all(); - Gtk.ResponseType response = (Gtk.ResponseType)dialog.run(); - if (response != Gtk.ResponseType.OK) { - dialog.destroy(); - return false; + if (!password_flags.has_imap()) + grid_imap.hide(); + + if (!password_flags.has_smtp()) + grid_smtp.hide(); + + Gtk.ResponseType response = (Gtk.ResponseType) dialog.run(); + if (response == Gtk.ResponseType.OK) { + imap_password = entry_imap_password.get_text(); + imap_remember_password = check_imap_remember_password.active; + smtp_password = entry_smtp_password.get_text(); + smtp_remember_password = check_smtp_remember_password.active; } - password = password_entry.get_text(); - remember_password = remember_password_checkbutton.active; - dialog.destroy(); - return true; + + return (response == Gtk.ResponseType.OK); } } diff --git a/src/client/ui/webview-edit-fixer.vala b/src/client/ui/webview-edit-fixer.vala index 51b837f8..3ec36a1a 100644 --- a/src/client/ui/webview-edit-fixer.vala +++ b/src/client/ui/webview-edit-fixer.vala @@ -304,7 +304,7 @@ public class WebViewEditFixer { child = node.child_nodes.item(i); if (child.node_name == BR_NAME) continue; - if (child.node_name == TEXT_NAME && Geary.String.is_null_or_whitespace(child.node_value)) + if (child.node_name == TEXT_NAME && Geary.String.is_empty_or_whitespace(child.node_value)) continue; if (child.node_name == DIV_NAME && !is_substantial(child)) continue; diff --git a/src/client/util/util-keyring.vala b/src/client/util/util-keyring.vala index cfb48d51..addf004f 100644 --- a/src/client/util/util-keyring.vala +++ b/src/client/util/util-keyring.vala @@ -4,27 +4,79 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -const string GEARY_USERNAME_PREFIX = "org.yorba.geary username:"; +const string OLD_GEARY_USERNAME_PREFIX = "org.yorba.geary username:"; -public static bool keyring_save_password(Geary.Credentials credentials) { - string name = GEARY_USERNAME_PREFIX + credentials.user; +public enum PasswordType { + IMAP, + SMTP; - GnomeKeyring.Result result = GnomeKeyring.store_password_sync(GnomeKeyring.NETWORK_PASSWORD, - null, name, credentials.pass, "user", name); - - return result == GnomeKeyring.Result.OK; + public string get_prefix() { + switch (this) { + case IMAP: + return "org.yorba.geary imap_username:"; + + case SMTP: + return "org.yorba.geary smtp_username:"; + + default: + assert_not_reached(); + } + } } -public static void keyring_delete_password(string username) { +[Flags] +public enum PasswordTypeFlag { + IMAP, + SMTP; + + public bool has_imap() { + return (this & IMAP) == IMAP; + } + + public bool has_smtp() { + return (this & SMTP) == SMTP; + } +} + +private static string keyring_get_key(PasswordType password_type, string username) { + return password_type.get_prefix() + username; +} + +public static bool keyring_save_password(Geary.Credentials credentials, PasswordType password_type) { + string key = keyring_get_key(password_type, credentials.user); + + GnomeKeyring.Result result = GnomeKeyring.store_password_sync(GnomeKeyring.NETWORK_PASSWORD, + null, key, credentials.pass, "user", key); + + if (result != GnomeKeyring.Result.OK) + debug("Unable to store password in GNOME keyring: %s", result.to_string()); + + return (result == GnomeKeyring.Result.OK); +} + +public static void keyring_delete_password(string username, PasswordType password_type) { + // delete new-style and old-style locations GnomeKeyring.delete_password_sync(GnomeKeyring.NETWORK_PASSWORD, "user", - GEARY_USERNAME_PREFIX + username); + keyring_get_key(password_type, username)); + GnomeKeyring.delete_password_sync(GnomeKeyring.NETWORK_PASSWORD, "user", + OLD_GEARY_USERNAME_PREFIX + username); } // Returns the password for the given username, or null if not set. -public static string? keyring_get_password(string username) { +public static string? keyring_get_password(string username, PasswordType password_type) { string password; - GnomeKeyring.Result res = GnomeKeyring.find_password_sync(GnomeKeyring.NETWORK_PASSWORD, - out password, "user", GEARY_USERNAME_PREFIX + username); + GnomeKeyring.Result result = GnomeKeyring.find_password_sync(GnomeKeyring.NETWORK_PASSWORD, + out password, "user", keyring_get_key(password_type, username)); - return res == GnomeKeyring.Result.OK ? password : null; + if (result != GnomeKeyring.Result.OK) { + // fallback to the old keyring key string for upgrading users + result = GnomeKeyring.find_password_sync(GnomeKeyring.NETWORK_PASSWORD, out password, + "user", OLD_GEARY_USERNAME_PREFIX + username); + } + + if (result != GnomeKeyring.Result.OK) + debug("Unable to fetch password in GNOME keyring: %s", result.to_string()); + + return (result == GnomeKeyring.Result.OK) ? password : null; } + diff --git a/src/dbusservice/controller.vala b/src/dbusservice/controller.vala index 321a41a5..3ccdcb7c 100644 --- a/src/dbusservice/controller.vala +++ b/src/dbusservice/controller.vala @@ -13,7 +13,7 @@ public class Geary.DBus.Controller { public DBusConnection connection { get; private set; } private string[] args; - private Geary.EngineAccount account; + private Geary.Account account; private Geary.DBus.Conversations conversations; public static void init(string[] args) { @@ -32,9 +32,12 @@ public class Geary.DBus.Controller { connection = yield Bus.get(GLib.BusType.SESSION); // Open the account. - Geary.Credentials credentials = new Geary.Credentials(args[1], args[2]); - Geary.AccountInformation account_information = new Geary.AccountInformation(credentials); - account_information.load_info_from_file(); + // TODO: Don't assume username is email, allow separate imap/smtp credentials. + Geary.AccountInformation account_information = Geary.Engine.get_account_for_email(args[1]); + account_information.imap_credentials = new Geary.Credentials(args[1], args[2]); + account_information.smtp_credentials = new Geary.Credentials(args[1], args[2]); + + // convert AccountInformation into an Account try { account = account_information.get_account(); } catch (EngineError err) { @@ -80,7 +83,7 @@ public class Geary.DBus.Controller { return File.new_for_path(Environment.get_current_dir()); } - private void on_report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, + private void on_report_problem(Geary.Account.Problem problem, Geary.AccountSettings settings, Error? err) { debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)"); } diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index 4319dc41..b3fad193 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -5,15 +5,13 @@ */ public abstract class Geary.AbstractAccount : Object, Geary.Account { + public Geary.AccountSettings settings { get; protected set; } + private string name; - public AbstractAccount(string name) { + public AbstractAccount(string name, AccountSettings settings) { this.name = name; - } - - public virtual void notify_report_problem(Geary.Account.Problem problem, - Geary.Credentials? credentials, Error? err) { - report_problem(problem, credentials, err); + this.settings = settings; } protected virtual void notify_folders_added_removed(Gee.Collection? added, @@ -29,6 +27,15 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account { closed(); } + protected virtual void notify_email_sent(RFC822.Message message) { + email_sent(message); + } + + protected virtual void notify_report_problem(Geary.Account.Problem problem, + Geary.AccountSettings? settings, Error? err) { + report_problem(problem, settings, err); + } + public abstract async void open_async(Cancellable? cancellable = null) throws Error; public abstract async void close_async(Cancellable? cancellable = null) throws Error; @@ -44,6 +51,9 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account { public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error; + public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) + throws Error; + public virtual string to_string() { return name; } diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala index 55c87aa6..34d03c57 100644 --- a/src/engine/api/geary-account-information.vala +++ b/src/engine/api/geary-account-information.vala @@ -8,7 +8,10 @@ public class Geary.AccountInformation : Object { private const string GROUP = "AccountInformation"; private const string REAL_NAME_KEY = "real_name"; private const string SERVICE_PROVIDER_KEY = "service_provider"; - private const string REMEMBER_PASSWORD_KEY = "remember_password"; + private const string IMAP_USERNAME_KEY = "imap_username"; + private const string IMAP_REMEMBER_PASSWORD_KEY = "imap_remember_password"; + private const string SMTP_USERNAME_KEY = "smtp_username"; + private const string SMTP_REMEMBER_PASSWORD_KEY = "smtp_remember_password"; private const string IMAP_HOST = "imap_host"; private const string IMAP_PORT = "imap_port"; private const string IMAP_SSL = "imap_ssl"; @@ -21,9 +24,11 @@ public class Geary.AccountInformation : Object { public const string SETTINGS_FILENAME = "geary.ini"; - internal File? settings_dir; - internal File? file = null; + internal File settings_dir; + internal File file; + public string real_name { get; set; } + public string email { get; set; } public Geary.ServiceProvider service_provider { get; set; } public bool imap_server_pipeline { get; set; default = true; } @@ -37,17 +42,16 @@ public class Geary.AccountInformation : Object { public bool default_smtp_server_ssl { get; set; } public bool default_smtp_server_starttls { get; set; } - public Geary.Credentials credentials { get; private set; } - public bool remember_password { get; set; default = true; } + public Geary.Credentials imap_credentials { get; set; default = new Geary.Credentials(null, null); } + public bool imap_remember_password { get; set; default = true; } + public Geary.Credentials smtp_credentials { get; set; default = new Geary.Credentials(null, null); } + public bool smtp_remember_password { get; set; default = true; } - public AccountInformation(Geary.Credentials credentials) { - this.credentials = credentials; - - this.settings_dir = Geary.Engine.user_data_dir.get_child(credentials.user); + internal AccountInformation(File directory) { + this.email = directory.get_basename(); + this.settings_dir = directory; this.file = settings_dir.get_child(SETTINGS_FILENAME); - } - - public void load_info_from_file() { + KeyFile key_file = new KeyFile(); try { key_file.load_from_file(file.get_path() ?? "", KeyFileFlags.NONE); @@ -57,9 +61,12 @@ public class Geary.AccountInformation : Object { // It's no big deal if we couldn't load the key file -- just means we give you the defaults. } finally { real_name = get_string_value(key_file, GROUP, REAL_NAME_KEY); - remember_password = get_bool_value(key_file, GROUP, REMEMBER_PASSWORD_KEY, true); + imap_credentials.user = get_string_value(key_file, GROUP, IMAP_USERNAME_KEY, email); + imap_remember_password = get_bool_value(key_file, GROUP, IMAP_REMEMBER_PASSWORD_KEY, true); + smtp_credentials.user = get_string_value(key_file, GROUP, SMTP_USERNAME_KEY, email); + smtp_remember_password = get_bool_value(key_file, GROUP, SMTP_REMEMBER_PASSWORD_KEY, true); service_provider = Geary.ServiceProvider.from_string(get_string_value(key_file, GROUP, - SERVICE_PROVIDER_KEY)); + SERVICE_PROVIDER_KEY, Geary.ServiceProvider.GMAIL.to_string())); imap_server_pipeline = get_bool_value(key_file, GROUP, IMAP_PIPELINE, true); @@ -82,33 +89,55 @@ public class Geary.AccountInformation : Object { public async bool validate_async(Cancellable? cancellable = null) throws EngineError { AccountSettings settings = new AccountSettings(this); - Geary.Imap.ClientSession client_session = new Imap.ClientSession(settings.imap_endpoint, true); + // validate IMAP, which requires logging in and establishing an AUTHORIZED cx state + bool imap_valid = false; + Geary.Imap.ClientSession? imap_session = new Imap.ClientSession(settings.imap_endpoint, true); try { - yield client_session.connect_async(cancellable); - yield client_session.initiate_session_async(settings.credentials, cancellable); + yield imap_session.connect_async(cancellable); + yield imap_session.initiate_session_async(settings.imap_credentials, cancellable); + + // Connected and initiated, still need to be sure connection authorized + string current_mailbox; + if (imap_session.get_context(out current_mailbox) == Imap.ClientSession.Context.AUTHORIZED) + imap_valid = true; } catch (Error err) { - debug("Error validating account info: %s", err.message); + debug("Error validating IMAP account info: %s", err.message); - try { - yield client_session.disconnect_async(cancellable); - } catch (Error disconnect_err) { - // ignored - } - - return false; + // fall through so session can be disconnected } - // Connected and initiated, still need to be sure connection authorized - string current_mailbox; - bool valid = (client_session.get_context(out current_mailbox) == Imap.ClientSession.Context.AUTHORIZED); - try { - yield client_session.disconnect_async(cancellable); + yield imap_session.disconnect_async(cancellable); } catch (Error err) { // ignored + } finally { + imap_session = null; } - return valid; + if (!imap_valid) + return false; + + // SMTP is simpler, merely see if login works and done (throws an SmtpError if not) + bool smtp_valid = false; + Geary.Smtp.ClientSession? smtp_session = new Geary.Smtp.ClientSession(settings.smtp_endpoint); + try { + yield smtp_session.login_async(settings.smtp_credentials, cancellable); + smtp_valid = true; + } catch (Error err) { + debug("Error validating SMTP account info: %s", err.message); + + // fall through so session can be disconnected + } + + try { + yield smtp_session.logout_async(cancellable); + } catch (Error err) { + // ignored + } finally { + smtp_session = null; + } + + return smtp_valid; } public Endpoint get_imap_endpoint() throws EngineError { @@ -159,23 +188,23 @@ public class Geary.AccountInformation : Object { } } - public Geary.EngineAccount get_account() throws EngineError { + public Geary.Account get_account() throws EngineError { AccountSettings settings = new AccountSettings(this); ImapDB.Account local_account = new ImapDB.Account(settings); Imap.Account remote_account = new Imap.Account(settings); - + switch (service_provider) { case ServiceProvider.GMAIL: - return new ImapEngine.GmailAccount("Gmail account %s".printf(credentials.to_string()), + return new ImapEngine.GmailAccount("Gmail account %s".printf(email), settings, remote_account, local_account); case ServiceProvider.YAHOO: - return new ImapEngine.YahooAccount("Yahoo account %s".printf(credentials.to_string()), + return new ImapEngine.YahooAccount("Yahoo account %s".printf(email), settings, remote_account, local_account); case ServiceProvider.OTHER: - return new ImapEngine.OtherAccount("Other account %s".printf(credentials.to_string()), + return new ImapEngine.OtherAccount("Other account %s".printf(email), settings, remote_account, local_account); default: @@ -184,34 +213,34 @@ public class Geary.AccountInformation : Object { } } - private string get_string_value(KeyFile key_file, string group, string key, string _default = "") { - string v = _default; + private string get_string_value(KeyFile key_file, string group, string key, string def = "") { try { - v = key_file.get_value(group, key); + return key_file.get_value(group, key); } catch(KeyFileError err) { // Ignore. } - return v; + + return def; } - private bool get_bool_value(KeyFile key_file, string group, string key, bool _default = false) { - bool v = _default; + private bool get_bool_value(KeyFile key_file, string group, string key, bool def = false) { try { - v = key_file.get_boolean(group, key); + return key_file.get_boolean(group, key); } catch(KeyFileError err) { // Ignore. } - return v; + + return def; } - private uint16 get_uint16_value(KeyFile key_file, string group, string key, uint16 _default = 0) { - uint16 v = _default; + private uint16 get_uint16_value(KeyFile key_file, string group, string key, uint16 def = 0) { try { - v = (uint16) key_file.get_integer(group, key); + return (uint16) key_file.get_integer(group, key); } catch(KeyFileError err) { // Ignore. } - return v; + + return def; } public async void store_async(Cancellable? cancellable = null) { @@ -221,7 +250,7 @@ public class Geary.AccountInformation : Object { try { settings_dir.make_directory_with_parents(); } catch (Error err) { - error("Error creating settings directory for user '%s': %s", credentials.user, + error("Error creating settings directory for email '%s': %s", email, err.message); } } @@ -238,7 +267,10 @@ public class Geary.AccountInformation : Object { key_file.set_value(GROUP, REAL_NAME_KEY, real_name); key_file.set_value(GROUP, SERVICE_PROVIDER_KEY, service_provider.to_string()); - key_file.set_boolean(GROUP, REMEMBER_PASSWORD_KEY, remember_password); + key_file.set_value(GROUP, IMAP_USERNAME_KEY, imap_credentials.user); + key_file.set_boolean(GROUP, IMAP_REMEMBER_PASSWORD_KEY, imap_remember_password); + key_file.set_value(GROUP, SMTP_USERNAME_KEY, smtp_credentials.user); + key_file.set_boolean(GROUP, SMTP_REMEMBER_PASSWORD_KEY, smtp_remember_password); key_file.set_boolean(GROUP, IMAP_PIPELINE, imap_server_pipeline); diff --git a/src/engine/api/geary-account-settings.vala b/src/engine/api/geary-account-settings.vala index 100a2983..6080d2be 100644 --- a/src/engine/api/geary-account-settings.vala +++ b/src/engine/api/geary-account-settings.vala @@ -7,25 +7,34 @@ /** * AccountSettings is a complement to AccountInformation. AccountInformation stores these settings * as well as defaults and provides validation and persistence functionality. Settings is simply - * the values loaded from a backing store, perhaps chosen from defaults, and validated filtered - * down to a set of working settings for the Account to use. + * the values loaded from a backing store, perhaps chosen from defaults, and validated, then filtered + * down to a set of working settings for the Account to use. Changes made to AccountInformation + * are not reflected here; a new AccountSettings object must be created. */ public class Geary.AccountSettings { + public RFC822.MailboxAddress email { get; private set; } public string real_name { get; private set; } - public Geary.Credentials credentials { get; private set; } + public Geary.Credentials imap_credentials { get; private set; } + public Geary.Credentials smtp_credentials { get; private set; } public Geary.ServiceProvider service_provider { get; private set; } public bool imap_server_pipeline { get; private set; } public Endpoint imap_endpoint { get; private set; } public Endpoint smtp_endpoint { get; private set; } + internal File settings_dir { get; private set; } + internal AccountSettings(AccountInformation info) throws EngineError { + email = new RFC822.MailboxAddress(info.real_name, info.email); real_name = info.real_name; - credentials = info.credentials; + imap_credentials = info.imap_credentials; + smtp_credentials = info.smtp_credentials; service_provider = info.service_provider; imap_server_pipeline = info.imap_server_pipeline; imap_endpoint = info.get_imap_endpoint(); smtp_endpoint = info.get_smtp_endpoint(); + + settings_dir = info.settings_dir; } } diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 51fddef2..9f059cea 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -6,17 +6,22 @@ public interface Geary.Account : Object { public enum Problem { - LOGIN_FAILED, + RECV_EMAIL_LOGIN_FAILED, + SEND_EMAIL_LOGIN_FAILED, HOST_UNREACHABLE, NETWORK_UNAVAILABLE, DATABASE_FAILURE } + public abstract Geary.AccountSettings settings { get; protected set; } + public signal void opened(); public signal void closed(); - public signal void report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, + public signal void email_sent(Geary.RFC822.Message rfc822); + + public signal void report_problem(Geary.Account.Problem problem, Geary.AccountSettings settings, Error? err); public signal void folders_added_removed(Gee.Collection? added, @@ -32,11 +37,16 @@ public interface Geary.Account : Object { */ protected abstract void notify_closed(); + /** + * Signal notification method for subclasses to use. + */ + protected abstract void notify_email_sent(Geary.RFC822.Message rfc822); + /** * Signal notification method for subclasses to use. */ protected abstract void notify_report_problem(Geary.Account.Problem problem, - Geary.Credentials? credentials, Error? err); + Geary.AccountSettings? settings, Error? err); /** * Signal notification method for subclasses to use. @@ -93,6 +103,15 @@ public interface Geary.Account : Object { public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error; + /** + * Submits a ComposedEmail for delivery. Messages may be scheduled for later delivery or immediately + * sent. Subscribe to the "email-sent" signal to be notified of delivery. Note that that signal + * does not return the ComposedEmail object but an RFC822-formatted object. Allowing for the + * subscriber to attach some kind of token for later comparison is being considered. + */ + public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) + throws Error; + /** * Used only for debugging. Should not be used for user-visible strings. */ diff --git a/src/engine/api/geary-credentials.vala b/src/engine/api/geary-credentials.vala index dba956e6..8dac73c9 100644 --- a/src/engine/api/geary-credentials.vala +++ b/src/engine/api/geary-credentials.vala @@ -4,17 +4,43 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +/** + * Credentials represent a username and a password authenticating a user for access to a resource. + * More sophisticated schemes exist; this suffices for now. + * + * Either property (user, pass) may be null. This indicates the Credentials are incomplete and + * need further information (i.e. prompt user for username, fetch password from keyring, etc.) + * Either field may be a non-null zero-length string; this is considered valid and is_complete() + * will return true in this case. + * + * Note that Geary will hold Credentials in memory for the long-term, usually the duration of the + * application. This is because network resources often have to be connected (or reconnected) to + * in the background and asking the user to reauthenticate each time is deemed inconvenient. + */ + public class Geary.Credentials { - public string user { get; private set; } - public string pass { get; set; } + public string? user { get; set; } + public string? pass { get; set; } + /** + * user and pass may be null here, but the properties will always be a non-null zero-length + * string. See is_empty(). + */ public Credentials(string? user, string? pass) { - this.user = user ?? ""; - this.pass = pass ?? ""; + this.user = user; + this.pass = pass; + } + + public bool is_complete() { + return (user != null) && (pass != null); + } + + public Credentials copy() { + return new Credentials(user, pass); } public string to_string() { - return "%s".printf(user); + return user; } } diff --git a/src/engine/api/geary-endpoint.vala b/src/engine/api/geary-endpoint.vala index 5887e128..6f677b8f 100644 --- a/src/engine/api/geary-endpoint.vala +++ b/src/engine/api/geary-endpoint.vala @@ -4,6 +4,11 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +/** + * An Endpoint represents the location of an Internet TCP connection as represented by a host, + * a port, and flags and other parameters that specify the nature of the connection itself. + */ + public class Geary.Endpoint : Object { [Flags] public enum Flags { @@ -21,6 +26,12 @@ public class Geary.Endpoint : Object { } } + public enum AttemptStarttls { + YES, + NO, + HALT + } + public string host_specifier { get; private set; } public uint16 default_port { get; private set; } public Flags flags { get; private set; } @@ -69,7 +80,27 @@ public class Geary.Endpoint : Object { return cx; } - + + /** + * Returns true if a STARTTLS command should be attempted on the connection: + * (a) STARTTLS is reported available (a parameter specified by the caller to this method), + * (b) not using SSL (so TLS is not required), and (c) STARTTLS is specified as a flag on + * the Endpoint. + * + * If AttemptStarttls.HALT is returned, the caller should not proceed to pass any + * authentication information down the connection; this situation indicates the connection is + * insecure and the Endpoint is configured otherwise. + */ + public AttemptStarttls attempt_starttls(bool starttls_available) { + if (is_ssl || !use_starttls) + return AttemptStarttls.NO; + + if (!starttls_available) + return AttemptStarttls.HALT; + + return AttemptStarttls.YES; + } + public string to_string() { return "%s/default:%u".printf(host_specifier, default_port); } diff --git a/src/engine/api/geary-engine-account.vala b/src/engine/api/geary-engine-account.vala deleted file mode 100644 index 54b531cc..00000000 --- a/src/engine/api/geary-engine-account.vala +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright 2011-2012 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 abstract class Geary.EngineAccount : Geary.AbstractAccount { - public virtual Geary.AccountSettings settings { get; private set; } - - public virtual signal void email_sent(Geary.RFC822.Message rfc822) { - } - - internal EngineAccount(string name, AccountSettings settings) { - base (name); - - this.settings = settings; - } - - protected virtual void notify_email_sent(Geary.RFC822.Message rfc822) { - email_sent(rfc822); - } - - public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) - throws Error; -} diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index 898bedc5..e9a1933b 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -9,7 +9,11 @@ public class Geary.Engine { public static File? resource_dir { get; private set; default = null; } private static bool inited = false; - + + /** + * Geary.Engine.init() should be the first call any application makes prior to calling into + * the Geary engine. + */ public static void init(File _user_data_dir, File _resource_dir) { if (inited) return; @@ -23,9 +27,18 @@ public class Geary.Engine { inited = true; } - // Returns a list of usernames associated with Geary. - public static Gee.List get_usernames() throws Error { - Gee.ArrayList list = new Gee.ArrayList(); + /** + * Returns a list of AccountInformation objects representing accounts setup for use by the Geary + * engine. + */ + public static Gee.List get_accounts() throws Error { + Gee.ArrayList list = new Gee.ArrayList(); + + if (!inited) { + debug("Geary.Engine.get_accounts(): not initialized"); + + return list; + } FileEnumerator enumerator = user_data_dir.enumerate_children("standard::*", FileQueryInfoFlags.NONE); @@ -33,9 +46,22 @@ public class Geary.Engine { FileInfo? info = null; while ((info = enumerator.next_file()) != null) { if (info.get_file_type() == FileType.DIRECTORY) - list.add(info.get_name()); + list.add(new AccountInformation(user_data_dir.get_child(info.get_name()))); } return list; } + + /** + * Returns a Geary.AccountInformation for the specified email address. If the account + * has not been set up previously, an object is returned, although it's merely backed by memory + * and filled with defaults. Otherwise, the account information for that address is loaded. + * + * "email" in this case means the Internet mailbox for the account, i.e. username@domain.com. + * Use the "address" field of RFC822.MailboxAddress for this parameter. + */ + public static Geary.AccountInformation get_account_for_email(string email) throws Error { + return new Geary.AccountInformation(user_data_dir.get_child(email)); + } } + diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index 56a493f3..fd158af2 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -18,11 +18,6 @@ private class Geary.ImapDB.Account : Object { // Only available when the Account is opened public SmtpOutboxFolder? outbox { get; private set; default = null; } - // TODO: This should be updated when Geary no longer assumes username is email. - public string account_owner_email { - get { return settings.credentials.user; } - } - private string name; private AccountSettings settings; private ImapDB.Database? db = null; @@ -34,7 +29,7 @@ private class Geary.ImapDB.Account : Object { this.settings = settings; contact_store = new ContactStore(); - name = "IMAP database account for %s".printf(settings.credentials.user); + name = "IMAP database account for %s".printf(settings.imap_credentials.user); } private void check_open() throws Error { @@ -47,7 +42,7 @@ private class Geary.ImapDB.Account : Object { if (db != null) throw new EngineError.ALREADY_OPEN("IMAP database already open"); - db = new ImapDB.Database(user_data_dir, schema_dir, account_owner_email); + db = new ImapDB.Database(user_data_dir, schema_dir, settings.email.address); try { db.open(Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE, null, @@ -361,7 +356,7 @@ private class Geary.ImapDB.Account : Object { } // create folder - folder = new Geary.ImapDB.Folder(db, path, contact_store, account_owner_email, folder_id, + folder = new Geary.ImapDB.Folder(db, path, contact_store, settings.email.address, folder_id, properties); // build a reference to it diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index 9825cc19..9a2a0292 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -32,6 +32,9 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport } } + public signal void report_problem(Geary.Account.Problem problem, Geary.AccountSettings settings, + Error? err); + private static FolderRoot? path = null; private ImapDB.Database db; @@ -119,6 +122,9 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport } catch (Error send_err) { debug("Outbox postman send error, retrying: %s", send_err.message); + if (send_err is SmtpError.AUTHENTICATION_FAILED) + report_problem(Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED, settings, send_err); + try { outbox_queue.send(row); } catch (Error send_err) { @@ -477,7 +483,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupport private async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable) throws Error { - yield smtp.login_async(settings.credentials, cancellable); + yield smtp.login_async(settings.smtp_credentials, cancellable); try { yield smtp.send_email_async(rfc822, cancellable); } finally { diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index 9d7f5011..1e9811d3 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount { +private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { private static Geary.FolderPath? inbox_path = null; private static Geary.FolderPath? outbox_path = null; @@ -52,8 +52,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount { if (open) throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string()); - yield local.open_async(Engine.user_data_dir.get_child(settings.credentials.user), - Engine.resource_dir.get_child("sql"), cancellable); + yield local.open_async(settings.settings_dir, Engine.resource_dir.get_child("sql"), cancellable); + + // outbox is now available + local.outbox.report_problem.connect(notify_report_problem); // need to back out local.open_async() if remote fails try { @@ -78,6 +80,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount { if (!open) return; + local.outbox.report_problem.disconnect(notify_report_problem); + // attempt to close both regardless of errors Error? local_err = null; try { @@ -316,7 +320,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount { } private void on_login_failed(Geary.Credentials? credentials) { - notify_report_problem(Geary.Account.Problem.LOGIN_FAILED, credentials, null); + notify_report_problem(Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED, settings, null); } } diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index 9bfc6eba..cdf6be12 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -37,7 +37,7 @@ private class Geary.Imap.Account : Object { public signal void login_failed(Geary.Credentials cred); public Account(Geary.AccountSettings settings) { - name = "IMAP Account for %s".printf(settings.credentials.to_string()); + name = "IMAP Account for %s".printf(settings.imap_credentials.to_string()); this.settings = settings; session_mgr = new ClientSessionManager(settings); @@ -185,7 +185,7 @@ private class Geary.Imap.Account : Object { } private void on_login_failed() { - login_failed(settings.credentials); + login_failed(settings.imap_credentials); } public string to_string() { diff --git a/src/engine/imap/decoders/imap-capabilities.vala b/src/engine/imap/decoders/imap-capabilities.vala index 349e975d..72df47ee 100644 --- a/src/engine/imap/decoders/imap-capabilities.vala +++ b/src/engine/imap/decoders/imap-capabilities.vala @@ -17,30 +17,15 @@ public class Geary.Imap.Capabilities : Geary.GenericCapabilities { * example). */ public Capabilities(int revision) { + base ("="); + this.revision = revision; } public bool add_parameter(StringParameter stringp) { - string[] tokens = stringp.value.split("=", 2); - if (tokens.length == 1) - add_capability(tokens[0]); - else if (tokens.length == 2) - add_capability(tokens[0], tokens[1]); - else - return false; - - return true; + return parse_and_add_capability(stringp.value); } - /** - * Like get_setting(), but returns a StringParameter representing the capability. - */ - public StringParameter? get_parameter(string name) { - string? setting = get_setting(name); - - return new StringParameter(String.is_empty(setting) ? name : "%s=%s".printf(name, setting)); - } - public override string to_string() { return "#%d: %s".printf(revision, base.to_string()); } diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index d0af1dfe..9f02278b 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -226,7 +226,7 @@ public class Geary.Imap.ClientSessionManager { try { yield new_session.connect_async(cancellable); - yield new_session.initiate_session_async(settings.credentials, cancellable); + yield new_session.initiate_session_async(settings.imap_credentials, cancellable); } catch (Error err) { debug("[%s] Connect failure: %s", new_session.to_string(), err.message); diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index 44161c3c..96c76240 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -547,7 +547,7 @@ public class Geary.Imap.ClientSession { * Performs the LOGIN command using the supplied credentials. See initiate_session_async() for * a more full-featured version of login_async(). */ - public async void login_async(Geary.Credentials credentials, Cancellable? cancellable = null) + public async CommandResponse login_async(Geary.Credentials credentials, Cancellable? cancellable = null) throws Error { LoginParams params = new LoginParams(credentials.user, credentials.pass, cancellable, login_async.callback); @@ -558,6 +558,11 @@ public class Geary.Imap.ClientSession { if (params.err != null) throw params.err; + + // No Error means a response had better be available + assert(params.cmd_response != null); + + return params.cmd_response; } /** @@ -572,28 +577,48 @@ public class Geary.Imap.ClientSession { Imap.Capabilities caps = get_capabilities(); - // Attempt TLS if specified or if available and not an SSL connection debug("[%s] use_starttls=%s is_ssl=%s starttls=%s", to_string(), imap_endpoint.use_starttls.to_string(), imap_endpoint.is_ssl.to_string(), caps.has_capability(Capabilities.STARTTLS).to_string()); - if (imap_endpoint.use_starttls - || (!imap_endpoint.is_ssl && caps.has_capability(Capabilities.STARTTLS))) { - debug("[%s] Attempting STARTTLS...", to_string()); - CommandResponse resp = yield send_command_async(new StarttlsCommand()); - if (resp.status_response.status == Status.OK) { - yield cx.starttls_async(cancellable); - debug("[%s] STARTTLS completed", to_string()); - } else { - debug("[%s} STARTTLS refused: %s", to_string(), resp.status_response.to_string()); + switch (imap_endpoint.attempt_starttls(caps.has_capability(Capabilities.STARTTLS))) { + case Endpoint.AttemptStarttls.YES: + debug("[%s] Attempting STARTTLS...", to_string()); + CommandResponse resp; + try { + resp = yield send_command_async(new StarttlsCommand()); + } catch (Error err) { + debug("Error attempting STARTTLS command on %s: %s", to_string(), err.message); + + throw err; + } - // throw an exception and fail rather than send credentials under suspect - // conditions - throw new ImapError.NOT_SUPPORTED("STARTTLS refused by %s: %s", to_string(), - resp.status_response.to_string()); - } + if (resp.status_response.status == Status.OK) { + yield cx.starttls_async(cancellable); + debug("[%s] STARTTLS completed", to_string()); + } else { + debug("[%s} STARTTLS refused: %s", to_string(), resp.status_response.to_string()); + + // throw an exception and fail rather than send credentials under suspect + // conditions + throw new ImapError.NOT_SUPPORTED("STARTTLS refused by %s: %s", to_string(), + resp.status_response.to_string()); + } + break; + + case Endpoint.AttemptStarttls.NO: + debug("[%s] No STARTTLS attempted", to_string()); + break; + + case Endpoint.AttemptStarttls.HALT: + default: + throw new ImapError.NOT_SUPPORTED("STARTTLS unavailable for %s", to_string()); } // Login after STARTTLS - yield login_async(credentials, cancellable); + CommandResponse login_resp = yield login_async(credentials, cancellable); + if (login_resp.status_response.status != Status.OK) { + throw new ImapError.UNAUTHENTICATED("Unable to login to %s with supplied credentials", + to_string()); + } // if new capabilities not offered after login, get them now if (caps.revision == get_capabilities().revision) diff --git a/src/engine/smtp/smtp-authenticator.vala b/src/engine/smtp/smtp-authenticator.vala index a8f569a6..99d9589a 100644 --- a/src/engine/smtp/smtp-authenticator.vala +++ b/src/engine/smtp/smtp-authenticator.vala @@ -26,5 +26,9 @@ public interface Geary.Smtp.Authenticator : Object { * server. Generally this leaves the connection in a bad state and should be closed. */ public abstract uint8[]? challenge(int step, Response response) throws SmtpError; + + public virtual string to_string() { + return get_name(); + } } diff --git a/src/engine/smtp/smtp-capabilities.vala b/src/engine/smtp/smtp-capabilities.vala index ba09cac0..d533e533 100644 --- a/src/engine/smtp/smtp-capabilities.vala +++ b/src/engine/smtp/smtp-capabilities.vala @@ -7,7 +7,8 @@ public class Geary.Smtp.Capabilities : Geary.GenericCapabilities { public static const string STARTTLS = "starttls"; - public Capabilities(){ + public Capabilities() { + base ("="); } } diff --git a/src/engine/smtp/smtp-client-connection.vala b/src/engine/smtp/smtp-client-connection.vala index 504a33be..71e43b28 100644 --- a/src/engine/smtp/smtp-client-connection.vala +++ b/src/engine/smtp/smtp-client-connection.vala @@ -56,30 +56,39 @@ public class Geary.Smtp.ClientConnection { public async Response authenticate_async(Authenticator authenticator, Cancellable? cancellable = null) throws Error { check_connected(); - - // Use STARTTLS if it has been explicitly enabled or if the server supports it and we are - // not using SSL encryption. - Response response; - if (endpoint.use_starttls || (!endpoint.is_ssl && capabilities.has_capability(Capabilities.STARTTLS))) { - response = yield transaction_async(new Request(Command.STARTTLS)); - if (!response.code.is_starttls_ready()) { - throw new SmtpError.STARTTLS_FAILED("STARTTLS failed: %s", response.to_string()); - } - - // TLS started, lets wrap the connection and shake hands. - TlsClientConnection tls_cx = TlsClientConnection.new(cx, socket_cx.get_remote_address()); - cx = tls_cx; - tls_cx.set_validation_flags(TlsCertificateFlags.UNKNOWN_CA); - set_data_streams(tls_cx); - yield tls_cx.handshake_async(Priority.DEFAULT, cancellable); - - // Now that we are on an encrypted line we need to say hello again in order to get the - // updated capabilities. - yield say_hello_async(cancellable); + + switch (endpoint.attempt_starttls(capabilities.has_capability(Capabilities.STARTTLS))) { + case Endpoint.AttemptStarttls.YES: + Response response = yield transaction_async(new Request(Command.STARTTLS)); + if (!response.code.is_starttls_ready()) { + throw new SmtpError.STARTTLS_FAILED("STARTTLS failed: %s", response.to_string()); + } + + // TLS started, lets wrap the connection and shake hands. + TlsClientConnection tls_cx = TlsClientConnection.new(cx, socket_cx.get_remote_address()); + cx = tls_cx; + tls_cx.set_validation_flags(TlsCertificateFlags.UNKNOWN_CA); + set_data_streams(tls_cx); + yield tls_cx.handshake_async(Priority.DEFAULT, cancellable); + + // Now that we are on an encrypted line we need to say hello again in order to get the + // updated capabilities. + yield say_hello_async(cancellable); + break; + + case Endpoint.AttemptStarttls.NO: + // do nothing + break; + + case Endpoint.AttemptStarttls.HALT: + default: + throw new SmtpError.NOT_SUPPORTED("STARTTLS not available for %s", endpoint.to_string()); } - - response = yield transaction_async(authenticator.initiate(), cancellable); - + + Response response = yield transaction_async(authenticator.initiate(), cancellable); + + debug("Initiating SMTP %s authentication", authenticator.to_string()); + // Possible for initiate() Request to: // (a) immediately generate success (due to valid authentication being passed in Request); // (b) immediately fails; @@ -92,14 +101,17 @@ public class Geary.Smtp.ClientConnection { uint8[]? data = authenticator.challenge(step++, response); if (data == null || data.length == 0) data = DataFormat.CANCEL_AUTHENTICATION.data; - + + Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP AUTH Response: %s <%ldb>", to_string(), + data.length); + yield Stream.write_all_async(douts, data, 0, -1, Priority.DEFAULT, cancellable); douts.put_string(DataFormat.LINE_TERMINATOR); yield douts.flush_async(Priority.DEFAULT, cancellable); - + response = yield recv_response_async(cancellable); } - + return response; } @@ -120,6 +132,8 @@ public class Geary.Smtp.ClientConnection { if (!response.code.is_start_data()) return response; + Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Data: <%ldb>", data.length); + yield Stream.write_all_async(douts, data, 0, -1, Priority.DEFAULT, cancellable); douts.put_string(DataFormat.DATA_TERMINATOR); yield douts.flush_async(Priority.DEFAULT, cancellable); @@ -130,6 +144,8 @@ public class Geary.Smtp.ClientConnection { public async void send_request_async(Request request, Cancellable? cancellable = null) throws Error { check_connected(); + Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Request: %s", to_string(), request.to_string()); + douts.put_string(request.serialize()); douts.put_string(DataFormat.LINE_TERMINATOR); yield douts.flush_async(Priority.DEFAULT, cancellable); @@ -150,12 +166,15 @@ public class Geary.Smtp.ClientConnection { // lines should never be empty; if it is, then somebody didn't throw an exception assert(lines.size > 0); - return new Response(lines); + Response response = new Response(lines); + Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Response: %s", to_string(), response.to_string()); + + return response; } public async Response say_hello_async(Cancellable? cancellable = null) throws Error { // try EHLO first, then fall back on HELO - Response response = yield transaction_async(new Request(Command.EHLO), cancellable); + Response response = yield transaction_async(new EhloRequest.for_endpoint(endpoint), cancellable); if (response.code.is_success_completed()) { // save list of caps returned in EHLO command, skipping first line because it's the // EHLO response @@ -165,10 +184,14 @@ public class Geary.Smtp.ClientConnection { capabilities.add_capability(response.lines[ctr].explanation); } } else { - response = yield transaction_async(new Request(Command.HELO), cancellable); - if (!response.code.is_success_completed()) - throw new SmtpError.SERVER_ERROR("Refused service: %s", response.to_string()); + string first_response = response.to_string().strip(); + response = yield transaction_async(new HeloRequest.for_endpoint(endpoint), cancellable); + if (!response.code.is_success_completed()) { + throw new SmtpError.SERVER_ERROR("Refused service: \"%s\" and \"%s\"", first_response, + response.to_string().strip()); + } } + return response; } @@ -180,7 +203,7 @@ public class Geary.Smtp.ClientConnection { public async Response transaction_async(Request request, Cancellable? cancellable = null) throws Error { yield send_request_async(request, cancellable); - + return yield recv_response_async(cancellable); } diff --git a/src/engine/smtp/smtp-error.vala b/src/engine/smtp/smtp-error.vala index e3538f15..162a2869 100644 --- a/src/engine/smtp/smtp-error.vala +++ b/src/engine/smtp/smtp-error.vala @@ -11,6 +11,7 @@ public errordomain Geary.SmtpError { SERVER_ERROR, ALREADY_CONNECTED, NOT_CONNECTED, - REQUIRED_FIELD + REQUIRED_FIELD, + NOT_SUPPORTED } diff --git a/src/engine/smtp/smtp-request.vala b/src/engine/smtp/smtp-request.vala index 2e07060a..56f504b1 100644 --- a/src/engine/smtp/smtp-request.vala +++ b/src/engine/smtp/smtp-request.vala @@ -35,6 +35,26 @@ public class Geary.Smtp.Request { } } +public class Geary.Smtp.HeloRequest : Geary.Smtp.Request { + public HeloRequest(string domain) { + base (Command.HELO, { domain }); + } + + public HeloRequest.for_endpoint(Geary.Endpoint endpoint) { + this (endpoint.host_specifier); + } +} + +public class Geary.Smtp.EhloRequest : Geary.Smtp.Request { + public EhloRequest(string domain) { + base (Command.EHLO, { domain }); + } + + public EhloRequest.for_endpoint(Geary.Endpoint endpoint) { + this (endpoint.host_specifier); + } +} + public class Geary.Smtp.MailRequest : Geary.Smtp.Request { public MailRequest(Geary.RFC822.MailboxAddress from) { base (Command.MAIL, { "from:%s".printf(from.get_simple_address()) }); diff --git a/src/engine/util/util-generic-capabilities.vala b/src/engine/util/util-generic-capabilities.vala index 1c1da098..e464b768 100644 --- a/src/engine/util/util-generic-capabilities.vala +++ b/src/engine/util/util-generic-capabilities.vala @@ -5,22 +5,37 @@ */ public class Geary.GenericCapabilities : Object { - private Gee.ArrayList list = new Gee.ArrayList(String.stri_equal); - private Gee.HashMap map = new Gee.HashMap( - String.stri_hash, String.stri_equal); + public string separator { get; private set; } + + private Gee.HashMultiMap map = new Gee.HashMultiMap( + String.stri_hash, String.stri_equal, String.nullable_stri_hash, String.nullable_stri_equal); /** * Creates an empty set of capabilities. */ - public GenericCapabilities(){ + public GenericCapabilities(string separator) { + assert(!String.is_empty(separator)); + + this.separator = separator; } public bool is_empty() { - return map.is_empty; + return (map.size == 0); + } + + public bool parse_and_add_capability(string text) { + string[] tokens = text.split(separator, 2); + if (tokens.length == 1) + add_capability(tokens[0]); + else if (tokens.length == 2) + add_capability(tokens[0], tokens[1]); + else + return false; + + return true; } public void add_capability(string name, string? setting = null) { - list.add(name); map.set(name, String.is_empty(setting) ? null : setting); } @@ -28,7 +43,7 @@ public class Geary.GenericCapabilities : Object { * Returns true only if the capability was named as available by the server. */ public bool has_capability(string name) { - return map.has_key(name); + return map.contains(name); } /** @@ -36,40 +51,57 @@ public class Geary.GenericCapabilities : Object { * by the server. */ public bool has_setting(string name, string? setting) { - if (!map.has_key(name)) { + if (!map.contains(name)) return false; - } else if (String.is_empty(setting)) { + + if (String.is_empty(setting)) return true; - } else { - string? stored_setting = map.get(name); - return !String.is_empty(stored_setting) && String.stri_equal(stored_setting, setting); - } + + return map.get(name).contains(setting); } /** - * Returns null if either the capability is available but has no associated setting, or if the + * Returns null if either the capability is available but has no associated settings, or if the * capability is not available. Thus, use has_capability() to determine if available, then * this method to get its value (if one is expected). Often has_setting() is a better choice. */ - public string? get_setting(string name) { - return map.get(name); + public Gee.Collection? get_settings(string name) { + Gee.Collection settings = map.get(name); + + return (settings.size > 0) ? settings : null; } - public Gee.List get_all_names() { - return list.read_only_view; + public Gee.Set? get_all_names() { + Gee.Set names = map.get_keys(); + + return (names.size > 0) ? names : null; + } + + private void append(StringBuilder builder, string text) { + if (!String.is_empty(builder.str)) + builder.append_c(' '); + + builder.append(text); } public virtual string to_string() { + Gee.Set? names = get_all_names(); + if (names == null || names.size == 0) + return ""; + StringBuilder builder = new StringBuilder(); - foreach (string name in list) { - if (!String.is_empty(builder.str)) - builder.append_c(' '); - - string? setting = map.get(name); - if (String.is_empty(setting)) - builder.append(name); - else - builder.append_printf("%s=%s", name, setting); + foreach (string name in names) { + Gee.Collection? settings = get_settings(name); + if (settings == null || settings.size == 0) { + append(builder, name); + } else { + foreach (string setting in settings) { + if (String.is_empty(setting)) + append(builder, name); + else + append(builder, "%s%s%s".printf(name, separator, setting)); + } + } } return builder.str; diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala index e065947b..d9d53ae1 100644 --- a/src/engine/util/util-string.vala +++ b/src/engine/util/util-string.vala @@ -10,8 +10,8 @@ extern string glib_substring(string str, long start_pos, long end_pos); namespace Geary.String { -public inline bool is_null_or_whitespace(string? str) { - return (str == null || str.strip()[0] == 0); +public bool is_empty_or_whitespace(string? str) { + return (str == null || str[0] == 0 || str.strip()[0] == 0); } public inline bool is_empty(string? str) { @@ -50,10 +50,25 @@ public uint stri_hash(void *str) { return str_hash(((string *) str)->down()); } +public uint nullable_stri_hash(void *str) { + return (str != null) ? stri_hash(str) : 0; +} + public bool stri_equal(void *a, void *b) { return str_equal(((string *) a)->down(), ((string *) b)->down()); } +public bool nullable_stri_equal(void *a, void *b) { + if (a == null) + return (b == null); + + // a != null, so always false + if (b == null) + return false; + + return stri_equal(a, b); +} + /** * Returns char from 0 to 9 converted to an int. If a non-numeric value, -1 is returned. */ diff --git a/ui/login.glade b/ui/login.glade index 790ab799..0f65a30f 100644 --- a/ui/login.glade +++ b/ui/login.glade @@ -54,7 +54,7 @@ 2 4 - + True True True @@ -73,7 +73,7 @@ - + True True True @@ -92,13 +92,13 @@ - + True False 0 _Email address: True - username + entry: email 0 @@ -114,7 +114,7 @@ 0 _Password: True - password + entry: password 0 @@ -124,7 +124,7 @@ - + True False True @@ -143,7 +143,7 @@ 0 _Service: True - service + combo: service 0 @@ -159,7 +159,7 @@ 0 N_ame: True - real_name + entry: real_name 0 @@ -169,7 +169,7 @@ - + True True True @@ -187,7 +187,7 @@ - + Re_member password False True @@ -217,18 +217,20 @@ - + False True False + 2 + 4 - + True False 0 - 12 + 10 IMAP settings @@ -249,7 +251,7 @@ 12 Se_rver: True - imap host + entry: imap host 0 @@ -259,7 +261,7 @@ - + True True True @@ -281,7 +283,7 @@ 12 P_ort: True - imap port + entry: imap port 2 @@ -291,7 +293,7 @@ - + True True @@ -307,43 +309,7 @@ - - True - False - 8 - 0 - 12 - SMTP settings - - - - - - 0 - 5 - 4 - 1 - - - - - True - False - 0 - 10 - Ser_ver: - True - smtp host - - - 0 - 6 - 1 - 1 - - - - + True True @@ -352,30 +318,13 @@ 1 - 6 + 9 1 1 - - True - False - 0 - 12 - Por_t: - True - smtp port - - - 2 - 6 - 1 - 1 - - - - + True True @@ -385,140 +334,393 @@ 3 - 6 + 9 1 1 - - No encrypt_ion - False - True - True - False - 12 - 12 - False - True - 0 - True - - - 0 - 7 - 4 - 1 - - - - - STARTTLS aut_hentication - False - True - True - False - 12 - 12 - False - True - 0 - True - smtp none - - - 0 - 9 - 4 - 1 - - - - - SSL/TLS encr_yption - False - True - True - False - 12 - 12 - False - True - 0 - True - True - smtp none - - - 0 - 8 - 4 - 1 - - - - + _No encryption False True True False - 12 - 12 + 2 False True 0 True - 0 - 2 - 4 + 1 + 5 + 3 1 - + STARTTLS a_uthentication False True True False - 12 - 12 False True 0 True - imap none + radio: imap none - 0 - 4 - 4 + 1 + 7 + 3 1 - + SS_L/TLS encryption False True True False - 12 - 12 False True 0 True True - imap none + radio: imap none + + + 1 + 6 + 3 + 1 + + + + + STARTTLS aut_hentication + False + True + True + False + False + True + 0 + 0.50999999046325684 + True + radio: smtp none + + + 1 + 15 + 3 + 1 + + + + + SSL/TLS encr_yption + False + True + True + False + False + True + 0 + True + radio: smtp none + + + 1 + 14 + 3 + 1 + + + + + No encrypt_ion + False + True + True + False + 2 + False + True + 0 + True + True + + + 1 + 13 + 3 + 1 + + + + + True + False + 0 + 12 + Ser_ver: + True + entry: smtp host + + + 0 + 9 + 1 + 1 + + + + + True + False + 0 + 12 + Por_t: + True + entry: smtp port + + + 2 + 9 + 1 + 1 + + + + + True + False + 8 + 0 + 10 + SMTP settings + + + + + + 0 + 8 + 1 + 1 + + + + + True + False + 0 + 12 + Username: + + + 0 + 10 + 1 + 1 + + + + + True + False + 0 + 12 + Password: + + + 0 + 11 + 1 + 1 + + + + + True + True + + + + 1 + 10 + 3 + 1 + + + + + True + True + False + + + + 1 + 11 + 3 + 1 + + + + + Remember password + False + True + True + False + False + 0 + True + + + 1 + 12 + 3 + 1 + + + + + True + False + 0 + 12 + Username: + + + 0 + 2 + 1 + 1 + + + + + True + False + 0 + 12 + Password: 0 3 - 4 + 1 + 1 + + + + + True + True + + + + 1 + 2 + 3 + 1 + + + + + True + True + False + + + + 1 + 3 + 3 + 1 + + + + + Remember password + False + True + True + False + False + 0 + True + + + 1 + 4 + 3 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + False + 2 + 0 + 12 + Encryption: + + + 0 + 5 + 1 + 1 + + + + + True + False + 2 + 0 + 12 + Encryption: + + + 0 + 13 + 1 1 diff --git a/ui/password-dialog.glade b/ui/password-dialog.glade index b8dd53d6..aa5b7201 100644 --- a/ui/password-dialog.glade +++ b/ui/password-dialog.glade @@ -55,33 +55,18 @@ - + True False 6 12 - + True False 0 - Email address: - - - 0 - 0 - 1 - 1 - - - - - True - False - 0 - _Password: - True - password_entry + 6 + Username: 0 @@ -91,7 +76,24 @@ - + + True + False + 0 + 6 + _Password: + True + entry: imap password + + + 0 + 2 + 1 + 1 + + + + True False True @@ -100,13 +102,13 @@ 1 - 0 + 1 1 1 - + True True True @@ -117,13 +119,13 @@ 1 - 1 + 2 1 1 - + _Remember password False True @@ -136,11 +138,28 @@ 1 - 2 + 3 1 1 + + + True + False + 0 + IMAP Credentials + + + + + + 0 + 0 + 2 + 1 + + @@ -151,6 +170,115 @@ 1 + + + True + False + 6 + 12 + + + True + False + 0 + SMTP Credentials + + + + + + 0 + 0 + 2 + 1 + + + + + True + False + 0 + 6 + Username: + + + 0 + 1 + 1 + 1 + + + + + True + False + 0 + 6 + Password: + + + 0 + 2 + 1 + 1 + + + + + True + False + 0 + 2 + + + 1 + 1 + 1 + 1 + + + + + True + True + False + + + + 1 + 2 + 1 + 1 + + + + + Remember password + False + True + True + False + False + 0 + True + + + 1 + 3 + 1 + 1 + + + + + + + + False + True + 2 + + True @@ -163,7 +291,7 @@ 6 12 - + True False 0 @@ -178,7 +306,7 @@ - + True False 0 @@ -193,7 +321,7 @@ - + True False 0 @@ -206,7 +334,7 @@ - + True False 0 @@ -219,7 +347,7 @@ - + True False 0 @@ -237,7 +365,7 @@ - + True False 6 @@ -256,7 +384,7 @@ - + True False 0 @@ -271,7 +399,7 @@ - + True False 0 @@ -286,7 +414,7 @@ - + True False 0 @@ -301,7 +429,7 @@ - + True False 0 @@ -314,7 +442,7 @@ - + True False 0 @@ -327,7 +455,7 @@ - + True False 0 @@ -340,7 +468,7 @@ - + True False 6 @@ -359,7 +487,7 @@ - + True False 0 @@ -374,7 +502,7 @@ - + True False 0 @@ -389,7 +517,7 @@ - + True False 0 @@ -404,7 +532,7 @@ - + True False 0 @@ -417,7 +545,7 @@ - + True False 0 @@ -430,7 +558,7 @@ - + True False 0 @@ -454,7 +582,7 @@ - + True False _Details @@ -465,7 +593,7 @@ False True - 2 + 3