diff --git a/po/POTFILES.in b/po/POTFILES.in index be3c3fec..8ab4fc13 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -4,6 +4,7 @@ desktop/geary-autostart.desktop.in [type: gettext/ini]desktop/geary-attach.contract.in src/client/accounts/account-dialog-account-list-pane.vala src/client/accounts/account-dialog-add-edit-pane.vala +src/client/accounts/account-dialog-edit-alternate-emails-pane.vala src/client/accounts/account-dialog-pane.vala src/client/accounts/account-dialog-remove-confirm-pane.vala src/client/accounts/account-dialog-remove-fail-pane.vala @@ -381,6 +382,7 @@ src/mailer/main.vala [type: gettext/glade]ui/certificate_warning_dialog.glade [type: gettext/glade]ui/composer_accelerators.ui [type: gettext/glade]ui/composer.glade +[type: gettext/glade]ui/edit_alternate_emails.glade [type: gettext/glade]ui/find_bar.glade [type: gettext/glade]ui/login.glade [type: gettext/glade]ui/message.glade diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 102917e0..d94fcb74 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -321,6 +321,7 @@ client/application/secret-mediator.vala client/accounts/account-dialog.vala client/accounts/account-dialog-account-list-pane.vala client/accounts/account-dialog-add-edit-pane.vala +client/accounts/account-dialog-edit-alternate-emails-pane.vala client/accounts/account-dialog-pane.vala client/accounts/account-dialog-remove-confirm-pane.vala client/accounts/account-dialog-remove-fail-pane.vala diff --git a/src/client/accounts/account-dialog-add-edit-pane.vala b/src/client/accounts/account-dialog-add-edit-pane.vala index 398c9eb5..65d30ed8 100644 --- a/src/client/accounts/account-dialog-add-edit-pane.vala +++ b/src/client/accounts/account-dialog-add-edit-pane.vala @@ -17,6 +17,8 @@ public class AccountDialogAddEditPane : AccountDialogPane { public signal void size_changed(); + public signal void edit_alternate_emails(string email_address); + public AccountDialogAddEditPane(Gtk.Stack stack) { base(stack); @@ -35,7 +37,8 @@ public class AccountDialogAddEditPane : AccountDialogPane { ok_button.clicked.connect(on_ok); cancel_button.clicked.connect(() => { cancel(); }); - add_edit_page.size_changed.connect(() => { size_changed(); } ); + add_edit_page.size_changed.connect(() => { size_changed(); }); + add_edit_page.edit_alternate_emails.connect(() => { edit_alternate_emails(add_edit_page.email_address); }); pack_start(add_edit_page); pack_start(button_box, false, false); diff --git a/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala b/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala new file mode 100644 index 00000000..e621ee58 --- /dev/null +++ b/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala @@ -0,0 +1,205 @@ +/* Copyright 2015 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class AccountDialogEditAlternateEmailsPane : AccountDialogPane { + private class ListItem : Gtk.Label { + public Geary.RFC822.MailboxAddress mailbox; + + public ListItem(Geary.RFC822.MailboxAddress mailbox) { + this.mailbox = mailbox; + + label = "%s".printf(Geary.HTML.escape_markup(mailbox.get_full_address())); + use_markup = true; + ellipsize = Pango.EllipsizeMode.END; + xalign = 0.0f; + } + } + + public string? email { get; private set; default = null; } + + public bool changed { get; private set; default = false; } + + private Gtk.Label title_label; + private Gtk.Entry email_entry; + private Gtk.Button add_button; + private Gtk.ListBox address_listbox; + private Gtk.ToolButton delete_button; + private Gtk.Button cancel_button; + private Gtk.Button update_button; + private ListItem? selected_item = null; + + private Geary.AccountInformation? account_info = null; + private Geary.RFC822.MailboxAddress? primary_mailbox = null; + private Gee.HashSet mailboxes = new Gee.HashSet(); + + public signal void done(); + + public AccountDialogEditAlternateEmailsPane(Gtk.Stack stack) { + base (stack); + + Gtk.Builder builder = GearyApplication.instance.create_builder("edit_alternate_emails.glade"); + + // Primary container + pack_start((Gtk.Widget) builder.get_object("container")); + + title_label = (Gtk.Label) builder.get_object("title_label"); + email_entry = (Gtk.Entry) builder.get_object("email_entry"); + add_button = (Gtk.Button) builder.get_object("add_button"); + address_listbox = (Gtk.ListBox) builder.get_object("address_listbox"); + delete_button = (Gtk.ToolButton) builder.get_object("delete_button"); + cancel_button = (Gtk.Button) builder.get_object("cancel_button"); + update_button = (Gtk.Button) builder.get_object("update_button"); + + // Clear text when the secondary icon (not always available) is pressed + email_entry.icon_release.connect((pos) => { + if (pos == Gtk.EntryIconPosition.SECONDARY) + email_entry.text = ""; + }); + + email_entry.bind_property("text", add_button, "sensitive", BindingFlags.SYNC_CREATE, + transform_email_to_sensitive); + email_entry.notify["text-length"].connect(on_email_entry_text_length_changed); + bind_property("changed", update_button, "sensitive", BindingFlags.SYNC_CREATE); + + delete_button.sensitive = false; + + address_listbox.row_selected.connect(on_row_selected); + add_button.clicked.connect(on_add_clicked); + delete_button.clicked.connect(on_delete_clicked); + cancel_button.clicked.connect(() => { done(); }); + update_button.clicked.connect(on_update_clicked); + } + + private bool validate_address_text(string email_address, out Geary.RFC822.MailboxAddress? parsed) { + parsed = null; + + Geary.RFC822.MailboxAddresses mailboxes = new Geary.RFC822.MailboxAddresses.from_rfc822_string( + email_address); + if (mailboxes.size != 1) + return false; + + Geary.RFC822.MailboxAddress mailbox = mailboxes.get(0); + + if (!mailbox.is_valid()) + return false; + + if (Geary.String.stri_equal(mailbox.address, primary_mailbox.address)) + return false; + + if (Geary.String.is_empty(mailbox.address)) + return false; + + parsed = mailbox; + + return true; + } + + private bool transform_email_to_sensitive(Binding binding, Value source, ref Value target) { + Geary.RFC822.MailboxAddress? parsed; + target = validate_address_text(email_entry.text, out parsed) && !mailboxes.contains(parsed); + + return true; + } + + private void on_email_entry_text_length_changed() { + bool has_text = email_entry.text_length != 0; + + email_entry.secondary_icon_name = has_text ? "edit-clear-symbolic" : null; + email_entry.secondary_icon_sensitive = has_text; + email_entry.secondary_icon_activatable = has_text; + } + + public void set_account(Geary.AccountInformation account_info) { + this.account_info = account_info; + + email = account_info.email; + primary_mailbox = account_info.get_primary_mailbox_address(); + mailboxes.clear(); + changed = false; + + // reset/clear widgets + title_label.label = _("Additional addresses for %s").printf(account_info.email); + email_entry.text = ""; + + // clear listbox + foreach (Gtk.Widget widget in address_listbox.get_children()) + address_listbox.remove(widget); + + // Add all email addresses; add_email_address() silently drops the primary address + foreach (Geary.RFC822.MailboxAddress mailbox in account_info.get_all_mailboxes()) + add_mailbox(mailbox, false); + } + + public override void present() { + base.present(); + + // because in a Gtk.Stack, need to do this manually after presenting + email_entry.grab_focus(); + add_button.has_default = true; + } + + private void add_mailbox(Geary.RFC822.MailboxAddress mailbox, bool is_change) { + if (mailboxes.contains(mailbox) || primary_mailbox.equal_to(mailbox)) + return; + + mailboxes.add(mailbox); + + ListItem item = new ListItem(mailbox); + item.show_all(); + address_listbox.add(item); + + if (is_change) + changed = true; + } + + private void remove_mailbox(Geary.RFC822.MailboxAddress address) { + if (!mailboxes.remove(address)) + return; + + foreach (Gtk.Widget widget in address_listbox.get_children()) { + Gtk.ListBoxRow row = (Gtk.ListBoxRow) widget; + ListItem item = (ListItem) row.get_child(); + + if (item.mailbox.equal_to(address)) { + address_listbox.remove(widget); + + changed = true; + + break; + } + } + } + + private void on_row_selected(Gtk.ListBoxRow? row) { + selected_item = (row != null) ? (ListItem) row.get_child() : null; + delete_button.sensitive = (selected_item != null); + } + + private void on_add_clicked() { + Geary.RFC822.MailboxAddress? parsed; + if (!validate_address_text(email_entry.text, out parsed) || parsed == null) + return; + + add_mailbox(parsed, true); + + // reset state for next input + email_entry.text = ""; + email_entry.grab_focus(); + add_button.has_default = true; + } + + private void on_delete_clicked() { + if (selected_item != null) + remove_mailbox(selected_item.mailbox); + } + + private void on_update_clicked() { + account_info.replace_alternate_mailboxes(mailboxes); + + done(); + } +} + diff --git a/src/client/accounts/account-dialog.vala b/src/client/accounts/account-dialog.vala index 1d42763c..03c6036c 100644 --- a/src/client/accounts/account-dialog.vala +++ b/src/client/accounts/account-dialog.vala @@ -13,6 +13,7 @@ public class AccountDialog : Gtk.Dialog { private AccountDialogSpinnerPane spinner_pane; private AccountDialogRemoveConfirmPane remove_confirm_pane; private AccountDialogRemoveFailPane remove_fail_pane; + private AccountDialogEditAlternateEmailsPane edit_alternate_emails_pane; private Gtk.HeaderBar headerbar = new Gtk.HeaderBar(); public AccountDialog(Gtk.Window parent) { @@ -33,6 +34,7 @@ public class AccountDialog : Gtk.Dialog { spinner_pane = new AccountDialogSpinnerPane(stack); remove_confirm_pane = new AccountDialogRemoveConfirmPane(stack); remove_fail_pane = new AccountDialogRemoveFailPane(stack); + edit_alternate_emails_pane = new AccountDialogEditAlternateEmailsPane(stack); // Connect signals from pages. account_list_pane.add_account.connect(on_add_account); @@ -41,9 +43,11 @@ public class AccountDialog : Gtk.Dialog { add_edit_pane.ok.connect(on_save_add_or_edit); add_edit_pane.cancel.connect(on_cancel_back_to_list); add_edit_pane.size_changed.connect(() => { resize(1, 1); }); + add_edit_pane.edit_alternate_emails.connect(on_edit_alternate_emails); remove_confirm_pane.ok.connect(on_delete_account_confirmed); remove_confirm_pane.cancel.connect(on_cancel_back_to_list); remove_fail_pane.ok.connect(on_cancel_back_to_list); + edit_alternate_emails_pane.done.connect(on_done_back_to_editor); // Set default page. account_list_pane.present(); @@ -132,6 +136,15 @@ public class AccountDialog : Gtk.Dialog { } } + private void on_edit_alternate_emails(string email_address) { + Geary.AccountInformation? account_info = get_account_info_for_email(email_address); + if (account_info == null) + return; + + edit_alternate_emails_pane.set_account(account_info); + edit_alternate_emails_pane.present(); + } + private void on_delete_account_confirmed(Geary.AccountInformation? account) { assert(account != null); // Should not be able to happen since we checked earlier. @@ -197,5 +210,9 @@ public class AccountDialog : Gtk.Dialog { private void on_cancel_back_to_list() { account_list_pane.present(); } + + private void on_done_back_to_editor() { + add_edit_pane.present(); + } } diff --git a/src/client/accounts/add-edit-page.vala b/src/client/accounts/add-edit-page.vala index 80ec59e4..226a9905 100644 --- a/src/client/accounts/add-edit-page.vala +++ b/src/client/accounts/add-edit-page.vala @@ -169,6 +169,7 @@ public class AddEditPage : Gtk.Box { private Gtk.ComboBoxText combo_service; private Gtk.CheckButton check_remember_password; private Gtk.CheckButton check_save_sent_mail; + private Gtk.Button alternate_email_button; // Signature private Gtk.Box composer_container; @@ -215,6 +216,8 @@ public class AddEditPage : Gtk.Box { public signal void size_changed(); + public signal void edit_alternate_emails(); + public AddEditPage() { Object(orientation: Gtk.Orientation.VERTICAL, spacing: 4); @@ -239,6 +242,7 @@ public class AddEditPage : Gtk.Box { entry_password = (Gtk.Entry) builder.get_object("entry: password"); check_remember_password = (Gtk.CheckButton) builder.get_object("check: remember_password"); check_save_sent_mail = (Gtk.CheckButton) builder.get_object("check: save_sent_mail"); + alternate_email_button = (Gtk.Button) builder.get_object("button: edit_alternate_email"); label_error = (Gtk.Label) builder.get_object("label: error"); other_info = (Gtk.Alignment) builder.get_object("container: other_info"); @@ -328,6 +332,7 @@ public class AddEditPage : Gtk.Box { check_smtp_use_imap_credentials.toggled.connect(on_changed); check_smtp_noauth.toggled.connect(on_changed); check_save_drafts.toggled.connect(on_changed); + alternate_email_button.clicked.connect(on_alternate_email_button_clicked); entry_email.changed.connect(on_email_changed); entry_password.changed.connect(on_password_changed); @@ -496,6 +501,10 @@ public class AddEditPage : Gtk.Box { info_changed(); } + private void on_alternate_email_button_clicked() { + edit_alternate_emails(); + } + // Prevent non-printable characters in nickname field. private void on_nickname_insert_text(Gtk.Editable e, string text, int length, ref int position) { unichar c; @@ -696,12 +705,14 @@ public class AddEditPage : Gtk.Box { // Updates UI based on various options. internal void update_ui() { base.show_all(); + welcome_box.visible = mode == PageMode.WELCOME; entry_nickname.visible = label_nickname.visible = mode != PageMode.WELCOME; storage_container.visible = mode == PageMode.EDIT; check_save_sent_mail.visible = mode == PageMode.EDIT; check_save_drafts.visible = mode == PageMode.EDIT; composer_container.visible = mode == PageMode.EDIT; + alternate_email_button.visible = mode == PageMode.EDIT; if (get_service_provider() == Geary.ServiceProvider.OTHER) { // Display all options for custom providers. diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 4c4bff37..e34fa536 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -26,6 +26,17 @@ public class ComposerWidget : Gtk.EventBox { INLINE, INLINE_COMPACT } + + private class FromAddressMap { + public Geary.Account account; + public Geary.RFC822.MailboxAddress? sender; + public Geary.RFC822.MailboxAddresses from; + public FromAddressMap(Geary.Account a, Geary.RFC822.MailboxAddresses f, Geary.RFC822.MailboxAddress? s = null) { + account = a; + from = f; + sender = s; + } + } public const string ACTION_UNDO = "undo"; public const string ACTION_REDO = "redo"; @@ -126,7 +137,9 @@ public class ComposerWidget : Gtk.EventBox { public Geary.Account account { get; private set; } - public string from { get; set; } + public Geary.RFC822.MailboxAddress sender { get; set; } + + public Geary.RFC822.MailboxAddresses from { get; set; } public string to { get { return to_entry.get_text(); } @@ -205,6 +218,7 @@ public class ComposerWidget : Gtk.EventBox { private Gtk.Label from_label; private Gtk.Label from_single; private Gtk.ComboBoxText from_multiple = new Gtk.ComboBoxText(); + private Gee.ArrayList from_list = new Gee.ArrayList(); private EmailEntry to_entry; private EmailEntry cc_entry; private Gtk.Label bcc_label; @@ -351,6 +365,7 @@ public class ComposerWidget : Gtk.EventBox { // Listen to account signals to update from menu. Geary.Engine.instance.account_available.connect(update_from_field); Geary.Engine.instance.account_unavailable.connect(update_from_field); + // TODO: also listen for account updates to allow adding identities while writing an email Gtk.ScrolledWindow scroll = new Gtk.ScrolledWindow(null, null); scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); @@ -427,9 +442,8 @@ public class ComposerWidget : Gtk.EventBox { add_extra_accelerators(); - from = account.information.get_from().to_rfc822_string(); + from = account.information.get_primary_from(); update_from_field(); - from_multiple.changed.connect(on_from_changed); if (referred != null) { if (compose_type != ComposeType.NEW_MESSAGE) { @@ -573,7 +587,7 @@ public class ComposerWidget : Gtk.EventBox { chain.append(attachments_box); box.set_focus_chain(chain); - // If there's only one account, open the drafts manager. If there's more than one account, + // If there's only one From option, open the drafts manager. If there's more than one, // the drafts manager will be opened by on_from_changed(). if (!from_multiple.visible) open_draft_manager_async.begin(null); @@ -675,9 +689,9 @@ public class ComposerWidget : Gtk.EventBox { compose_type = ComposeType.REPLY_ALL; to_entry.modified = cc_entry.modified = bcc_entry.modified = false; - if (!Geary.RFC822.Utils.equal(to_entry.addresses, reply_to_addresses)) + if (!to_entry.addresses.equal_to(reply_to_addresses)) to_entry.modified = true; - if (cc != "" && !Geary.RFC822.Utils.equal(cc_entry.addresses, reply_cc_addresses)) + if (cc != "" && !cc_entry.addresses.equal_to(reply_cc_addresses)) cc_entry.modified = true; if (bcc != "") bcc_entry.modified = true; @@ -704,6 +718,33 @@ public class ComposerWidget : Gtk.EventBox { } } + private bool check_preferred_from_address(Gee.List account_addresses, + Geary.RFC822.MailboxAddresses? referred_addresses) { + if (referred_addresses != null) { + foreach (Geary.RFC822.MailboxAddress address in account_addresses) { + if (referred_addresses.get_all().contains(address)) { + from = new Geary.RFC822.MailboxAddresses.single(address); + return true; + } + } + } + return false; + } + + private void set_preferred_from_address(Geary.Email referred, ComposeType compose_type) { + if (compose_type == ComposeType.NEW_MESSAGE) { + if (referred.from != null) + from = referred.from; + } else { + Gee.List account_addresses = account.information.get_all_mailboxes(); + if (!check_preferred_from_address(account_addresses, referred.to)) { + if (!check_preferred_from_address(account_addresses, referred.cc)) + if (!check_preferred_from_address(account_addresses, referred.bcc)) + check_preferred_from_address(account_addresses, referred.from); + } + } + } + private void on_load_finished(WebKit.WebFrame frame) { if (get_realized()) on_load_finished_and_realized(); @@ -845,9 +886,8 @@ public class ComposerWidget : Gtk.EventBox { public Geary.ComposedEmail get_composed_email(DateTime? date_override = null, bool only_html = false) { Geary.ComposedEmail email = new Geary.ComposedEmail( - date_override ?? new DateTime.now_local(), - new Geary.RFC822.MailboxAddresses.from_rfc822_string(from) - ); + date_override ?? new DateTime.now_local(), from); + email.sender = sender; if (to_entry.addresses != null) email.to = to_entry.addresses; @@ -947,15 +987,16 @@ public class ComposerWidget : Gtk.EventBox { private void add_recipients_and_ids(ComposeType type, Geary.Email referred, bool modify_headers = true) { - string? sender_address = account.information.get_mailbox_address().address; + Gee.List sender_addresses = account.information.get_all_mailboxes(); Geary.RFC822.MailboxAddresses to_addresses = - Geary.RFC822.Utils.create_to_addresses_for_reply(referred, sender_address); + Geary.RFC822.Utils.create_to_addresses_for_reply(referred, sender_addresses); Geary.RFC822.MailboxAddresses cc_addresses = - Geary.RFC822.Utils.create_cc_addresses_for_reply_all(referred, sender_address); + Geary.RFC822.Utils.create_cc_addresses_for_reply_all(referred, sender_addresses); reply_to_addresses = Geary.RFC822.Utils.merge_addresses(reply_to_addresses, to_addresses); reply_cc_addresses = Geary.RFC822.Utils.remove_addresses( Geary.RFC822.Utils.merge_addresses(reply_cc_addresses, cc_addresses), reply_to_addresses); + set_preferred_from_address(referred, type); if (!modify_headers) return; @@ -2224,7 +2265,39 @@ public class ComposerWidget : Gtk.EventBox { } } + private bool add_account_emails_to_from_list(Geary.Account account, bool set_active = false) { + Geary.RFC822.MailboxAddresses primary_address = new Geary.RFC822.MailboxAddresses.single( + account.information.get_primary_mailbox_address()); + from_multiple.append_text(primary_address.to_rfc822_string()); + from_list.add(new FromAddressMap(account, primary_address)); + if (!set_active && from.equal_to(primary_address)) { + from_multiple.set_active(from_list.size - 1); + set_active = true; + } + + if (account.information.alternate_mailboxes != null) { + foreach (Geary.RFC822.MailboxAddress alternate_mailbox in account.information.alternate_mailboxes) { + Geary.RFC822.MailboxAddresses addresses = new Geary.RFC822.MailboxAddresses.single( + alternate_mailbox); + + // Displayed in the From dropdown to indicate an "alternate email address" + // for an account. The first printf argument will be the alternate email + // address, and the second will be the account's primary email address. + string display = _("%1$s via %2$s").printf(addresses.to_rfc822_string(), account.information.email); + from_multiple.append_text(display); + from_list.add(new FromAddressMap(account, addresses)); + + if (!set_active && from.equal_to(addresses)) { + from_multiple.set_active(from_list.size - 1); + set_active = true; + } + } + } + return set_active; + } + private void update_from_field() { + from_multiple.changed.disconnect(on_from_changed); from_single.visible = from_multiple.visible = from_label.visible = false; Gee.Map accounts; @@ -2242,44 +2315,49 @@ public class ComposerWidget : Gtk.EventBox { return; // If there's only one account, show nothing. (From fields are hidden above.) - if (accounts.size <= 1) + if (accounts.size < 1 || (accounts.size == 1 && Geary.traverse( + accounts.values).first().alternate_mailboxes == null)) return; from_label.visible = true; + from_label.set_use_underline(true); + from_label.set_mnemonic_widget(from_multiple); + // Composer label (with mnemonic underscore) for the account selector + // when choosing what address to send a message from. + from_label.set_text_with_mnemonic(_("_From:")); + + from_multiple.visible = true; + from_multiple.remove_all(); + from_list = new Gee.ArrayList(); + + bool set_active = false; if (compose_type == ComposeType.NEW_MESSAGE) { - // For new messages, show the account combo-box. - from_label.set_use_underline(true); - from_label.set_mnemonic_widget(from_multiple); - // Composer label (with mnemonic underscore) for the account selector - // when choosing what address to send a message from. - from_label.set_text_with_mnemonic(_("_From:")); - - from_multiple.visible = true; - from_multiple.remove_all(); - foreach (Geary.AccountInformation a in accounts.values) - from_multiple.append(a.email, a.get_mailbox_address().get_full_address()); - - // Set the active account to the currently selected account, or failing that, set it - // to the first account in the list. - if (!from_multiple.set_active_id(account.information.email)) - from_multiple.set_active(0); + set_active = add_account_emails_to_from_list(account); + foreach (Geary.AccountInformation info in accounts.values) { + try { + Geary.Account a = Geary.Engine.instance.get_account_instance(info); + if (a != account) + set_active = add_account_emails_to_from_list(a, set_active); + } catch (Error e) { + debug("Error getting account in composer: %s", e.message); + } + } } else { - // For other types of messages, just show the from account. - from_label.set_use_underline(false); - // Composer label (without mnemonic underscore) for the account selector - // when choosing what address to send a message from. - from_label.set_text(_("From:")); - - from_single.label = account.information.get_mailbox_address().get_full_address(); - from_single.visible = true; + set_active = add_account_emails_to_from_list(account); } + + if (!set_active) { + // The identity or account that was active before has been removed + // use the best we can get now (primary address of the account or any other) + from_multiple.set_active(0); + on_from_changed(); + } + + from_multiple.changed.connect(on_from_changed); } private void on_from_changed() { - if (compose_type != ComposeType.NEW_MESSAGE) - return; - bool changed = false; try { changed = update_from_account(); @@ -2298,24 +2376,19 @@ public class ComposerWidget : Gtk.EventBox { } private bool update_from_account() throws Error { - // Since we've set the combo box ID to the email addresses, we can - // fetch that and use it to grab the account from the engine. - string? id = from_multiple.get_active_id(); - if (id == null) + int index = from_multiple.get_active(); + if (index < 0) return false; - // it's possible for changed signals to fire even though nothing has changed; catch that - // here when possible to avoid a lot of extra work - Geary.AccountInformation? new_account_info = Geary.Engine.instance.get_accounts().get(id); - if (new_account_info == null) - return false; + assert(from_list.size > index); - Geary.Account new_account = Geary.Engine.instance.get_account_instance(new_account_info); + Geary.Account new_account = from_list.get(index).account; + from = from_list.get(index).from; + sender = from_list.get(index).sender; if (new_account == account) return false; account = new_account; - from = new_account_info.get_from().to_rfc822_string(); set_entry_completions(); return true; diff --git a/src/client/conversation-list/conversation-list-store.vala b/src/client/conversation-list/conversation-list-store.vala index 7d831364..eca2ced6 100644 --- a/src/client/conversation-list/conversation-list-store.vala +++ b/src/client/conversation-list/conversation-list-store.vala @@ -285,7 +285,8 @@ public class ConversationListStore : Gtk.ListStore { private void set_row(Gtk.TreeIter iter, Geary.App.Conversation conversation, Geary.Email preview) { FormattedConversationData conversation_data = new FormattedConversationData(conversation, - preview, conversation_monitor.folder, conversation_monitor.folder.account.information.email); + preview, conversation_monitor.folder, + conversation_monitor.folder.account.information.get_all_mailboxes()); Gtk.TreePath? path = get_path(iter); assert(path != null); diff --git a/src/client/conversation-list/formatted-conversation-data.vala b/src/client/conversation-list/formatted-conversation-data.vala index a01e26fe..4cd56d2f 100644 --- a/src/client/conversation-list/formatted-conversation-data.vala +++ b/src/client/conversation-list/formatted-conversation-data.vala @@ -20,22 +20,20 @@ public class FormattedConversationData : Geary.BaseObject { private const int FONT_SIZE_PREVIEW = 8; private class ParticipantDisplay : Geary.BaseObject, Gee.Hashable { - public string key; public Geary.RFC822.MailboxAddress address; public bool is_unread; public ParticipantDisplay(Geary.RFC822.MailboxAddress address, bool is_unread) { - key = address.as_key(); this.address = address; this.is_unread = is_unread; } - public string get_full_markup(string normalized_account_key) { - return get_as_markup((key == normalized_account_key) ? ME : address.get_short_address()); + public string get_full_markup(Gee.List account_mailboxes) { + return get_as_markup((address in account_mailboxes) ? ME : address.get_short_address()); } - public string get_short_markup(string normalized_account_key) { - if (key == normalized_account_key) + public string get_short_markup(Gee.List account_mailboxes) { + if (address in account_mailboxes) return get_as_markup(ME); string short_address = address.get_short_address().strip(); @@ -45,17 +43,17 @@ public class FormattedConversationData : Geary.BaseObject { string[] tokens = short_address.split(", ", 2); short_address = tokens[1].strip(); if (Geary.String.is_empty(short_address)) - return get_full_markup(normalized_account_key); + return get_full_markup(account_mailboxes); } // use first name as delimited by a space string[] tokens = short_address.split(" ", 2); if (tokens.length < 1) - return get_full_markup(normalized_account_key); + return get_full_markup(account_mailboxes); string first_name = tokens[0].strip(); if (Geary.String.is_empty_or_whitespace(first_name)) - return get_full_markup(normalized_account_key); + return get_full_markup(account_mailboxes); return get_as_markup(first_name); } @@ -66,14 +64,11 @@ public class FormattedConversationData : Geary.BaseObject { } public bool equal_to(ParticipantDisplay other) { - if (this == other) - return true; - - return key == other.key; + return address.equal_to(other.address); } public uint hash() { - return key.hash(); + return address.hash(); } } @@ -89,17 +84,17 @@ public class FormattedConversationData : Geary.BaseObject { public Geary.Email? preview { get; private set; default = null; } private Geary.App.Conversation? conversation = null; - private string? account_owner_email = null; + private Gee.List? account_owner_emails = null; private bool use_to = true; private CountBadge count_badge = new CountBadge(2); // Creates a formatted message data from an e-mail. public FormattedConversationData(Geary.App.Conversation conversation, Geary.Email preview, - Geary.Folder folder, string account_owner_email) { + Geary.Folder folder, Gee.List account_owner_emails) { assert(preview.fields.fulfills(ConversationListStore.REQUIRED_FIELDS)); this.conversation = conversation; - this.account_owner_email = account_owner_email; + this.account_owner_emails = account_owner_emails; use_to = (folder != null) && folder.special_folder_type.is_outgoing(); // Load preview-related data. @@ -173,11 +168,9 @@ public class FormattedConversationData : Geary.BaseObject { } private string get_participants_markup(Gtk.Widget widget, bool selected) { - if (conversation == null || account_owner_email == null) + if (conversation == null || account_owner_emails == null || account_owner_emails.size == 0) return ""; - string normalized_account_owner_email = account_owner_email.normalize().casefold(); - // Build chronological list of AuthorDisplay records, setting to unread if any message by // that author is unread Gee.ArrayList list = new Gee.ArrayList(); @@ -210,14 +203,14 @@ public class FormattedConversationData : Geary.BaseObject { rgba_to_markup(get_foreground_rgba(widget, selected)))); if (list.size == 1) { // if only one participant, use full name - builder.append(list[0].get_full_markup(normalized_account_owner_email)); + builder.append(list[0].get_full_markup(account_owner_emails)); } else { bool first = true; foreach (ParticipantDisplay participant in list) { if (!first) builder.append(", "); - builder.append(participant.get_short_markup(normalized_account_owner_email)); + builder.append(participant.get_short_markup(account_owner_emails)); first = false; } } diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala index 7f9f49ac..2623cb09 100644 --- a/src/engine/api/geary-account-information.vala +++ b/src/engine/api/geary-account-information.vala @@ -10,6 +10,7 @@ public class Geary.AccountInformation : BaseObject { private const string GROUP = "AccountInformation"; private const string REAL_NAME_KEY = "real_name"; private const string NICKNAME_KEY = "nickname"; + private const string ALTERNATE_EMAILS_KEY = "alternate_emails"; private const string SERVICE_PROVIDER_KEY = "service_provider"; private const string ORDINAL_KEY = "ordinal"; private const string PREFETCH_PERIOD_DAYS_KEY = "prefetch_period_days"; @@ -60,11 +61,41 @@ public class Geary.AccountInformation : BaseObject { internal File? file = null; + // // IMPORTANT: When adding new properties, be sure to add them to the copy method. + // + /** + * User's name for the {@link primary_mailbox}. + */ public string real_name { get; set; } + + /** + * User label for primary account (not transmitted on wire or used in correspondence). + */ public string nickname { get; set; } + + /** + * The primary email address for the account. + * + * This the RFC822 simple mailbox style, i.e. "jim@example.com". + * + * In general, it's better to use the result of {@link get_primary_mailbox_address}, as the + * {@link Geary.RFC822.MailboxAddress} object is better suited for comparisons, Gee collections, + * validation, composing quoted strings, and so forth. + */ public string email { get; set; } + + /** + * A list of additional email addresses this account accepts. + * + * Use {@link add_alternate_mailbox} or {@link replace_alternate_mailboxes} rather than edit + * this collection directly. + * + * @see get_all_mailboxes + */ + public Gee.List? alternate_mailboxes { get; private set; } + public Geary.ServiceProvider service_provider { get; set; } public int prefetch_period_days { get; set; } @@ -148,6 +179,19 @@ public class Geary.AccountInformation : BaseObject { } finally { real_name = get_string_value(key_file, GROUP, REAL_NAME_KEY); nickname = get_string_value(key_file, GROUP, NICKNAME_KEY); + + // Store alternate emails in a list of case-insensitive strings + Gee.List alt_email_list = get_string_list_value(key_file, GROUP, ALTERNATE_EMAILS_KEY); + if (alt_email_list.size == 0) { + alternate_mailboxes = null; + } else { + foreach (string alt_email in alt_email_list) { + RFC822.MailboxAddresses mailboxes = new RFC822.MailboxAddresses.from_rfc822_string(alt_email); + foreach (RFC822.MailboxAddress mailbox in mailboxes.get_all()) + add_alternate_mailbox(mailbox); + } + } + 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); @@ -231,6 +275,11 @@ public class Geary.AccountInformation : BaseObject { real_name = from.real_name; nickname = from.nickname; email = from.email; + alternate_mailboxes = null; + if (from.alternate_mailboxes != null) { + foreach (RFC822.MailboxAddress alternate_mailbox in from.alternate_mailboxes) + add_alternate_mailbox(alternate_mailbox); + } service_provider = from.service_provider; prefetch_period_days = from.prefetch_period_days; save_sent_mail = from.save_sent_mail; @@ -258,6 +307,48 @@ public class Geary.AccountInformation : BaseObject { email_signature = from.email_signature; } + /** + * Return a list of the primary and all alternate email addresses. + */ + public Gee.List get_all_mailboxes() { + Gee.ArrayList all = new Gee.ArrayList(); + + all.add(get_primary_mailbox_address()); + + if (alternate_mailboxes != null) + all.add_all(alternate_mailboxes); + + return all; + } + + /** + * Add an alternate email address to the account. + * + * Duplicates will be ignored. + */ + public void add_alternate_mailbox(Geary.RFC822.MailboxAddress mailbox) { + if (alternate_mailboxes == null) + alternate_mailboxes = new Gee.ArrayList(); + + if (!alternate_mailboxes.contains(mailbox)) + alternate_mailboxes.add(mailbox); + } + + /** + * Replaces the list of alternate email addresses with the supplied collection. + * + * Duplicates will be ignored. + */ + public void replace_alternate_mailboxes(Gee.Collection? mailboxes) { + alternate_mailboxes = null; + + if (mailboxes == null || mailboxes.size == 0) + return; + + foreach (RFC822.MailboxAddress mailbox in mailboxes) + add_alternate_mailbox(mailbox); + } + /** * Return whether this account allows setting the save_sent_mail option. * If not, save_sent_mail will always be true and setting it will be @@ -712,6 +803,13 @@ public class Geary.AccountInformation : BaseObject { key_file.set_boolean(GROUP, SAVE_SENT_MAIL_KEY, save_sent_mail); key_file.set_boolean(GROUP, USE_EMAIL_SIGNATURE_KEY, use_email_signature); key_file.set_string(GROUP, EMAIL_SIGNATURE_KEY, email_signature); + if (alternate_mailboxes != null && alternate_mailboxes.size > 0) { + string[] list = new string[alternate_mailboxes.size]; + for (int ctr = 0; ctr < alternate_mailboxes.size; ctr++) + list[ctr] = alternate_mailboxes[ctr].to_rfc822_string(); + + key_file.set_string_list(GROUP, ALTERNATE_EMAILS_KEY, list); + } if (service_provider == ServiceProvider.OTHER) { key_file.set_value(GROUP, IMAP_HOST, default_imap_server_host); @@ -797,15 +895,17 @@ public class Geary.AccountInformation : BaseObject { /** * Returns a MailboxAddress object for this account. */ - public RFC822.MailboxAddress get_mailbox_address() { + public RFC822.MailboxAddress get_primary_mailbox_address() { return new RFC822.MailboxAddress(real_name, email); } /** - * Returns a MailboxAddresses object with this mailbox address. + * Returns MailboxAddresses with the primary mailbox address. + * + * @see get_primary_mailbox_address */ - public RFC822.MailboxAddresses get_from() { - return new RFC822.MailboxAddresses.single(get_mailbox_address()); + public RFC822.MailboxAddresses get_primary_from() { + return new RFC822.MailboxAddresses.single(get_primary_mailbox_address()); } public static int compare_ascending(AccountInformation a, AccountInformation b) { diff --git a/src/engine/api/geary-composed-email.vala b/src/engine/api/geary-composed-email.vala index 72491207..e4aa3aaa 100644 --- a/src/engine/api/geary-composed-email.vala +++ b/src/engine/api/geary-composed-email.vala @@ -17,6 +17,8 @@ public class Geary.ComposedEmail : BaseObject { | Geary.Email.Field.DATE; public DateTime date { get; set; } + // TODO: sender goes here, but not beyond, as it's not properly supported by GMime yet. + public RFC822.MailboxAddress? sender { get; set; default = null; } public RFC822.MailboxAddresses from { get; set; } public RFC822.MailboxAddresses? to { get; set; default = null; } public RFC822.MailboxAddresses? cc { get; set; default = null; } diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index b7c8d99f..ab7ca0aa 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -634,7 +634,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu if (smtp_err == null) { try { - yield smtp.send_email_async(_account.information.get_mailbox_address(), + yield smtp.send_email_async(_account.information.get_primary_mailbox_address(), rfc822, cancellable); } catch (Error send_err) { debug("SMTP send mail error: %s", send_err.message); diff --git a/src/engine/rfc822/rfc822-mailbox-address.vala b/src/engine/rfc822/rfc822-mailbox-address.vala index 4478cc99..20cb4575 100644 --- a/src/engine/rfc822/rfc822-mailbox-address.vala +++ b/src/engine/rfc822/rfc822-mailbox-address.vala @@ -4,13 +4,47 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData, BaseObject { +/** + * An immutable object containing a representation of an Internet email address. + * + * See [[https://tools.ietf.org/html/rfc2822#section-3.4]] + */ + +public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData, + Gee.Hashable, BaseObject { internal delegate string ListToStringDelegate(MailboxAddress address); + /** + * The optional user-friendly name associated with the {@link MailboxAddress}. + * + * For "Dirk Gently ", this would be "Dirk Gently". + */ public string? name { get; private set; } + + /** + * The routing of the message (optional, obsolete). + */ public string? source_route { get; private set; } + + /** + * The mailbox (local-part) portion of the {@link MailboxAddress}. + * + * For "Dirk Gently ", this would be "dirk". + */ public string mailbox { get; private set; } + + /** + * The domain portion of the {@link MailboxAddress}. + * + * For "Dirk Gently ", this would be "example.com". + */ public string domain { get; private set; } + + /** + * The address specification of the {@link MailboxAddress}. + * + * For "Dirk Gently ", this would be "dirk@example.com". + */ public string address { get; private set; } public MailboxAddress(string? name, string address) { @@ -23,6 +57,9 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa if (atsign > 0) { mailbox = address.slice(0, atsign); domain = address.slice(atsign + 1, address.length); + } else { + mailbox = ""; + domain = ""; } } @@ -115,13 +152,6 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa } } - /** - * Returns a normalized casefolded string of the address, suitable for comparison and hashing. - */ - public string as_key() { - return address.normalize().casefold(); - } - /** * Returns the address suitable for insertion into an RFC822 message. RFC822 quoting is * performed if required. @@ -141,6 +171,17 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa return get_full_address(); } + public uint hash() { + return String.stri_hash(address); + } + + /** + * Equality is defined as a case-insensitive comparison of the {@link address}. + */ + public bool equal_to(MailboxAddress other) { + return this != other ? String.stri_equal(address, other.address) : true; + } + public string to_string() { return get_full_address(); } diff --git a/src/engine/rfc822/rfc822-mailbox-addresses.vala b/src/engine/rfc822/rfc822-mailbox-addresses.vala index d1b99c57..c70f1ff9 100644 --- a/src/engine/rfc822/rfc822-mailbox-addresses.vala +++ b/src/engine/rfc822/rfc822-mailbox-addresses.vala @@ -5,7 +5,7 @@ */ public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageData, - Geary.MessageData.SearchableMessageData, Geary.RFC822.MessageData { + Geary.MessageData.SearchableMessageData, Geary.RFC822.MessageData, Gee.Hashable { public int size { get { return addrs.size; } } @@ -74,11 +74,46 @@ public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageDa return false; } - + /** + * Returns the addresses suitable for insertion into an RFC822 message. RFC822 quoting is + * performed if required. + * + * @see RFC822.to_rfc822_string + */ public string to_rfc822_string() { return MailboxAddress.list_to_string(addrs, "", (a) => a.to_rfc822_string()); } + public uint hash() { + // create sorted set to ensure ordering no matter the list's order + Gee.TreeSet sorted_addresses = traverse(addrs) + .map(m => m.address) + .to_tree_set(String.stri_cmp); + + // xor all strings in sorted order + uint xor = 0; + foreach (string address in sorted_addresses) + xor ^= address.hash(); + + return xor; + } + + public bool equal_to(MailboxAddresses other) { + if (this == other) + return true; + + if (addrs.size != other.addrs.size) + return false; + + Gee.HashSet first = new Gee.HashSet(); + first.add_all(addrs); + + Gee.HashSet second = new Gee.HashSet(); + second.add_all(other.addrs); + + return Collection.are_sets_equal(first, second); + } + /** * See Geary.MessageData.SearchableMessageData. */ diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala index abed06cb..99bf3cb5 100644 --- a/src/engine/rfc822/rfc822-utils.vala +++ b/src/engine/rfc822/rfc822-utils.vala @@ -62,21 +62,29 @@ public string create_subject_for_forward(Geary.Email email) { // address in the list once. Used to remove the sender's address from a list of addresses being // created for the "reply to" recipients. private void remove_address(Gee.List addresses, - string address, bool empty_ok = false) { + RFC822.MailboxAddress address, bool empty_ok = false) { for (int i = 0; i < addresses.size; ++i) { - if (addresses[i].address == address && (empty_ok || addresses.size > 1)) + if (addresses[i].equal_to(address) && (empty_ok || addresses.size > 1)) addresses.remove_at(i--); } } +private bool email_is_from_sender(Geary.Email email, Gee.List? sender_addresses) { + if (sender_addresses == null) + return false; + + return Geary.traverse(sender_addresses) + .any(a => email.from.get_all().contains(a)); +} + public Geary.RFC822.MailboxAddresses create_to_addresses_for_reply(Geary.Email email, - string? sender_address = null) { + Gee.List< Geary.RFC822.MailboxAddress>? sender_addresses = null) { Gee.List new_to = new Gee.ArrayList(); // If we're replying to something we sent, send it to the same people we originally did. // Otherwise, we'll send to the reply-to address or the from address. - if (email.to != null && !String.is_empty(sender_address) && email.from.contains(sender_address)) + if (email.to != null && email_is_from_sender(email, sender_addresses)) new_to.add_all(email.to.get_all()); else if (email.reply_to != null) new_to.add_all(email.reply_to.get_all()); @@ -84,29 +92,32 @@ public Geary.RFC822.MailboxAddresses create_to_addresses_for_reply(Geary.Email e new_to.add_all(email.from.get_all()); // Exclude the current sender. No need to receive the mail they're sending. - if (!String.is_empty(sender_address)) - remove_address(new_to, sender_address); + if (sender_addresses != null) { + foreach (RFC822.MailboxAddress address in sender_addresses) + remove_address(new_to, address); + } return new Geary.RFC822.MailboxAddresses(new_to); } public Geary.RFC822.MailboxAddresses create_cc_addresses_for_reply_all(Geary.Email email, - string? sender_address = null) { + Gee.List? sender_addresses = null) { Gee.List new_cc = new Gee.ArrayList(); // If we're replying to something we received, also add other recipients. Don't do this for // emails we sent, since everyone we sent it to is already covered in // create_to_addresses_for_reply(). - if (email.to != null && (String.is_empty(sender_address) || - !email.from.contains(sender_address))) + if (email.to != null && !email_is_from_sender(email, sender_addresses)) new_cc.add_all(email.to.get_all()); if (email.cc != null) new_cc.add_all(email.cc.get_all()); // Again, exclude the current sender. - if (!String.is_empty(sender_address)) - remove_address(new_cc, sender_address, true); + if (sender_addresses != null) { + foreach (RFC822.MailboxAddress address in sender_addresses) + remove_address(new_cc, address, true); + } return new Geary.RFC822.MailboxAddresses(new_cc); } @@ -135,28 +146,11 @@ public Geary.RFC822.MailboxAddresses remove_addresses(Geary.RFC822.MailboxAddres result.add_all(from_addresses.get_all()); if (remove_addresses != null) foreach (Geary.RFC822.MailboxAddress address in remove_addresses) - remove_address(result, address.address, true); + remove_address(result, address, true); } return new Geary.RFC822.MailboxAddresses(result); } -public bool equal(Geary.RFC822.MailboxAddresses? first, Geary.RFC822.MailboxAddresses? second) { - bool first_empty = first == null || first.size == 0; - bool second_empty = second == null || second.size == 0; - if (first_empty && second_empty || first == second) - return true; - if (first_empty || second_empty || first.size != second.size) - return false; - - Gee.HashSet first_addresses = new Gee.HashSet(); - Gee.HashSet second_addresses = new Gee.HashSet(); - foreach (Geary.RFC822.MailboxAddress a in first) - first_addresses.add(a.as_key()); - foreach (Geary.RFC822.MailboxAddress a in second) - second_addresses.add(a.as_key()); - return Geary.Collection.are_sets_equal(first_addresses, second_addresses); -} - public string reply_references(Geary.Email source) { // generate list for References Gee.ArrayList list = new Gee.ArrayList(); diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala index 468e09a6..3c09bac3 100644 --- a/src/engine/util/util-string.vala +++ b/src/engine/util/util-string.vala @@ -46,6 +46,10 @@ public bool stri_equal(string a, string b) { return str_equal(a.down(), b.down()); } +public int stri_cmp(string a, string b) { + return strcmp(a.down(), b.down()); +} + // Removes redundant spaces, tabs, and newlines. public string reduce_whitespace(string _s) { string s = _s; diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index bb1329f2..c4034830 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -8,6 +8,7 @@ install(FILES app_menu.interface DESTINATION ${UI_DEST}) install(FILES certificate_warning_dialog.glade DESTINATION ${UI_DEST}) install(FILES composer.glade DESTINATION ${UI_DEST}) install(FILES composer_accelerators.ui DESTINATION ${UI_DEST}) +install(FILES edit_alternate_emails.glade DESTINATION ${UI_DEST}) install(FILES find_bar.glade DESTINATION ${UI_DEST}) install(FILES login.glade DESTINATION ${UI_DEST}) install(FILES message.glade DESTINATION ${UI_DEST}) diff --git a/ui/edit_alternate_emails.glade b/ui/edit_alternate_emails.glade new file mode 100644 index 00000000..2e268879 --- /dev/null +++ b/ui/edit_alternate_emails.glade @@ -0,0 +1,196 @@ + + + + + + True + False + vertical + 4 + + + True + False + 8 + (added in code) + + + + + + False + True + 0 + + + + + True + False + 4 + + + True + True + True + True + email + + + False + True + 0 + + + + + True + True + True + True + True + + + True + False + 16 + list-add-symbolic + + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + True + True + in + + + True + False + + + True + False + True + False + + + + + + + False + True + 2 + + + + + True + False + icons + False + 2 + + + True + False + Remove email address + True + list-remove-symbolic + + + False + True + + + + + + False + True + 3 + + + + + True + False + 0 + Some email services require additional addresses be configured on the server. Contact your email provider for more information. + True + + + + + + False + True + 4 + + + + + True + False + end + 8 + False + 6 + True + end + + + _Cancel + True + True + True + True + True + + + False + True + 0 + + + + + _Update + True + True + True + True + True + + + False + True + 1 + + + + + False + True + 5 + + + + diff --git a/ui/login.glade b/ui/login.glade index 1e85ad0c..0f768acc 100644 --- a/ui/login.glade +++ b/ui/login.glade @@ -298,7 +298,18 @@ - + + Addi_tional email addresses… + True + True + True + + + 1 + 7 + 1 + 1 + @@ -842,80 +853,6 @@ 3 - - - False - 10 - vertical - - - True - False - 8 - 0 - 4 - 6 - Storage - - - - - - False - True - 0 - - - - - True - False - - - True - False - 0 - 6 - _Download mail - True - - - - 0 - 0 - 1 - 1 - - - - - True - False - 0 - - - 1 - 0 - 1 - 1 - - - - - False - True - 1 - - - - - False - True - 4 - - False @@ -1003,5 +940,79 @@ 4 + + + False + 10 + vertical + + + True + False + 8 + 0 + 4 + 6 + Storage + + + + + + False + True + 0 + + + + + True + False + + + True + False + 0 + 6 + _Download mail + True + + + + 0 + 0 + 1 + 1 + + + + + True + False + 0 + + + 1 + 0 + 1 + 1 + + + + + False + True + 1 + + + + + False + True + 4 + +