[geary] Support multiple ("alternate") email addresses per acct: Bug #714922



commit 12704542b340245a9bacc89ceea9bc17313bdfc4
Author: Charles Lindsay <chaz yorba org>
Date:   Wed Mar 4 17:26:44 2015 -0800

    Support multiple ("alternate") email addresses per acct: Bug #714922
    
    If the server supports sending and receiving messages from additional
    email addresses, those addresses can now be added to Geary.  It will
    allow the user to send new messages and replies from those additional
    addresses and recognizes them as "Me" in the conversation list.

 po/POTFILES.in                                     |    2 +
 src/CMakeLists.txt                                 |    1 +
 .../accounts/account-dialog-add-edit-pane.vala     |    5 +-
 .../account-dialog-edit-alternate-emails-pane.vala |  205 ++++++++++++++++++++
 src/client/accounts/account-dialog.vala            |   17 ++
 src/client/accounts/add-edit-page.vala             |   11 +
 src/client/composer/composer-widget.vala           |  175 ++++++++++++-----
 .../conversation-list/conversation-list-store.vala |    3 +-
 .../formatted-conversation-data.vala               |   37 ++--
 src/engine/api/geary-account-information.vala      |  108 ++++++++++-
 src/engine/api/geary-composed-email.vala           |    2 +
 src/engine/imap-db/outbox/smtp-outbox-folder.vala  |    2 +-
 src/engine/rfc822/rfc822-mailbox-address.vala      |   57 +++++-
 src/engine/rfc822/rfc822-mailbox-addresses.vala    |   39 ++++-
 src/engine/rfc822/rfc822-utils.vala                |   52 +++---
 src/engine/util/util-string.vala                   |    4 +
 ui/CMakeLists.txt                                  |    1 +
 ui/edit_alternate_emails.glade                     |  196 +++++++++++++++++++
 ui/login.glade                                     |  161 ++++++++-------
 19 files changed, 884 insertions(+), 194 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index be3c3fe..8ab4fc1 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 102917e..d94fcb7 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 398c9eb..65d30ed 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 0000000..e621ee5
--- /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 = "<b>%s</b>".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<Geary.RFC822.MailboxAddress> mailboxes = new 
Gee.HashSet<Geary.RFC822.MailboxAddress>();
+    
+    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 1d42763..03c6036 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 80ec59e..226a990 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 4c4bff3..e34fa53 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<FromAddressMap> from_list = new Gee.ArrayList<FromAddressMap>();
     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<Geary.RFC822.MailboxAddress> 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<Geary.RFC822.MailboxAddress> 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<Geary.RFC822.MailboxAddress> 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<string, Geary.AccountInformation> 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<Geary.AccountInformation>(
+            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<FromAddressMap>();
+        
+        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 7d83136..eca2ced 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 a01e26f..4cd56d2 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<ParticipantDisplay> {
-        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<Geary.RFC822.MailboxAddress> 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<Geary.RFC822.MailboxAddress> 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<Geary.RFC822.MailboxAddress>? 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<Geary.RFC822.MailboxAddress> 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<ParticipantDisplay> list = new Gee.ArrayList<ParticipantDisplay>();
@@ -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 7f9f49a..2623cb0 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<Geary.RFC822.MailboxAddress>? 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<string> 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;
@@ -259,6 +308,48 @@ public class Geary.AccountInformation : BaseObject {
     }
     
     /**
+     * Return a list of the primary and all alternate email addresses.
+     */
+    public Gee.List<Geary.RFC822.MailboxAddress> get_all_mailboxes() {
+        Gee.ArrayList<RFC822.MailboxAddress> all = new Gee.ArrayList<RFC822.MailboxAddress>();
+        
+        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<RFC822.MailboxAddress>();
+        
+        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<Geary.RFC822.MailboxAddress>? 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
      * ignored.
@@ -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 7249120..e4aa3aa 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 b7c8d99..ab7ca0a 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 4478cc9..20cb457 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<MailboxAddress>, BaseObject {
     internal delegate string ListToStringDelegate(MailboxAddress address);
     
+    /**
+     * The optional user-friendly name associated with the { link MailboxAddress}.
+     *
+     * For "Dirk Gently <dirk example com>", 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 <dirk example com>", this would be "dirk".
+     */
     public string mailbox { get; private set; }
+    
+    /**
+     * The domain portion of the { link MailboxAddress}.
+     *
+     * For "Dirk Gently <dirk example com>", this would be "example.com".
+     */
     public string domain { get; private set; }
+    
+    /**
+     * The address specification of the { link MailboxAddress}.
+     *
+     * For "Dirk Gently <dirk example com>", 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 = "";
         }
     }
     
@@ -116,13 +153,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 d1b99c5..c70f1ff 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<MailboxAddresses> {
     
     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<string> sorted_addresses = traverse<RFC822.MailboxAddress>(addrs)
+            .map<string>(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<RFC822.MailboxAddress> first = new Gee.HashSet<RFC822.MailboxAddress>();
+        first.add_all(addrs);
+        
+        Gee.HashSet<RFC822.MailboxAddress> second = new Gee.HashSet<RFC822.MailboxAddress>();
+        second.add_all(other.addrs);
+        
+        return Collection.are_sets_equal<RFC822.MailboxAddress>(first, second);
+    }
+    
     /**
      * See Geary.MessageData.SearchableMessageData.
      */
diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala
index abed06c..99bf3cb 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<Geary.RFC822.MailboxAddress> 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<RFC822.MailboxAddress>? sender_addresses) {
+    if (sender_addresses == null)
+        return false;
+    
+    return Geary.traverse<RFC822.MailboxAddress>(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<Geary.RFC822.MailboxAddress> new_to =
         new Gee.ArrayList<Geary.RFC822.MailboxAddress>();
     
     // 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<Geary.RFC822.MailboxAddress>? sender_addresses = null) {
     Gee.List<Geary.RFC822.MailboxAddress> new_cc = new Gee.ArrayList<Geary.RFC822.MailboxAddress>();
     
     // 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<string> first_addresses = new Gee.HashSet<string>();
-    Gee.HashSet<string> second_addresses = new Gee.HashSet<string>();
-    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<string>(first_addresses, second_addresses);
-}
-
 public string reply_references(Geary.Email source) {
     // generate list for References
     Gee.ArrayList<RFC822.MessageID> list = new Gee.ArrayList<RFC822.MessageID>();
diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala
index 468e09a..3c09bac 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 bb1329f..c403483 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 0000000..2e26887
--- /dev/null
+++ b/ui/edit_alternate_emails.glade
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <object class="GtkBox" id="container">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">4</property>
+    <child>
+      <object class="GtkLabel" id="title_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">8</property>
+        <property name="label">(added in code)</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="box1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">4</property>
+        <child>
+          <object class="GtkEntry" id="email_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="activates_default">True</property>
+            <property name="input_purpose">email</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="add_button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <child>
+              <object class="GtkImage" id="image1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="pixel_size">16</property>
+                <property name="icon_name">list-add-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolledwindow1">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="vexpand">True</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkViewport" id="viewport1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkListBox" id="address_listbox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="vexpand">True</property>
+                <property name="activate_on_single_click">False</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToolbar" id="toolbar1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="toolbar_style">icons</property>
+        <property name="show_arrow">False</property>
+        <property name="icon_size">2</property>
+        <child>
+          <object class="GtkToolButton" id="delete_button">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="tooltip_text" translatable="yes">Remove email address</property>
+            <property name="use_underline">True</property>
+            <property name="icon_name">list-remove-symbolic</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="homogeneous">True</property>
+          </packing>
+        </child>
+        <style>
+          <class name="inline-toolbar"/>
+        </style>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Some email services require additional addresses be 
configured on the server.  Contact your email provider for more information.</property>
+        <property name="wrap">True</property>
+        <attributes>
+          <attribute name="style" value="italic"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButtonBox" id="buttonbox1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">end</property>
+        <property name="margin_top">8</property>
+        <property name="vexpand">False</property>
+        <property name="spacing">6</property>
+        <property name="homogeneous">True</property>
+        <property name="layout_style">end</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="update_button">
+            <property name="label" translatable="yes">_Update</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">5</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/ui/login.glade b/ui/login.glade
index 1e85ad0..0f768ac 100644
--- a/ui/login.glade
+++ b/ui/login.glade
@@ -298,7 +298,18 @@
           </packing>
         </child>
         <child>
-          <placeholder/>
+          <object class="GtkButton" id="button: edit_alternate_email">
+            <property name="label" translatable="yes">Addi_tional email addresses…</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">7</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
         </child>
         <child>
           <placeholder/>
@@ -843,80 +854,6 @@
       </packing>
     </child>
     <child>
-      <object class="GtkBox" id="storage container">
-        <property name="can_focus">False</property>
-        <property name="margin_bottom">10</property>
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkLabel" id="label: storage">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="margin_top">8</property>
-            <property name="xalign">0</property>
-            <property name="xpad">4</property>
-            <property name="ypad">6</property>
-            <property name="label" translatable="yes">Storage</property>
-            <attributes>
-              <attribute name="weight" value="bold"/>
-            </attributes>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">0</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkGrid" id="grid3">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <child>
-              <object class="GtkLabel" id="label: sync">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="xalign">0</property>
-                <property name="xpad">6</property>
-                <property name="label" translatable="yes">_Download mail</property>
-                <property name="use_underline">True</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">0</property>
-                <property name="width">1</property>
-                <property name="height">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkComboBoxText" id="combo: storage">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="active">0</property>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">0</property>
-                <property name="width">1</property>
-                <property name="height">1</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
-      </object>
-      <packing>
-        <property name="expand">False</property>
-        <property name="fill">True</property>
-        <property name="position">4</property>
-      </packing>
-    </child>
-    <child>
       <object class="GtkBox" id="composer container">
         <property name="can_focus">False</property>
         <property name="margin_bottom">10</property>
@@ -1003,5 +940,79 @@
         <property name="position">4</property>
       </packing>
     </child>
+    <child>
+      <object class="GtkBox" id="storage container">
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">10</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkLabel" id="label: storage">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_top">8</property>
+            <property name="xalign">0</property>
+            <property name="xpad">4</property>
+            <property name="ypad">6</property>
+            <property name="label" translatable="yes">Storage</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkGrid" id="grid3">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkLabel" id="label: sync">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="xpad">6</property>
+                <property name="label" translatable="yes">_Download mail</property>
+                <property name="use_underline">True</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkComboBoxText" id="combo: storage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="active">0</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
   </object>
 </interface>


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