Bug 1764652 - Replace XUL mail-multi-emailheaderfield, mail-headerfield, and mail-newsgroup with HTML custom elements. r=mkmelin,henry
authorAlessandro Castellani <alessandro@thunderbird.net>
Wed, 18 May 2022 22:29:43 +0000
changeset 35765 f4fb3a75992a9417c563953deed3783a0bf4cd1b
parent 35764 90328ce5bee2f13de45bdb402054e0288abf7c05
child 35766 279167f85f2f59526b2599ab02c2c6afc55a10a1
push id19934
push useralessandro@thunderbird.net
push dateWed, 18 May 2022 22:31:52 +0000
treeherdercomm-central@9ac8cbc657e1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin, henry
bugs1764652
Bug 1764652 - Replace XUL mail-multi-emailheaderfield, mail-headerfield, and mail-newsgroup with HTML custom elements. r=mkmelin,henry Convert the multi header recipients, newsgroup, and simple header row into HTML custom elements. Simplify the handling of node population and update of recipients. Defer the listening for address book changes to the multi recipients custom element. Differential Revision: https://phabricator.services.mozilla.com/D144358
mail/base/content/aboutMessage.xhtml
mail/base/content/editContactPanel.inc.xhtml
mail/base/content/editContactPanel.js
mail/base/content/mainPopupSet.inc.xhtml
mail/base/content/messageWindow.xhtml
mail/base/content/messenger.xhtml
mail/base/content/msgHdrPopup.inc.xhtml
mail/base/content/msgHdrView.inc.xhtml
mail/base/content/msgHdrView.js
mail/base/content/widgets/header-fields.js
mail/base/content/widgets/mailWidgets.js
mail/base/jar.mn
mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
mail/extensions/smime/content/msgHdrViewSMIMEOverlay.js
mail/locales/en-US/messenger/messageheader/headerFields.ftl
mail/locales/en-US/messenger/messenger.ftl
mail/test/browser/message-header/browser_messageHeader.js
mail/test/browser/multiple-identities/browser_displayNames.js
mail/themes/shared/jar.inc.mn
mail/themes/shared/mail/icons/new/not-in-address-book.svg
mail/themes/shared/mail/messageHeader.css
mail/themes/shared/mail/popupPanel.css
--- a/mail/base/content/aboutMessage.xhtml
+++ b/mail/base/content/aboutMessage.xhtml
@@ -41,23 +41,25 @@
   <link rel="stylesheet" href="chrome://messenger/skin/contextMenu.css" />
 
   <link rel="localization" href="messenger/messenger.ftl" />
   <link rel="localization" href="toolkit/main-window/findbar.ftl" />
   <link rel="localization" href="toolkit/global/textActions.ftl" />
   <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
   <link rel="localization" href="messenger/openpgp/openpgp-frontend.ftl" />
   <link rel="localization" href="messenger/openpgp/msgReadStatus.ftl" />
+  <link rel="localization" href="messenger/messageheader/headerFields.ftl" />
 
   <script defer="defer" src="chrome://global/content/globalOverlay.js"></script>
   <script defer="defer" src="chrome://global/content/contentAreaUtils.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailContextMenus.js"></script>
   <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgViewNavigation.js"></script>
   <script defer="defer" src="chrome://messenger/content/editContactPanel.js"></script>
+  <script defer="defer" src="chrome://messenger/content/header-fields.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgHdrView.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMessengerOverlay.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMsgHdrViewOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgSecurityPane.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailWindowOverlay.js"></script>
   <script defer="defer" src="chrome://messenger-newsblog/content/newsblogOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailCore.js"></script>
--- a/mail/base/content/editContactPanel.inc.xhtml
+++ b/mail/base/content/editContactPanel.inc.xhtml
@@ -10,17 +10,17 @@
         hidden="true"
         aria-labelledby="editContactPanelTitle"
         onpopuphidden="editContactInlineUI.onPopupHidden(event);"
         onpopupshown="editContactInlineUI.onPopupShown(event);"
         onkeypress="editContactInlineUI.onKeyPress(event, true);">
   <html:div class="popup-panel-body">
     <html:div id="editContactHeader">
       <html:img id="editContactPanelIcon"
-                src="chrome://messenger/skin/icons/starred.svg"
+                src="chrome://messenger/skin/icons/new/normal/address-book.svg"
                 alt="" />
       <html:h3 id="editContactPanelTitle" flex="1"></html:h3>
     </html:div>
 
     <box id="editContactContent">
       <hbox pack="end">
         <label value="&editContactName.label;"
                class="editContactPanel_rowLabel"
--- a/mail/base/content/editContactPanel.js
+++ b/mail/base/content/editContactPanel.js
@@ -133,19 +133,19 @@ var editContactInlineUI = {
       nameElement.class = "editContactTextbox";
     } else {
       nameElement.setAttribute("readonly", "readonly");
       nameElement.class = "plain";
     }
 
     // Fill in the card details
     nameElement.value = this._cardDetails.card.displayName;
-    document.getElementById(
-      "editContactEmail"
-    ).value = aAnchorElement.getAttribute("emailAddress");
+    document.getElementById("editContactEmail").value =
+      aAnchorElement.getAttribute("emailAddress") ||
+      aAnchorElement.emailAddress;
 
     document.getElementById(
       "editContactAddressBookList"
     ).value = this._cardDetails.book.URI;
 
     // Is this card contained within mailing lists?
     let inMailList = false;
     if (this._cardDetails.book.supportsMailingLists) {
--- a/mail/base/content/mainPopupSet.inc.xhtml
+++ b/mail/base/content/mainPopupSet.inc.xhtml
@@ -1,61 +1,100 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #ifndef BROWSER_POPUPS_ONLY
-  <menupopup id="emailAddressPopup" position="after_start" class="emailAddressPopup"
-             onpopupshowing="setupEmailAddressPopup(event.target.triggerNode); goUpdateCommand('cmd_createFilterFromPopup')"
-             onpopuphiding="hideEmailNewsPopup(event.target.triggerNode);">
+  <menupopup id="emailAddressPopup"
+             position="after_start"
+             class="no-icon-menupopup"
+             popupshowing="goUpdateCommand('cmd_createFilterFromPopup');">
     <menuitem id="emailAddressPlaceHolder"
+              class="menuitem-iconic"
               disabled="true"/>
     <menuseparator/>
     <menuitem id="addToAddressBookItem"
               label="&AddDirectlyToAddressBook.label;"
               accesskey="&AddDirectlyToAddressBook.accesskey;"
-              oncommand="AddContact(this.parentNode.triggerNode)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.addContact(event);"/>
     <menuitem id="editContactItem" label="&EditContact1.label;" hidden="true"
               accesskey="&EditContact1.accesskey;"
-              oncommand="EditContact(this.parentNode.triggerNode)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.showContactEdit(event);"/>
     <menuitem id="viewContactItem" label="&ViewContact.label;" hidden="true"
               accesskey="&ViewContact.accesskey;"
-              oncommand="EditContact(this.parentNode.triggerNode)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.showContactEdit(event);"/>
     <menuitem id="sendMailToItem" label="&SendMessageTo.label;"
               accesskey="&SendMessageTo.accesskey;"
-              oncommand="SendMailToNode(this.parentNode.triggerNode, event)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.composeMessage(event);"/>
     <menuitem id="copyEmailAddressItem" label="&CopyEmailAddress.label;"
               accesskey="&CopyEmailAddress.accesskey;"
-              oncommand="CopyEmailNewsAddress(this.parentNode.triggerNode)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.copyAddress(event)"/>
     <menuitem id="copyNameAndEmailAddressItem" label="&CopyNameAndEmailAddress.label;"
               accesskey="&CopyNameAndEmailAddress.accesskey;"
-              oncommand="CopyEmailNewsAddress(this.parentNode.triggerNode, true)"/>
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.copyAddress(event, true);"/>
     <menuseparator class="openpgp-item"/>
     <menuitem id="searchKeysOpenPGP" data-l10n-id="openpgp-search-keys-openpgp"
-              class="openpgp-item"
-              oncommand="Enigmail.msg.searchKeysOnInternet(this.parentNode.triggerNode)"/>
+              class="openpgp-item menuitem-iconic"
+              oncommand="Enigmail.msg.searchKeysOnInternet(event)"/>
     <menuseparator/>
     <menuitem id="createFilterFromItem" label="&CreateFilterFrom.label;"
               accesskey="&CreateFilterFrom.accesskey;"
-              oncommand="CreateFilter(this.parentNode.triggerNode, gMessageDisplay.displayedMessage)"
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.createFilter(event);"
+              observes="cmd_createFilterFromPopup"/>
+  </menupopup>
+
+  <menupopup id="copyPopup" class="no-icon-menupopup">
+    <menuitem id="copyMenuitem"
+              data-l10n-id="text-action-copy"
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.copyString(event);"/>
+    <menuitem id="createFilterFromMenuItem"
+              label="&CreateFilterFrom.label;"
+              accesskey="&CreateFilterFrom.accesskey;"
+              class="menuitem-iconic"
+              oncommand="gMessageHeader.createFilter(event);"
               observes="cmd_createFilterFromPopup"/>
   </menupopup>
 
-  <menupopup id="copyPopup">
-    <menuitem id="copyMenuitem"
-              data-l10n-id="text-action-copy"
-              oncommand="Cc['@mozilla.org/widget/clipboardhelper;1']
-                           .getService(Ci.nsIClipboardHelper)
-                           .copyString(window.getSelection().isCollapsed ?
-                             this.parentNode.triggerNode.textContent :
-                             window.getSelection().toString());"/>
-    <menuitem id="createFilterFromMenuItem" label="&CreateFilterFrom.label;"
-              accesskey="&CreateFilterFrom.accesskey;"
-              oncommand="CreateFilter(this.parentNode.triggerNode, gMessageDisplay.displayedMessage)"
-              observes="cmd_createFilterFromPopup"/>
+  <menupopup id="newsgroupPopup"
+             position="after_start"
+             class="newsgroupPopup no-icon-menupopup"
+             onpopupshowing="goUpdateCommand('cmd_createFilterFromPopup');">
+      <menuitem id="newsgroupPlaceHolder"
+                class="menuitem-iconic"
+                disabled="true"/>
+      <menuseparator/>
+      <menuitem id="sendMessageToNewsgroupItem"
+                label="&SendMessageTo.label;"
+                accesskey="&SendMessageTo.accesskey;"
+                class="menuitem-iconic"
+                oncommand="gMessageHeader.composeMessage(event);"/>
+      <menuitem id="copyNewsgroupNameItem"
+                label="&CopyNewsgroupName.label;"
+                accesskey="&CopyNewsgroupName.accesskey;"
+                class="menuitem-iconic"
+                oncommand="gMessageHeader.copyAddress(event);"/>
+      <menuitem id="copyNewsgroupURLItem"
+                label="&CopyNewsgroupURL.label;"
+                accesskey="&CopyNewsgroupURL.accesskey;"
+                class="menuitem-iconic"
+                oncommand="gMessageHeader.copyNewsgroupURL(event);"/>
+      <menuseparator id="subscribeToNewsgroupSeparator"/>
+      <menuitem id="subscribeToNewsgroupItem"
+                label="&SubscribeToNewsgroup.label;"
+                accesskey="&SubscribeToNewsgroup.accesskey;"
+                class="menuitem-iconic"
+                oncommand="gMessageHeader.subscribeToNewsgroup(event)"/>
   </menupopup>
 #endif
 
 #ifndef NO_MAILCONTEXT
   <!-- "Please keep all items and separators up to date in nsContextMenu.js when making changes here" -->
   <menupopup id="mailContext"
              onpopupshowing="return fillMailContextMenu(event);"
              onpopuphiding="mailContextOnPopupHiding(event);">
--- a/mail/base/content/messageWindow.xhtml
+++ b/mail/base/content/messageWindow.xhtml
@@ -84,31 +84,33 @@
   <link rel="localization" href="toolkit/global/textActions.ftl" />
   <link rel="localization" href="toolkit/printing/printUI.ftl" />
   <link rel="localization" href="messenger/menubar.ftl" />
   <link rel="localization" href="messenger/appmenu.ftl" />
   <link rel="localization" href="calendar/calendar-invitation-panel.ftl" />
   <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
   <link rel="localization" href="messenger/openpgp/openpgp-frontend.ftl" />
   <link rel="localization" href="messenger/openpgp/msgReadStatus.ftl" />
+  <link rel="localization" href="messenger/messageheader/headerFields.ftl" />
 
   <script defer="defer" src="chrome://global/content/globalOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/commandglue.js"></script>
   <script defer="defer" src="chrome://messenger/content/folderDisplay.js"></script>
   <script defer="defer" src="chrome://messenger/content/messageDisplay.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailWindow.js"></script>
   <script defer="defer" src="chrome://messenger/content/messageWindow.js"></script>
   <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script>
   <script defer="defer" src="chrome://global/content/contentAreaUtils.js"></script>
   <script defer="defer" src="chrome://messenger/content/nsContextMenu.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailContextMenus.js"></script>
   <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgViewNavigation.js"></script>
   <script defer="defer" src="chrome://messenger/content/editContactPanel.js"></script>
   <script defer="defer" src="chrome://messenger/content/toolbarIconColor.js"></script>
+  <script defer="defer" src="chrome://messenger/content/header-fields.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgHdrView.js"></script>
   <script defer="defer" src="chrome://messenger-smime/content/msgHdrViewSMIMEOverlay.js"></script>
   <script defer="defer" src="chrome://messenger-smime/content/msgReadSMIMEOverlay.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMessengerOverlay.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMsgHdrViewOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgSecurityPane.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
   <script defer="defer" src="chrome://messenger/content/junkCommands.js"></script>
--- a/mail/base/content/messenger.xhtml
+++ b/mail/base/content/messenger.xhtml
@@ -84,16 +84,17 @@
   <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
   <link rel="localization" href="messenger/openpgp/openpgp-frontend.ftl" />
   <link rel="localization" href="messenger/openpgp/msgReadStatus.ftl"/>
   <link rel="localization" href="calendar/calendar-widgets.ftl" />
   <link rel="localization" href="calendar/calendar-context-menus.ftl" />
   <link rel="localization" href="calendar/calendar-editable-item.ftl" />
   <link rel="localization" href="calendar/calendar-invitation-panel.ftl" />
   <link rel="localization" href="messenger/chat.ftl" />
+  <link rel="localization" href="messenger/messageheader/headerFields.ftl" />
 
   <title>&titledefault.label;@PRE_RELEASE_SUFFIX@</title>
 
   <script defer="defer" src="chrome://global/content/globalOverlay.js"></script>
   <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/commandglue.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgViewNavigation.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailWindow.js"></script>
@@ -110,16 +111,17 @@
   <script defer="defer" src="chrome://messenger/content/nsContextMenu.js"></script>
   <script defer="defer" src="chrome://messenger/content/mailContextMenus.js"></script>
   <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script>
   <script defer="defer" src="chrome://messenger/content/folderPane.js"></script>
   <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script>
   <script defer="defer" src="chrome://messenger/content/editContactPanel.js"></script>
   <script defer="defer" src="chrome://messenger/content/toolbarIconColor.js"></script>
   <script defer="defer" src="chrome://messenger/content/jsTreeView.js"></script>
+  <script defer="defer" src="chrome://messenger/content/header-fields.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgHdrView.js"></script>
   <script defer="defer" src="chrome://messenger-smime/content/msgHdrViewSMIMEOverlay.js"></script>
   <script defer="defer" src="chrome://messenger-smime/content/msgReadSMIMEOverlay.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMessengerOverlay.js"></script>
   <script defer="defer" src="chrome://openpgp/content/ui/enigmailMsgHdrViewOverlay.js"></script>
   <script defer="defer" src="chrome://messenger/content/msgSecurityPane.js"></script>
   <script defer="defer" src="chrome://messenger/content/chat/chat-messenger.js"></script>
   <script defer="defer" src="chrome://messenger/content/chat/imStatusSelector.js"></script>
@@ -339,37 +341,16 @@
 
 #include msgHdrPopup.inc.xhtml
   <panel is="glodacomplete-rich-result-popup"
          id="PopupGlodaAutocomplete"
          noautofocus="true"/>
 
   <tooltip id="attachmentListTooltip"/>
 
-  <menupopup id="newsgroupPopup" position="after_start" class="newsgroupPopup"
-             onpopupshowing="setupNewsgroupPopup(event.target.triggerNode); goUpdateCommand('cmd_createFilterFromPopup')"
-             onpopuphiding="hideEmailNewsPopup(event.target.triggerNode);">
-      <menuitem id="newsgroupPlaceHolder"
-                disabled="true"/>
-      <menuseparator/>
-      <menuitem id="sendMessageToNewsgroupItem" label="&SendMessageTo.label;"
-                accesskey="&SendMessageTo.accesskey;"
-                oncommand="SendMailToNode(this.parentNode.triggerNode, event)"/>
-      <menuitem id="copyNewsgroupNameItem" label="&CopyNewsgroupName.label;"
-                accesskey="&CopyNewsgroupName.accesskey;"
-                oncommand="CopyEmailNewsAddress(this.parentNode.triggerNode)"/>
-      <menuitem id="copyNewsgroupURLItem" label="&CopyNewsgroupURL.label;"
-                accesskey="&CopyNewsgroupURL.accesskey;"
-                oncommand="CopyNewsgroupURL(this.parentNode.triggerNode)"/>
-      <menuseparator id="subscribeToNewsgroupSeparator"/>
-      <menuitem id="subscribeToNewsgroupItem" label="&SubscribeToNewsgroup.label;"
-                accesskey="&SubscribeToNewsgroup.accesskey;"
-                oncommand="SubscribeToNewsgroup(this.parentNode.triggerNode)"/>
-  </menupopup>
-
   <menupopup id="pageContextMenu"
              onpopupshowing="InitPageMenu(this, event);">
   </menupopup>
 
   <!-- We want to be able to do the following:
 
        1)  Open the tabContextMenu by right-clicking on individual tab selectors
        2)  Open the mail-toolbox customize context menu when right-clicking on
--- a/mail/base/content/msgHdrPopup.inc.xhtml
+++ b/mail/base/content/msgHdrPopup.inc.xhtml
@@ -5,23 +5,21 @@
   <menupopup id="messageIdContext" popupanchor="bottomleft"
              onpopupshowing="FillMessageIdContextMenu(event.target.triggerNode);">
     <menuitem id="messageIdContext-messageIdTarget"
               disabled="true"/>
     <menuseparator id="messageIdContext-separator"/>
     <menuitem id="messageIdContext-openMessageForMsgId"
               label="&OpenMessageForMsgId.label;"
               accesskey="&OpenMessageForMsgId.accesskey;"
-              oncommand="var messageId = GetMessageIdFromNode(this.parentNode.triggerNode, true);
-                         OpenMessageForMessageId(messageId)"/>
+              oncommand="var messageId = GetMessageIdFromNode(this.parentNode.triggerNode, true); OpenMessageForMessageId(messageId)"/>
     <menuitem id="messageIdContext-openBrowserWithMsgId"
               label="&OpenBrowserWithMsgId.label;"
               accesskey="&OpenBrowserWithMsgId.accesskey;"
-              oncommand="var messageId = GetMessageIdFromNode(this.parentNode.triggerNode, true);
-                         OpenBrowserWithMessageId(messageId)"/>
+              oncommand="var messageId = GetMessageIdFromNode(this.parentNode.triggerNode, true); OpenBrowserWithMessageId(messageId)"/>
     <menuitem id="messageIdContext-copyMessageId"
               label="&CopyMessageId.label;"
               accesskey="&CopyMessageId.accesskey;"
               oncommand="var messageId = GetMessageIdFromNode(this.parentNode.triggerNode, false);
                          CopyMessageId(messageId);"/>
   </menupopup>
 
   <menupopup id="attachmentItemContext"
--- a/mail/base/content/msgHdrView.inc.xhtml
+++ b/mail/base/content/msgHdrView.inc.xhtml
@@ -9,17 +9,17 @@
             class="customize-context-manageExtension"/>
   <menuitem oncommand="ToolbarContextMenu.removeExtensionForContextAction(this.parentElement)"
             data-l10n-id="toolbar-context-menu-remove-extension"
             class="customize-context-removeExtension"/>
 </menupopup>
 
 <!-- Header container -->
 <html:header id="messageHeader" class="message-header-container">
-  <!-- Sender + buttons -->
+  <!-- From + buttons -->
   <html:div id="headerSenderToolbarContainer"
             class="message-header-row header-row-reverse message-header-wrap items-center">
     <html:div id="header-view-toolbox" role="toolbar" class="header-buttons-container">
       <!-- NOTE: Temporarily keep the hbox to allow extensions to add custom sets of
         toolbar buttons. This can be later removed once a new API that doesn't rely
         on XUL currentset is in place. -->
       <hbox id="header-view-toolbar" class="header-buttons-container themeable-brighttext">
       <toolbarbutton id="hdrReplyToSenderButton" label="&hdrReplyButton1.label;"
@@ -263,87 +263,98 @@
       </html:button>
       </hbox>
     </html:div>
 
     <html:div id="expandedfromRow" class="header-row-grow">
       <label id="expandedfromLabel" class="message-header-label header-pill-label"
              value="&fromField4.label;"
              valueFrom="&fromField4.label;" valueAuthor="&author.label;"/>
-      <mail-multi-emailheaderfield id="expandedfromBox"/>
+      <html:div id="expandedfromBox" is="multi-recipient-row"
+                data-header-name="from" data-show-all="true"></html:div>
     </html:div>
   </html:div>
 
   <!-- To recipients + date -->
   <html:div id="expandedtoRow" class="message-header-row">
     <html:div class="header-row-grow">
       <label id="expandedtoLabel" class="message-header-label header-pill-label"
              value="&toField4.label;"/>
-      <mail-multi-emailheaderfield id="expandedtoBox"/>
+      <html:div id="expandedtoBox" is="multi-recipient-row"
+                data-header-name="to"></html:div>
     </html:div>
     <html:time id="dateLabel"
                class="message-header-datetime"
                aria-readonly="true"></html:time>
   </html:div>
 
   <!-- Cc recipients -->
   <html:div id="expandedccRow" class="message-header-row" hidden="hidden">
     <label id="expandedccLabel" class="message-header-label header-pill-label"
            value="&ccField4.label;"/>
-    <mail-multi-emailheaderfield id="expandedccBox"/>
+    <html:div id="expandedccBox" is="multi-recipient-row"
+              data-header-name="cc"></html:div>
   </html:div>
 
   <!-- Bcc recipients -->
   <html:div id="expandedbccRow" class="message-header-row" hidden="hidden">
     <label id="expandedbccLabel" class="message-header-label header-pill-label"
            value="&bccField4.label;"/>
-    <mail-multi-emailheaderfield id="expandedbccBox"/>
+    <html:div id="expandedbccBox" is="multi-recipient-row"
+              data-header-name="bcc"></html:div>
   </html:div>
 
   <!-- Reply-to -->
   <html:div id="expandedreply-toRow" class="message-header-row" hidden="hidden">
     <label id="expandedreply-toLabel" class="message-header-label header-pill-label"
            value="&replyToField4.label;"/>
-    <mail-multi-emailheaderfield id="expandedreply-toBox"/>
+    <html:div id="expandedreply-toBox" is="multi-recipient-row"
+              data-header-name="reply-to"></html:div>
   </html:div>
 
   <!-- Organization -->
   <html:div id="expandedorganizationRow" class="message-header-row" hidden="hidden">
     <label id="expandedorganizationLabel" class="message-header-label"
            value="&organizationField4.label;"/>
-    <mail-headerfield id="expandedorganizationBox"/>
+    <html:div id="expandedorganizationBox" is="simple-header-row"
+              data-header-name="organization"></html:div>
   </html:div>
 
   <!-- Sender -->
   <html:div id="expandedsenderRow" class="message-header-row" hidden="hidden">
     <label id="expandedsenderLabel" class="message-header-label header-pill-label"
            value="&senderField4.label;"/>
-    <mail-emailheaderfield id="expandedsenderBox"/>
+    <html:div id="expandedsenderBox" is="multi-recipient-row"
+              data-header-name="sender"></html:div>
   </html:div>
 
   <!-- Newsgroups -->
   <html:div id="expandednewsgroupsRow" class="message-header-row" hidden="hidden">
     <label id="expandednewsgroupsLabel" class="message-header-label header-pill-label"
            value="&newsgroupsField4.label;"/>
-    <mail-newsgroups-headerfield id="expandednewsgroupsBox"/>
+    <html:div id="expandednewsgroupsBox" is="header-newsgroups-row"
+              data-header-name="newsgroups"/>
   </html:div>
 
   <!-- Follow up -->
   <html:div id="expandedfollowup-toRow" class="message-header-row" hidden="hidden">
     <label id="expandedfollowup-toLabel" class="message-header-label header-pill-label"
            value="&followupToField4.label;"/>
-    <mail-newsgroups-headerfield id="expandedfollowup-toBox"/>
+    <html:div id="expandedfollowup-toBox"
+              is="header-newsgroups-row"
+              data-header-name="followup-to"/>
   </html:div>
 
   <!-- Subject + security info + extra date label for hidden To variation -->
   <html:div id="headerSubjectSecurityContainer" class="message-header-row">
      <html:div id="expandedsubjectRow" class="header-row-grow">
       <label id="expandedsubjectLabel" class="message-header-label"
              value="&subjectField4.label;"/>
-      <mail-headerfield id="expandedsubjectBox" headerName="subject"/>
+      <html:div id="expandedsubjectBox" is="simple-header-row"
+                data-header-name="subject"></html:div>
     </html:div>
     <html:div id="cryptoBox" hidden="hidden">
       <html:button id="encryptionTechBtn"
                    class="toolbarbutton-1 crypto-button themeable-brighttext button-focusable"
                    data-l10n-id="message-security-button">
         <html:span class="crypto-label"></html:span>
         <html:img id="encryptedHdrIcon" hidden="hidden" alt="" />
         <html:img id="signedHdrIcon" hidden="hidden" alt="" />
@@ -361,17 +372,18 @@
            value="&tagsHdr4.label;"/>
     <mail-tagfield id="expandedtagsBox"/>
   </html:div>
 
   <!-- Date -->
   <html:div id="expandeddateRow" class="message-header-row" hidden="hidden">
     <label id="expandeddateLabel" class="message-header-label"
            value="&dateField4.label;"/>
-    <mail-headerfield id="expandeddateBox"/>
+    <html:div id="expandeddateBox" is="simple-header-row"
+              data-header-name="date"></html:div>
   </html:div>
 
   <!-- Message ID -->
   <html:div id="expandedmessage-idRow" class="message-header-row" hidden="hidden">
     <label id="expandedmessage-idLabel" class="message-header-label"
            value="&messageIdField4.label;"/>
     <mail-messageids-headerfield id="expandedmessage-idBox"/>
   </html:div>
@@ -396,17 +408,18 @@
            value="&originalWebsite4.label;"/>
     <mail-urlfield id="expandedcontent-baseBox"/>
   </html:div>
 
   <!-- User agent -->
   <html:div id="expandeduser-agentRow" class="message-header-row" hidden="hidden">
     <label id="expandeduser-agentLabel" class="message-header-label"
            value="&userAgentField4.label;"/>
-    <mail-headerfield id="expandeduser-agentBox"/>
+    <html:div id="expandeduser-agentBox" is="simple-header-row"
+              data-header-name="user-agent"></html:div>
   </html:div>
 
   <!-- All extra headers will be dynamically added here. -->
   <html:div id="extraHeadersArea" class="message-header-extra-container"></html:div>
 
 </html:header>
 <!-- END Header container -->
 
--- a/mail/base/content/msgHdrView.js
+++ b/mail/base/content/msgHdrView.js
@@ -78,54 +78,47 @@ var gShowCondensedEmailAddresses;
  * Additionally, if your object has an onBeforeShowHeaderPane() method, it will
  * be called at the appropriate time.  This is designed to give add-ons a
  * chance to examine and modify the currentHeaderData array before it gets
  * displayed.
  */
 var gMessageListeners = [];
 
 /**
- * This expanded header view shows many of the more common (and useful) headers.
+ * List fo common headers that need to be populated.
  *
  * For every possible "view" in the message pane, you need to define the header
  * names you want to see in that view. In addition, include information
- * describing how you want that header field to be presented. i.e. if it's an
- * email address field, if you want a toggle inserted on the node in case
- * of multiple email addresses, etc. We'll then use this static table to
- * dynamically generate header view entries which manipulate the UI.
- * When you add a header to one of these view lists you can specify
- * the following properties:
- * name:           the name of the header. i.e. "to", "subject". This must be in
- *                 lower case and the name of the header is used to help
- *                 dynamically generate ids for objects in the document. (REQUIRED)
- * useToggle:      true if the values for this header are multiple email
- *                 addresses and you want a (more) toggle to show a short
- *                 vs. long list (DEFAULT: false)
- * outputFunction: this is a method which takes a headerEntry (see the definition
- *                 below) and a header value. This allows you to provide your own
- *                 methods for actually determining how the header value
- *                 is displayed. (DEFAULT: updateHeaderValue which just sets the
- *                 header value on the text node)
+ * describing how you want that header field to be presented. We'll then use
+ * this static table to dynamically generate header view entries which
+ * manipulate the UI.
+ * @param {string} name - The name of the header. i.e. "to", "subject". This
+ *   must be in lower case and the name of the header is used to help
+ *   dynamically generate ids for objects in the document.
+ * @param {Function} outputFunction - This is a method which takes a headerEntry
+ *   (see the definition below) and a header value. This allows to provide a
+ *   unique methods for determining how the header value is displayed. Defaults
+ *   to updateHeaderValue which just sets the header value on the text node.
  */
-var gExpandedHeaderList = [
+const gExpandedHeaderList = [
   { name: "subject" },
-  { name: "from", useToggle: true, outputFunction: OutputEmailAddresses },
-  { name: "reply-to", useToggle: true, outputFunction: OutputEmailAddresses },
-  { name: "to", useToggle: true, outputFunction: OutputEmailAddresses },
-  { name: "cc", useToggle: true, outputFunction: OutputEmailAddresses },
-  { name: "bcc", useToggle: true, outputFunction: OutputEmailAddresses },
-  { name: "newsgroups", outputFunction: OutputNewsgroups },
+  { name: "from", outputFunction: outputEmailAddresses },
+  { name: "reply-to", outputFunction: outputEmailAddresses },
+  { name: "to", outputFunction: outputEmailAddresses },
+  { name: "cc", outputFunction: outputEmailAddresses },
+  { name: "bcc", outputFunction: outputEmailAddresses },
+  { name: "newsgroups", outputFunction: outputNewsgroups },
   { name: "references", outputFunction: OutputMessageIds },
-  { name: "followup-to", outputFunction: OutputNewsgroups },
+  { name: "followup-to", outputFunction: outputNewsgroups },
   { name: "content-base" },
   { name: "tags" },
 ];
 
 /**
- * These are all the items that use a mail-multi-emailheaderfield widget and
+ * These are all the items that use a multi-recipient-row widget and
  * therefore may require updating if the address book changes.
  */
 var gEmailAddressHeaderNames = [
   "from",
   "reply-to",
   "to",
   "cc",
   "bcc",
@@ -277,42 +270,23 @@ function clearFolderDBListener() {
  * presented. The header entry actually has knowledge about the DOM
  * and the actual DOM elements associated with the header.
  *
  * @param prefix  the name of the view (e.g. "expanded")
  * @param headerListInfo  entry from a header list.
  */
 class MsgHeaderEntry {
   constructor(prefix, headerListInfo) {
-    let partialIDName = prefix + headerListInfo.name;
-    this.enclosingBox = document.getElementById(partialIDName + "Box");
-    this.enclosingRow = document.getElementById(partialIDName + "Row");
+    this.enclosingBox = document.getElementById(
+      `${prefix}${headerListInfo.name}Box`
+    );
+    this.enclosingRow = this.enclosingBox.closest(".message-header-row");
     this.isNewHeader = false;
     this.valid = false;
-
-    if ("useToggle" in headerListInfo) {
-      this.useToggle = headerListInfo.useToggle;
-    } else {
-      this.useToggle = false;
-    }
-
-    if ("outputFunction" in headerListInfo) {
-      this.outputFunction = headerListInfo.outputFunction;
-    } else {
-      this.outputFunction = updateHeaderValue;
-    }
-
-    // Stash this so that the <mail-multi-emailheaderfield/> binding can
-    // later attach it to any <mail-emailaddress> tags it creates for later
-    // extraction and use by UpdateEmailNodeDetails.
-    this.enclosingBox.headerName = headerListInfo.name;
-    // Set the headerName attribute for the value nodes too.
-    this.enclosingBox.querySelectorAll(".headerValue").forEach(e => {
-      e.setAttribute("headerName", headerListInfo.name);
-    });
+    this.outputFunction = headerListInfo.outputFunction || updateHeaderValue;
   }
 }
 
 function initializeHeaderViewTables() {
   // Iterate over each header in our header list arrays and create header entries
   // for each one. These header entries are then stored in the appropriate header
   // table.
   for (let header of gExpandedHeaderList) {
@@ -361,17 +335,20 @@ function initializeHeaderViewTables() {
     };
     gExpandedHeaderView[messageIdEntry.name] = new MsgHeaderEntry(
       "expanded",
       messageIdEntry
     );
   }
 
   if (Services.prefs.getBoolPref("mailnews.headers.showSender")) {
-    var senderEntry = { name: "sender", outputFunction: OutputEmailAddresses };
+    let senderEntry = {
+      name: "sender",
+      outputFunction: outputEmailAddresses,
+    };
     gExpandedHeaderView[senderEntry.name] = new MsgHeaderEntry(
       "expanded",
       senderEntry
     );
   }
 }
 
 async function OnLoadMsgHeaderPane() {
@@ -391,20 +368,16 @@ async function OnLoadMsgHeaderPane() {
   Services.prefs.addObserver("mail.showCondensedAddresses", MsgHdrViewObserver);
   Services.prefs.addObserver(
     "mailnews.headers.showReferences",
     MsgHdrViewObserver
   );
 
   initializeHeaderViewTables();
 
-  // Add an address book listener so we can update the header view when things
-  // change.
-  AddressBookListener.register();
-
   // Only offer openInTab and openInNewWindow if this window supports tabs.
   let opensAreHidden = !document.getElementById("tabmail");
   for (let id of ["otherActionsOpenInNewWindow", "otherActionsOpenInNewTab"]) {
     let menu = document.getElementById(id);
     if (menu) {
       // May not be available yet.
       menu.hidden = opensAreHidden;
     }
@@ -463,18 +436,16 @@ function OnUnloadMsgHeaderPane() {
     "mail.showCondensedAddresses",
     MsgHdrViewObserver
   );
   Services.prefs.removeObserver(
     "mailnews.headers.showReferences",
     MsgHdrViewObserver
   );
 
-  AddressBookListener.unregister();
-
   clearFolderDBListener();
 
   // Dispatch an event letting any listeners know that we have unloaded
   // the message pane.
   headerViewElement.dispatchEvent(
     new Event("messagepane-unloaded", { bubbles: false, cancelable: true })
   );
 }
@@ -493,85 +464,16 @@ var MsgHdrViewObserver = {
           "mailnews.headers.showReferences"
         );
         ReloadMessage();
       }
     }
   },
 };
 
-var AddressBookListener = {
-  _notifications: [
-    "addrbook-directory-created",
-    "addrbook-directory-deleted",
-    "addrbook-contact-created",
-    "addrbook-contact-updated",
-    "addrbook-contact-deleted",
-  ],
-  register() {
-    for (let topic of this._notifications) {
-      Services.obs.addObserver(this, topic);
-    }
-  },
-  unregister() {
-    for (let topic of this._notifications) {
-      Services.obs.removeObserver(this, topic);
-    }
-  },
-  observe(subject, topic, data) {
-    switch (topic) {
-      case "addrbook-directory-created":
-        subject.QueryInterface(Ci.nsIAbDirectory);
-        OnAddressBookDataChanged("itemAdded", null, subject);
-        break;
-      case "addrbook-directory-deleted":
-        subject.QueryInterface(Ci.nsIAbDirectory);
-        OnAddressBookDataChanged("directoryRemoved", null, subject);
-        break;
-      case "addrbook-contact-created":
-        subject.QueryInterface(Ci.nsIAbCard);
-        OnAddressBookDataChanged(
-          "itemAdded",
-          MailServices.ab.getDirectoryFromUID(data),
-          subject
-        );
-        break;
-      case "addrbook-contact-updated":
-        subject.QueryInterface(Ci.nsIAbCard);
-        OnAddressBookDataChanged("itemChanged", null, subject);
-        break;
-      case "addrbook-contact-deleted":
-        subject.QueryInterface(Ci.nsIAbCard);
-        OnAddressBookDataChanged(
-          "directoryItemRemoved",
-          MailServices.ab.getDirectoryFromUID(data),
-          subject
-        );
-        break;
-    }
-  },
-};
-
-function OnAddressBookDataChanged(aAction, aParentDir, aItem) {
-  gEmailAddressHeaderNames.forEach(function(headerName) {
-    let headerEntry = null;
-
-    if (headerName in gExpandedHeaderView) {
-      headerEntry = gExpandedHeaderView[headerName];
-      if (headerEntry) {
-        headerEntry.enclosingBox.updateExtraAddressProcessing(
-          aAction,
-          aParentDir,
-          aItem
-        );
-      }
-    }
-  });
-}
-
 /**
  * The messageHeaderSink is the class that gets notified of a message's headers
  * as we display the message through our mime converter.
  */
 var messageHeaderSink = {
   QueryInterface: ChromeUtils.generateQI(["nsIMsgHeaderSink"]),
   onStartHeaders() {
     this.mSaveHdr = null;
@@ -1112,19 +1014,18 @@ function OnTagsChange() {
 /**
  * Flush out any local state being held by a header entry for a given table.
  *
  * @param aHeaderTable Table of header entries
  */
 function ClearHeaderView(aHeaderTable) {
   for (let name in aHeaderTable) {
     let headerEntry = aHeaderTable[name];
-    if (headerEntry.enclosingBox.clearHeaderValues) {
-      headerEntry.enclosingBox.clearHeaderValues();
-    }
+    headerEntry.enclosingBox.clearHeaderValues?.();
+    headerEntry.enclosingBox.clear?.();
 
     headerEntry.valid = false;
   }
 }
 
 /**
  * Make sure that any valid header entry in the table is collapsed.
  *
@@ -1297,35 +1198,35 @@ class HeaderView {
       let newLabelNode = document.createXULElement("label");
       newLabelNode.setAttribute("id", "expanded" + headerName + "Label");
       newLabelNode.setAttribute("value", label);
       newLabelNode.setAttribute("class", "message-header-label");
 
       newRowNode.appendChild(newLabelNode);
 
       // Create and append the new header value.
-      newHeaderNode = document.createXULElement("mail-headerfield");
+      newHeaderNode = document.createElement("div", {
+        is: "simple-header-row",
+      });
       newHeaderNode.setAttribute("id", idName);
-      newHeaderNode.setAttribute("flex", "1");
-
+      newHeaderNode.dataset.headerName = headerName;
       newRowNode.appendChild(newHeaderNode);
 
       // Add the new row to the extra headers container.
       document.getElementById("extraHeadersArea").appendChild(newRowNode);
       this.isNewHeader = true;
     } else {
       newRowNode.hidden = true;
       newHeaderNode = document.getElementById(idName);
       this.isNewHeader = false;
     }
 
     this.enclosingBox = newHeaderNode;
     this.enclosingRow = newRowNode;
     this.valid = false;
-    this.useToggle = false;
     this.outputFunction = updateHeaderValue;
   }
 }
 
 /**
  * Removes all non-predefined header nodes from the view.
  *
  * @param aHeaderTable  Table of header entries.
@@ -1466,29 +1367,28 @@ function HideMessageHeaderPane() {
   document.getElementById("attachment-splitter").collapsed = true;
 
   gMessageNotificationBar.clearMsgNotifications();
   // Clear the DBListener since we don't have any visible UI to update.
   clearFolderDBListener();
 }
 
 /**
- * Take string of newsgroups separated by commas, split it
- * into newsgroups and send them to the corresponding
- * mail-newsgroups-headerfield element.
+ * Take a string of newsgroups separated by commas, split it into newsgroups and
+ * add them to the corresponding header-newsgroups-row element.
  *
- * @param headerEntry  the entry data structure for this header
- * @param headerValue  the string value for the header from the message
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} headerValue - The string of newsgroups from the message.
  */
-function OutputNewsgroups(headerEntry, headerValue) {
+function outputNewsgroups(headerEntry, headerValue) {
   headerValue
     .split(",")
-    .forEach(newsgroup => headerEntry.enclosingBox.addNewsgroupView(newsgroup));
-
-  headerEntry.enclosingBox.buildViews();
+    .forEach(newsgroup => headerEntry.enclosingBox.addNewsgroup(newsgroup));
+
+  headerEntry.enclosingBox.buildView();
 }
 
 /**
  * Take string of message-ids separated by whitespace, split it
  * into message-ids and send them together with the index number
  * to the corresponding mail-messageids-headerfield element.
  */
 function OutputMessageIds(headerEntry, headerValue) {
@@ -1498,522 +1398,54 @@ function OutputMessageIds(headerEntry, h
   for (let i = 0; i < messageIdArray.length; i++) {
     headerEntry.enclosingBox.addMessageIdView(messageIdArray[i]);
   }
 
   headerEntry.enclosingBox.fillMessageIdNodes();
 }
 
 /**
- * OutputEmailAddresses: knows how to take a comma separated list of email
- * addresses, extracts them one by one, linkifying each email address into
- * a mailto url. Then we add the link-ified email address to the parentDiv
- * passed in.
+ * Take a string of addresses separated by commas, split it into separated
+ * recipient objects and add them to the related parent container row.
  *
- * @param headerEntry     parent div
- * @param emailAddresses  comma separated list of the addresses for this
- *                        header field
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} emailAddresses - The string of addresses from the message.
  */
-function OutputEmailAddresses(headerEntry, emailAddresses) {
+function outputEmailAddresses(headerEntry, emailAddresses) {
   if (!emailAddresses) {
     return;
   }
 
-  // The email addresses are still RFC2047 encoded but libmime has already converted from
-  // "raw UTF-8" to "wide" (UTF-16) characters.
-  var addresses = MailServices.headerParser.parseEncodedHeaderW(emailAddresses);
-
-  if (headerEntry.useToggle) {
-    // Make sure we start clean.
-    headerEntry.enclosingBox.resetAddressView();
+  // The email addresses are still RFC2047 encoded but libmime has already
+  // converted from "raw UTF-8" to "wide" (UTF-16) characters.
+  let addresses = MailServices.headerParser.parseEncodedHeaderW(emailAddresses);
+
+  // Make sure we start clean.
+  headerEntry.enclosingBox.clear();
+
+  // No addresses and a colon, so an empty group like "undisclosed-recipients: ;".
+  // Add group name so at least something displays.
+  if (!addresses.length && emailAddresses.includes(":")) {
+    let address = { displayName: emailAddresses };
+    headerEntry.enclosingBox.addRecipient(address);
   }
-  if (addresses.length == 0 && emailAddresses.includes(":")) {
-    // No addresses and a colon, so an empty group like "undisclosed-recipients: ;".
-    // Add group name so at least something displays.
-    let address = { displayName: emailAddresses };
-    if (headerEntry.useToggle) {
-      headerEntry.enclosingBox.addAddressView(address);
-    } else {
-      updateEmailAddressNode(
-        headerEntry.enclosingBox.emailAddressNode,
-        address
-      );
-    }
-  }
+
   for (let addr of addresses) {
     // If we want to include short/long toggle views and we have a long view,
     // always add it. If we aren't including a short/long view OR if we are and
     // we haven't parsed enough addresses to reach the cutoff valve yet then add
     // it to the default (short) div.
     let address = {};
     address.emailAddress = addr.email;
     address.fullAddress = addr.toString();
     address.displayName = addr.name;
-    if (headerEntry.useToggle) {
-      headerEntry.enclosingBox.addAddressView(address);
-    } else {
-      updateEmailAddressNode(
-        headerEntry.enclosingBox.emailAddressNode,
-        address
-      );
-    }
-  }
-
-  if (headerEntry.useToggle) {
-    headerEntry.enclosingBox.buildViews();
-  }
-}
-
-function updateEmailAddressNode(emailAddressNode, address) {
-  emailAddressNode.setAttribute("emailAddress", address.emailAddress || "");
-  emailAddressNode.setAttribute("fullAddress", address.fullAddress || "");
-  emailAddressNode.setAttribute("displayName", address.displayName || "");
-
-  if (address.emailAddress) {
-    UpdateEmailNodeDetails(address.emailAddress, emailAddressNode);
-  }
-}
-
-function UpdateEmailNodeDetails(aEmailAddress, aDocumentNode, aCardDetails) {
-  // If we haven't been given specific details, search for a card.
-  var cardDetails =
-    aCardDetails || DisplayNameUtils.getCardForEmail(aEmailAddress);
-  // FIXME: It would be useful and cleaner to move the handling of the
-  // mail-emailaddress elements to the element's class itself. That way the
-  // logic wouldn't be spread between two separate scripts.
-  aDocumentNode.cardDetails = cardDetails;
-
-  aDocumentNode.setAddressBookState(!!cardDetails.card);
-
-  // When we are adding cards, we don't want to move the display around if the
-  // user has clicked on the star, therefore if it is locked, just exit and
-  // leave the display updates until later.
-  if (aDocumentNode.hasAttribute("updatingUI")) {
-    return;
-  }
-
-  var displayName = DisplayNameUtils.formatDisplayName(
-    aEmailAddress,
-    aDocumentNode.getAttribute("displayName"),
-    aDocumentNode.getAttribute("headerName"),
-    aDocumentNode.cardDetails.card
-  );
-
-  if (gShowCondensedEmailAddresses && displayName) {
-    aDocumentNode.setAttribute("tooltiptext", aEmailAddress);
-  } else {
-    aDocumentNode.removeAttribute("tooltiptext");
-    displayName =
-      aDocumentNode.getAttribute("fullAddress") ||
-      aDocumentNode.getAttribute("displayName");
-  }
-  aDocumentNode.setAttribute("label", displayName);
-}
-
-// FIXME: This method is only called in another file by
-// MozMailMultiEmailheaderfield.updateExtraAddressProcessing, which in turn
-// is only invoked by OnAddressBookDataChanged in this file. We should avoid
-// moving between files when this could all be handled by the element's class
-// itself.
-function UpdateExtraAddressProcessing(
-  aAddressData,
-  aDocumentNode,
-  aAction,
-  aParentDir,
-  aItem
-) {
-  switch (aAction) {
-    case "itemChanged":
-      if (
-        aAddressData &&
-        aDocumentNode.cardDetails.card &&
-        !aItem.isMailList &&
-        aItem.hasEmailAddress(aAddressData.emailAddress)
-      ) {
-        aDocumentNode.cardDetails.card = aItem;
-        var displayName = DisplayNameUtils.formatDisplayName(
-          aAddressData.emailAddress,
-          aDocumentNode.getAttribute("displayName"),
-          aDocumentNode.getAttribute("headerName"),
-          aDocumentNode.cardDetails.card
-        );
-
-        if (gShowCondensedEmailAddresses && displayName) {
-          aDocumentNode.setAttribute("label", displayName);
-        } else {
-          aDocumentNode.setAttribute(
-            "label",
-            aDocumentNode.getAttribute("fullAddress") ||
-              aDocumentNode.getAttribute("displayName")
-          );
-        }
-      }
-      break;
-    case "itemAdded":
-      // Is it a new address book?
-      if (aItem instanceof Ci.nsIAbDirectory) {
-        // If we don't have a match, search again for updates (e.g. a interface
-        // to an existing book may just have been added).
-        if (aDocumentNode && !aDocumentNode.cardDetails.card) {
-          UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
-        }
-      } else if (aItem instanceof Ci.nsIAbCard) {
-        // If we don't have a card, does this new one match?
-        if (
-          !aDocumentNode?.cardDetails?.card &&
-          !aItem.isMailList &&
-          aItem.hasEmailAddress(aAddressData.emailAddress)
-        ) {
-          // Just in case we have a bogus parent directory.
-          if (aParentDir instanceof Ci.nsIAbDirectory) {
-            var cardDetails = { book: aParentDir, card: aItem };
-            UpdateEmailNodeDetails(
-              aAddressData.emailAddress,
-              aDocumentNode,
-              cardDetails
-            );
-          } else {
-            UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
-          }
-        }
-      }
-      break;
-    case "directoryItemRemoved":
-      // Unfortunately we don't necessarily get the same card object back.
-      if (
-        aAddressData &&
-        aDocumentNode.cardDetails &&
-        aDocumentNode.cardDetails.card &&
-        aDocumentNode.cardDetails.book == aParentDir &&
-        !aItem.isMailList &&
-        aItem.hasEmailAddress(aAddressData.emailAddress)
-      ) {
-        UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
-      }
-      break;
-    case "directoryRemoved":
-      if (aDocumentNode?.cardDetails.book == aItem) {
-        UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
-      }
-      break;
-  }
-}
-
-function findEmailNodeFromPopupNode(elt, popup) {
-  // This annoying little function is needed because in the binding for
-  // mail-emailaddress, we set the context on the <description>, but that if
-  // the user clicks on the label, then popupNode is set to it, rather than
-  // the description.  So we have walk up the parent until we find the
-  // element with the popup set, and then return its parent.
-
-  while (elt.getAttribute("popup") != popup) {
-    elt = elt.parentNode;
-    if (elt == null) {
-      return null;
-    }
-  }
-  return elt.parentNode;
-}
-
-function hideEmailNewsPopup(addressNode) {
-  addressNode = addressNode.hasAttribute("newsgroup")
-    ? addressNode.closest("mail-newsgroup")
-    : addressNode.closest("mail-emailaddress");
-  // highlight the emailBox/newsgroupBox
-  addressNode.removeAttribute("selected");
-}
-
-async function setupEmailAddressPopup(emailAddressNode) {
-  emailAddressNode = emailAddressNode.closest("mail-emailaddress");
-  emailAddressNode.setAttribute("selected", "true");
-  var emailAddressPlaceHolder = document.getElementById(
-    "emailAddressPlaceHolder"
-  );
-  emailAddressPlaceHolder.setAttribute(
-    "label",
-    emailAddressNode.getAttribute("label")
-  );
-
-  if (emailAddressNode.cardDetails && emailAddressNode.cardDetails.card) {
-    document
-      .getElementById("addToAddressBookItem")
-      .setAttribute("hidden", true);
-    if (!emailAddressNode.cardDetails.book.readOnly) {
-      document.getElementById("editContactItem").removeAttribute("hidden");
-      document.getElementById("viewContactItem").setAttribute("hidden", true);
-    } else {
-      document.getElementById("editContactItem").setAttribute("hidden", true);
-      document.getElementById("viewContactItem").removeAttribute("hidden");
-    }
-  } else {
-    document.getElementById("addToAddressBookItem").removeAttribute("hidden");
-    document.getElementById("editContactItem").setAttribute("hidden", true);
-    document.getElementById("viewContactItem").setAttribute("hidden", true);
-  }
-  let discoverKeyMenuItem = document.getElementById("searchKeysOpenPGP");
-  if (discoverKeyMenuItem) {
-    let address = emailAddressNode
-      .closest("mail-emailaddress")
-      .getAttribute("emailAddress");
-    let hidden = await PgpSqliteDb2.hasAnyPositivelyAcceptedKeyForEmail(
-      address
-    );
-    discoverKeyMenuItem.hidden = hidden;
-    discoverKeyMenuItem.nextElementSibling.hidden = hidden; // Hide separator.
+    headerEntry.enclosingBox.addRecipient(address);
   }
-}
-
-/**
- * Takes the email address node, adds a new contact from the node's
- * displayName and emailAddress attributes to the personal address book.
- *
- * @param emailAddressNode  a node with displayName and emailAddress attributes
- */
-function AddContact(emailAddressNode) {
-  emailAddressNode = emailAddressNode.closest("mail-emailaddress");
-  // When we collect an address, it updates the AB which sends out
-  // notifications to update the UI. In the add case we don't want to update
-  // the UI so that accidentally double-clicking on the star doesn't lead
-  // to something strange (i.e star would be moved out from underneath,
-  // leaving something else there).
-  emailAddressNode.setAttribute("updatingUI", true);
-
-  let kPersonalAddressbookURI = "jsaddrbook://abook.sqlite";
-  let addressBook = MailServices.ab.getDirectory(kPersonalAddressbookURI);
-
-  let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
-    Ci.nsIAbCard
-  );
-  card.displayName = emailAddressNode.getAttribute("displayName");
-  card.primaryEmail = emailAddressNode.getAttribute("emailAddress");
-
-  // Just save the new node straight away.
-  addressBook.addCard(card);
-
-  emailAddressNode.removeAttribute("updatingUI");
-}
-
-function EditContact(emailAddressNode) {
-  emailAddressNode = emailAddressNode.closest("mail-emailaddress");
-  if (emailAddressNode.cardDetails.card) {
-    editContactInlineUI.showEditContactPanel(
-      emailAddressNode.cardDetails,
-      emailAddressNode
-    );
-  }
-}
-
-/**
- * Takes the email address title button, extracts the email address we stored
- * in there and opens a compose window with that address.
- *
- * @param addressNode  a node which has a "fullAddress" or "newsgroup" attribute
- * @param aEvent       the event object when user triggers the menuitem
- */
-function SendMailToNode(addressNode, aEvent) {
-  addressNode = addressNode.hasAttribute("newsgroup")
-    ? addressNode.closest("mail-newsgroup")
-    : addressNode.closest("mail-emailaddress");
-  let fields = Cc[
-    "@mozilla.org/messengercompose/composefields;1"
-  ].createInstance(Ci.nsIMsgCompFields);
-  let params = Cc[
-    "@mozilla.org/messengercompose/composeparams;1"
-  ].createInstance(Ci.nsIMsgComposeParams);
-
-  fields.newsgroups = addressNode.getAttribute("newsgroup");
-  if (addressNode.hasAttribute("fullAddress")) {
-    let addresses = MailServices.headerParser.makeFromDisplayAddress(
-      addressNode.getAttribute("fullAddress")
-    );
-    if (addresses.length > 0) {
-      fields.to = MailServices.headerParser.makeMimeHeader([addresses[0]]);
-    }
-  }
-
-  params.type = Ci.nsIMsgCompType.New;
-
-  // If aEvent is passed, check if Shift key was pressed for composition in
-  // non-default format (HTML vs. plaintext).
-  params.format =
-    aEvent && aEvent.shiftKey
-      ? Ci.nsIMsgCompFormat.OppositeOfDefault
-      : Ci.nsIMsgCompFormat.Default;
-
-  if (gFolderDisplay.displayedFolder) {
-    params.identity = accountManager.getFirstIdentityForServer(
-      gFolderDisplay.displayedFolder.server
-    );
-  }
-  params.composeFields = fields;
-  MailServices.compose.OpenComposeWindowWithParams(null, params);
-}
-
-/**
- * Takes the email address or newsgroup title button, extracts the address/name
- * we stored in there and copies it to the clipboard.
- *
- * @param addressNode  a node which has an "emailAddress" or "newsgroup"
- *                     attribute
- * @param aIncludeName when true, also copy the name onto the clipboard,
- *                     otherwise only the email address
- */
-function CopyEmailNewsAddress(addressNode, aIncludeName = false) {
-  addressNode = addressNode.hasAttribute("newsgroup")
-    ? addressNode.closest("mail-newsgroup")
-    : addressNode.closest("mail-emailaddress");
-  let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
-    Ci.nsIClipboardHelper
-  );
-  let address =
-    addressNode.getAttribute(aIncludeName ? "fullAddress" : "emailAddress") ||
-    addressNode.getAttribute("newsgroup");
-  clipboard.copyString(address);
-}
-
-/**
- * Causes the filter dialog to pop up, prefilled for the specified e-mail
- * address or header value.
- *
- * @param aHeaderNode  Node for which to create the filter. This can be a node
- *                     in an mail-emailaddress element, or a node with just
- *                     textual data, like Subject or Date.
- * @param aMessage     Optional nsIMsgHdr of the message from which the values
- *                     are taken. Will be used to preselect its folder in the
- *                     filter list.
- */
-function CreateFilter(aHeaderNode, aMessage) {
-  let addressNode = aHeaderNode.closest("mail-emailaddress");
-  let value;
-  let name;
-  if (addressNode) {
-    name = addressNode.getAttribute("headerName");
-    value = addressNode.getAttribute("emailAddress");
-  } else {
-    name = aHeaderNode.getAttribute("headerName");
-    value = aHeaderNode.textContent;
-  }
-  let folder = aMessage ? aMessage.folder : null;
-  top.MsgFilters(value, folder, name);
-}
-
-/**
- * Get the newsgroup server corresponding to the currently selected message.
- *
- * @return nsISubscribableServer for the newsgroup, or null
- */
-function GetNewsgroupServer() {
-  if (gFolderDisplay.selectedMessageIsNews) {
-    let server = gFolderDisplay.selectedMessage.folder.server;
-    if (server) {
-      return server.QueryInterface(Ci.nsISubscribableServer);
-    }
-  }
-  return null;
-}
-
-/**
- * Initialize the newsgroup popup, showing/hiding menu items as appropriate.
- *
- * @param newsgroupNode  a node which has a "newsgroup" attribute
- */
-function setupNewsgroupPopup(newsgroupNode) {
-  let newsgroupPlaceHolder = document.getElementById("newsgroupPlaceHolder");
-  let newsgroup = newsgroupNode.getAttribute("newsgroup");
-  newsgroupNode.setAttribute("selected", "true");
-  newsgroupPlaceHolder.setAttribute("label", newsgroup);
-
-  let server = GetNewsgroupServer();
-  if (server) {
-    // XXX Why is this necessary when nsISubscribableServer contains
-    // |isSubscribed|?
-    server = server.QueryInterface(Ci.nsINntpIncomingServer);
-    if (!server.containsNewsgroup(newsgroup)) {
-      document
-        .getElementById("subscribeToNewsgroupItem")
-        .removeAttribute("hidden");
-      document
-        .getElementById("subscribeToNewsgroupSeparator")
-        .removeAttribute("hidden");
-      return;
-    }
-  }
-  document
-    .getElementById("subscribeToNewsgroupItem")
-    .setAttribute("hidden", true);
-  document
-    .getElementById("subscribeToNewsgroupSeparator")
-    .setAttribute("hidden", true);
-}
-
-/**
- * Subscribe to a newsgroup based on the newsgroup title button
- *
- * @param newsgroupNode  a node which has a "newsgroup" attribute
- */
-function SubscribeToNewsgroup(newsgroupNode) {
-  let server = GetNewsgroupServer();
-  if (server) {
-    let newsgroup = newsgroupNode.getAttribute("newsgroup");
-    server.subscribe(newsgroup);
-    server.commitSubscribeChanges();
-  }
-}
-
-/**
- * Takes the newsgroup address title button, extracts the newsgroup name we
- * stored in there and copies it to the clipboard.
- *
- * @param newsgroupNode  a node which has a "newsgroup" attribute
- */
-function CopyNewsgroupName(newsgroupNode) {
-  let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
-    Ci.nsIClipboardHelper
-  );
-  clipboard.copyString(newsgroupNode.getAttribute("newsgroup"));
-}
-
-/**
- * Takes the newsgroup address title button, extracts the newsgroup name we
- * stored in there and copies it URL to it.
- *
- * @param newsgroupNode  a node which has a "newsgroup" attribute
- */
-function CopyNewsgroupURL(newsgroupNode) {
-  let server = GetNewsgroupServer();
-  if (!server) {
-    return;
-  }
-
-  let ng = newsgroupNode.getAttribute("newsgroup");
-
-  let url;
-  if (server.socketType != Ci.nsMsgSocketType.SSL) {
-    url = "news://" + server.hostName;
-    if (server.port != Ci.nsINntpUrl.DEFAULT_NNTP_PORT) {
-      url += ":" + server.port;
-    }
-    url += "/" + ng;
-  } else {
-    url = "snews://" + server.hostName;
-    if (server.port != Ci.nsINntpUrl.DEFAULT_NNTPS_PORT) {
-      url += ":" + server.port;
-    }
-    url += "/" + ng;
-  }
-
-  try {
-    let uri = Services.io.newURI(url);
-    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
-      Ci.nsIClipboardHelper
-    );
-    clipboard.copyString(decodeURI(uri.spec));
-  } catch (e) {
-    Cu.reportError("Invalid URL: " + url);
-  }
+
+  headerEntry.enclosingBox.buildView();
 }
 
 /**
  * Create a new attachment object which goes into the data attachment array.
  * This method checks whether the passed attachment is empty or not.
  *
  * @param {String} contentType - The attachment's mimetype.
  * @param {String} url         - The URL for the attachment.
@@ -3599,20 +3031,17 @@ function HandleMultipleAttachments(attac
         }
       };
 
       saveAttachments(attachments);
       return;
     case "copyUrl":
       // Copy external http url(s) to clipboard. The menuitem is hidden unless
       // all selected attachment urls are http.
-      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
-        Ci.nsIClipboardHelper
-      );
-      clipboard.copyString(attachmentDisplayUrlArray.join("\n"));
+      navigator.clipboard.writeText(attachmentDisplayUrlArray.join("\n"));
       return;
     case "openFolder":
       for (let attachment of attachments) {
         setTimeout(() => attachment.openFolder());
       }
       return;
     default:
       throw new Error("unknown HandleMultipleAttachments action: " + action);
@@ -3704,22 +3133,17 @@ let attachmentNameDNDObserver = {
 };
 
 /**
  * CopyWebsiteAddress takes the website address title button, extracts
  * the website address we stored in there and copies it to the clipboard
  */
 function CopyWebsiteAddress(websiteAddressNode) {
   if (websiteAddressNode) {
-    var websiteAddress = websiteAddressNode.textContent;
-
-    var contractid = "@mozilla.org/widget/clipboardhelper;1";
-    var iid = Ci.nsIClipboardHelper;
-    var clipboard = Cc[contractid].getService(iid);
-    clipboard.copyString(websiteAddress);
+    navigator.clipboard.writeText(websiteAddressNode.textContent);
   }
 }
 
 function nsDummyMsgHeader() {}
 
 nsDummyMsgHeader.prototype = {
   mProperties: [],
   getStringProperty(aProperty) {
@@ -3922,8 +3346,282 @@ var gHeaderCustomize = {
     Services.xulStore.setValue(
       this.docURL,
       "messageHeader",
       "layout",
       JSON.stringify(this.customizeData)
     );
   },
 };
+
+/**
+ * Object to handle the creation, destruction, and update of all recipient
+ * fields that will be showed in the message header.
+ */
+const gMessageHeader = {
+  /**
+   * Get the newsgroup server corresponding to the currently selected message.
+   *
+   * @return {?nsISubscribableServer} The server for the newsgroup, or null.
+   */
+  get newsgroupServer() {
+    if (gFolderDisplay.selectedMessageIsNews) {
+      let server = gFolderDisplay.selectedMessage.folder.server;
+      if (server) {
+        return server.QueryInterface(Ci.nsISubscribableServer);
+      }
+    }
+
+    return null;
+  },
+
+  /**
+   * Toggle the scrollable style of the message header area.
+   *
+   * @param {boolean} showAllHeaders - True if we need to show all header fields
+   *   and ignore the space limit for multi recipients row.
+   */
+  toggleScrollableHeader(showAllHeaders) {
+    document
+      .getElementById("messageHeader")
+      .classList.toggle("scrollable", showAllHeaders);
+  },
+
+  openCopyPopup(event, element) {
+    let popup = document.getElementById("copyPopup");
+    popup.headerField = element;
+    popup.openPopupAtScreen(event.screenX, event.screenY, true);
+  },
+
+  async openEmailAddressPopup(event, element) {
+    // Bail out if we don't have an email address.
+    if (!element.emailAddress) {
+      return;
+    }
+
+    document
+      .getElementById("emailAddressPlaceHolder")
+      .setAttribute("label", element.emailAddress);
+
+    document.getElementById("addToAddressBookItem").hidden =
+      element.cardDetails.card;
+    document.getElementById("editContactItem").hidden =
+      !element.cardDetails.card || element.cardDetails.book?.readOnly;
+    document.getElementById("viewContactItem").hidden =
+      !element.cardDetails.card || !element.cardDetails.book?.readOnly;
+
+    let discoverKeyMenuItem = document.getElementById("searchKeysOpenPGP");
+    if (discoverKeyMenuItem) {
+      let hidden = await PgpSqliteDb2.hasAnyPositivelyAcceptedKeyForEmail(
+        element.emailAddress
+      );
+      discoverKeyMenuItem.hidden = hidden;
+      discoverKeyMenuItem.nextElementSibling.hidden = hidden; // Hide separator.
+    }
+
+    let popup = document.getElementById("emailAddressPopup");
+    popup.headerField = element;
+
+    if (!event.screenX) {
+      popup.openPopup(event.target, "after_start", 0, 0, true);
+      return;
+    }
+
+    popup.openPopupAtScreen(event.screenX, event.screenY, true);
+  },
+
+  openNewsgroupPopup(event, element) {
+    document
+      .getElementById("newsgroupPlaceHolder")
+      .setAttribute("label", element.textContent);
+
+    let server = this.newsgroupServer;
+    if (server) {
+      // XXX Why is this necessary when nsISubscribableServer contains
+      // |isSubscribed|?
+      server = server.QueryInterface(Ci.nsINntpIncomingServer);
+      if (!server.containsNewsgroup(element.textContent)) {
+        document.getElementById("subscribeToNewsgroupItem").hidden = false;
+        document.getElementById("subscribeToNewsgroupSeparator").hidden = false;
+        return;
+      }
+    }
+    document.getElementById("subscribeToNewsgroupItem").hidden = true;
+    document.getElementById("subscribeToNewsgroupSeparator").hidden = true;
+
+    let popup = document.getElementById("newsgroupPopup");
+    popup.headerField = element;
+
+    if (!event.screenX) {
+      popup.openPopup(event.target, "after_start", 0, 0, true);
+      return;
+    }
+
+    popup.openPopupAtScreen(event.screenX, event.screenY, true);
+  },
+
+  /**
+   * Add a contact to the address book.
+   *
+   * @param {Event} event - The DOM Event.
+   */
+  addContact(event) {
+    event.currentTarget.parentNode.headerField.addToAddressBook();
+  },
+
+  /**
+   * Show the edit card popup panel.
+   *
+   * @param {Event} event - The DOM Event.
+   */
+  showContactEdit(event) {
+    this.editContact(event.currentTarget.parentNode.headerField);
+  },
+
+  /**
+   * Trigger a new message compose window.
+   *
+   * @param {Event} event - The click DOMEvent.
+   */
+  composeMessage(event) {
+    let recipient = event.currentTarget.parentNode.headerField;
+
+    let fields = Cc[
+      "@mozilla.org/messengercompose/composefields;1"
+    ].createInstance(Ci.nsIMsgCompFields);
+
+    if (recipient.classList.contains("header-newsgroup")) {
+      fields.newsgroups = recipient.textContent;
+    }
+
+    if (recipient.fullAddress) {
+      let addresses = MailServices.headerParser.makeFromDisplayAddress(
+        recipient.fullAddress
+      );
+      if (addresses.length) {
+        fields.to = MailServices.headerParser.makeMimeHeader([addresses[0]]);
+      }
+    }
+
+    let params = Cc[
+      "@mozilla.org/messengercompose/composeparams;1"
+    ].createInstance(Ci.nsIMsgComposeParams);
+    params.type = Ci.nsIMsgCompType.New;
+
+    // If the Shift key was pressed toggle the composition format
+    // (HTML vs. plaintext).
+    params.format = event.shiftKey
+      ? Ci.nsIMsgCompFormat.OppositeOfDefault
+      : Ci.nsIMsgCompFormat.Default;
+
+    if (gFolderDisplay.displayedFolder) {
+      params.identity = accountManager.getFirstIdentityForServer(
+        gFolderDisplay.displayedFolder.server
+      );
+    }
+    params.composeFields = fields;
+    MailServices.compose.OpenComposeWindowWithParams(null, params);
+  },
+
+  /**
+   * Copy the email address, as well as the name if wanted, in the clipboard.
+   *
+   * @param {Event} event - The DOM Event.
+   * @param {boolean} withName - True if we need to copy also the name.
+   */
+  copyAddress(event, withName = false) {
+    let recipient = event.currentTarget.parentNode.headerField;
+    let address;
+    if (recipient.classList.contains("header-newsgroup")) {
+      address = recipient.textContent;
+    } else {
+      address = withName ? recipient.fullAddress : recipient.emailAddress;
+    }
+    navigator.clipboard.writeText(address);
+  },
+
+  copyNewsgroupURL(event) {
+    let server = this.newsgroupServer;
+    if (!server) {
+      return;
+    }
+
+    let newsgroup = event.currentTarget.parentNode.headerField.textContent;
+
+    let url;
+    if (server.socketType != Ci.nsMsgSocketType.SSL) {
+      url = "news://" + server.hostName;
+      if (server.port != Ci.nsINntpUrl.DEFAULT_NNTP_PORT) {
+        url += ":" + server.port;
+      }
+      url += "/" + newsgroup;
+    } else {
+      url = "snews://" + server.hostName;
+      if (server.port != Ci.nsINntpUrl.DEFAULT_NNTPS_PORT) {
+        url += ":" + server.port;
+      }
+      url += "/" + newsgroup;
+    }
+
+    try {
+      let uri = Services.io.newURI(url);
+      navigator.clipboard.writeText(decodeURI(uri.spec));
+    } catch (e) {
+      Cu.reportError("Invalid URL: " + url);
+    }
+  },
+
+  /**
+   * Subscribe to a newsgroup.
+   *
+   * @param {Event} event - The DOM Event.
+   */
+  subscribeToNewsgroup(event) {
+    let server = this.newsgroupServer;
+    if (server) {
+      let newsgroup = event.currentTarget.parentNode.headerField.textContent;
+      server.subscribe(newsgroup);
+      server.commitSubscribeChanges();
+    }
+  },
+
+  /**
+   * Copy the text value of an header field.
+   *
+   * @param {Event} event - The DOM Event.
+   */
+  copyString(event) {
+    // This method is used inside the copyPopup menupopup, which is triggered by
+    // both HTML headers fields and XUL labels. We need to account for those
+    // different widgets in order to properly copy the text.
+    let target =
+      event.currentTarget.parentNode.triggerNode ||
+      event.currentTarget.parentNode.headerField;
+    navigator.clipboard.writeText(
+      window.getSelection().isCollapsed
+        ? target.textContent
+        : window.getSelection().toString()
+    );
+  },
+
+  /**
+   * Open the message filter dialog prefilled with available data.
+   *
+   * @param {Event} event - The DOM Event.
+   */
+  createFilter(event) {
+    let recipient = event.currentTarget.parentNode.headerField;
+    MsgFilters(
+      recipient.emailAddress || recipient.textContent,
+      gMessageDisplay?.displayedMessage?.folder,
+      recipient.dataset.headerName
+    );
+  },
+
+  /**
+   * Show the edit contact popup panel.
+   *
+   * @param {HTMLLIElement} element - The recipient element.
+   */
+  editContact(element) {
+    editContactInlineUI.showEditContactPanel(element.cardDetails, element);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mail/base/content/widgets/header-fields.js
@@ -0,0 +1,627 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global gMessageHeader, gShowCondensedEmailAddresses */
+
+{
+  const { Services } = ChromeUtils.import(
+    "resource://gre/modules/Services.jsm"
+  );
+  const { DisplayNameUtils } = ChromeUtils.import(
+    "resource:///modules/DisplayNameUtils.jsm"
+  );
+  const { MailServices } = ChromeUtils.import(
+    "resource:///modules/MailServices.jsm"
+  );
+
+  class MultiRecipientRow extends HTMLDivElement {
+    /**
+     * The number of lines of recipients to display before adding a <more>
+     * indicator to the widget. This can be increased using the preference
+     * mailnews.headers.show_n_lines_before_more.
+     *
+     * @type {integer}
+     */
+    #maxLinesBeforeMore = 1;
+
+    /**
+     * The array of all the recipients that need to be shown in this widget.
+     *
+     * @type {Array<Object>}
+     */
+    #recipients = [];
+
+    connectedCallback() {
+      if (this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.classList.add("multi-recipient-row");
+
+      this.heading = document.createElement("span");
+      this.heading.id = `${this.dataset.headerName}Heading`;
+      let sep = document.createElement("span");
+      sep.classList.add("screen-reader-only");
+      sep.setAttribute("data-l10n-name", "field-separator");
+      this.heading.appendChild(sep);
+      this.heading.hidden = true;
+      // message-header-to-field
+      // message-header-from-field
+      // message-header-cc-field
+      // message-header-bcc-field
+      // message-header-sender-field
+      // message-header-reply-to-field
+      document.l10n.setAttributes(
+        this.heading,
+        `message-header-${this.dataset.headerName}-field`
+      );
+      this.appendChild(this.heading);
+
+      this.recipientsList = document.createElement("ol");
+      this.recipientsList.classList.add("recipients-list");
+      this.appendChild(this.recipientsList);
+
+      this.moreButton = document.createElement("button");
+      this.moreButton.setAttribute("type", "button");
+      this.moreButton.classList.add("show-more-recipients", "plain");
+      this.moreButton.addEventListener("click", () => this.showAllRecipients());
+
+      document.l10n.setAttributes(
+        this.moreButton,
+        "message-header-field-show-more"
+      );
+
+      // @implements {nsIObserver}
+      this.ABObserver = {
+        /**
+         * Array list of all observable notifications.
+         *
+         * @type {Array<string>}
+         */
+        _notifications: [
+          "addrbook-directory-created",
+          "addrbook-directory-deleted",
+          "addrbook-contact-created",
+          "addrbook-contact-updated",
+          "addrbook-contact-deleted",
+        ],
+
+        addObservers() {
+          for (let topic of this._notifications) {
+            Services.obs.addObserver(this, topic);
+          }
+          this._added = true;
+          window.addEventListener("unload", () => this);
+        },
+
+        removeObservers() {
+          if (!this._added) {
+            return;
+          }
+          for (let topic of this._notifications) {
+            Services.obs.removeObserver(this, topic);
+          }
+          this._added = false;
+          window.removeEventListener("unload", () => this);
+        },
+
+        handleEvent() {
+          this.removeObservers();
+        },
+
+        observe: (subject, topic, data) => {
+          switch (topic) {
+            case "addrbook-directory-created":
+            case "addrbook-directory-deleted":
+              subject.QueryInterface(Ci.nsIAbDirectory);
+              this.directoryChanged(subject);
+              break;
+            case "addrbook-contact-created":
+            case "addrbook-contact-updated":
+            case "addrbook-contact-deleted":
+              subject.QueryInterface(Ci.nsIAbCard);
+              this.contactUpdated(subject);
+              break;
+          }
+        },
+      };
+
+      this.ABObserver.addObservers();
+    }
+
+    /**
+     * Clear things out when the element is removed from the DOM.
+     */
+    disconnectedCallback() {
+      this.ABObserver.removeObservers();
+    }
+
+    /**
+     * Loop through all available recipients and check if any of those belonged
+     * to the created or removed address book.
+     *
+     * @param {nsIAbDirectory} subject - The created or removed Address Book.
+     */
+    directoryChanged(subject) {
+      if (!(subject instanceof Ci.nsIAbDirectory)) {
+        return;
+      }
+
+      for (let recipient of [...this.recipientsList.childNodes].filter(
+        r => r.cardDetails?.book?.dirPrefId == subject.dirPrefId
+      )) {
+        recipient.updateRecipient();
+      }
+    }
+
+    /**
+     * Loop through all available recipients and update the UI to reflect if
+     * they were saved, updated, or removed as contacts in an address book.
+     *
+     * @param {nsIAbCard} subject - The changed contact card.
+     */
+    contactUpdated(subject) {
+      if (!(subject instanceof Ci.nsIAbCard)) {
+        // Bail out if this is not a valid Address Book Card object.
+        return;
+      }
+
+      if (!subject.isMailList && !subject.emailAddresses.length) {
+        // Bail out if we don't have any addresses to match against.
+        return;
+      }
+
+      let addresses = subject.emailAddresses;
+      for (let recipient of [...this.recipientsList.childNodes].filter(
+        r => r.emailAddress && addresses.includes(r.emailAddress)
+      )) {
+        recipient.updateRecipient();
+      }
+    }
+
+    /**
+     * Add a recipient to be shown in this widget. The recipient won't be shown
+     * until the row view is built.
+     *
+     * @param {Object} recipient - The recipient element.
+     * @param {String} recipient.displayName - The recipient display name.
+     * @param {String} [recipient.emailAddress] - The recipient email address.
+     * @param {String} [recipient.fullAddress] - The recipient full address.
+     */
+    addRecipient(recipient) {
+      this.#recipients.push(recipient);
+    }
+
+    buildView() {
+      this.#maxLinesBeforeMore = Services.prefs.getIntPref(
+        "mailnews.headers.show_n_lines_before_more"
+      );
+      let showAllHeaders =
+        this.#maxLinesBeforeMore < 1 ||
+        Services.prefs.getIntPref("mail.show_headers") ==
+          Ci.nsMimeHeaderDisplayTypes.AllHeaders ||
+        this.dataset.showAll == "true";
+      this.buildRecipients(showAllHeaders);
+    }
+
+    buildRecipients(showAllHeaders) {
+      this.recipientsList.replaceChildren();
+      gMessageHeader.toggleScrollableHeader(showAllHeaders);
+
+      // Store the available width of the entire row.
+      // FIXME! The size of the rows can variate depending on when adjacent
+      // elements are generated (e.g.: TO row + date row), therefore this size
+      // is not always accurate when viewing the first email. We should defer
+      // the generation of the multi recipient rows only after all the other
+      // headers have been populated.
+      let availableWidth = !showAllHeaders
+        ? this.recipientsList.getBoundingClientRect().width
+        : 0;
+
+      // Track the space occupied by recipients per row. Every time we exceed
+      // the available space of a single row, we reset this value.
+      let currentRowWidth = 0;
+      // Track how many rows are being populated by recipients.
+      let rows = 1;
+      for (let [count, recipient] of this.#recipients.entries()) {
+        let li = document.createElement("li", { is: "header-recipient" });
+        // Append the element to the DOM to trigger the connectedCallback.
+        this.recipientsList.appendChild(li);
+        li.dataset.headerName = this.dataset.headerName;
+        li.recipient = recipient;
+        // Set a proper accessible label by combining the row label and the
+        // full address of the recipient.
+        li.setAttribute(
+          "aria-label",
+          `${this.heading.textContent} ${li.fullAddress}`
+        );
+
+        // Bail out if we need to show all elements.
+        if (showAllHeaders) {
+          continue;
+        }
+
+        // Keep track of how much space our recipients are occupying.
+        let width = li.getBoundingClientRect().width;
+        // FIXME! If we have more than one recipient, we add a comma as pseudo
+        // element after the previous element. Account for that by adding an
+        // arbitrary 30px size to simulate extra characters space. This is a bit
+        // of an extreme sizing as it's almost as large as the more button, but
+        // it's necessary to make sure we never encounter that scenario.
+        if (count > 0) {
+          width += 30;
+        }
+        currentRowWidth += width;
+
+        if (currentRowWidth <= availableWidth) {
+          continue;
+        }
+
+        // If the recipients available in the current row exceed the
+        // available space, increase the row count and set the value of the
+        // last added list item to the next row width counter.
+        if (rows < this.#maxLinesBeforeMore) {
+          rows++;
+          currentRowWidth = width;
+          continue;
+        }
+
+        // Append the "more" button inside a list item to be properly handled
+        // as an inline element of the recipients list UI.
+        let buttonLi = document.createElement("li");
+        buttonLi.appendChild(this.moreButton);
+        this.recipientsList.appendChild(buttonLi);
+        currentRowWidth += buttonLi.getBoundingClientRect().width;
+
+        // Reverse loop through the added list item and remove them until
+        // they all fit in the current row alongside the "more" button.
+        for (; count && currentRowWidth > availableWidth; count--) {
+          let toRemove = this.recipientsList.childNodes[count];
+          currentRowWidth -= toRemove.getBoundingClientRect().width;
+          toRemove.remove();
+        }
+
+        // Skip the "more" button, which is present if we reached this stage.
+        let lastRecipientIndex = this.recipientsList.childNodes.length - 2;
+        // Add a unique class to the last visible recipient to remove the
+        // comma separator added via pseudo element.
+        this.recipientsList.childNodes[lastRecipientIndex].classList.add(
+          "last-before-button"
+        );
+
+        break;
+      }
+    }
+
+    /**
+     * Show all recipients available in this widget.
+     */
+    showAllRecipients() {
+      this.buildRecipients(true);
+    }
+
+    /**
+     * Empty the widget.
+     */
+    clear() {
+      this.#recipients = [];
+      this.recipientsList.replaceChildren();
+    }
+  }
+  customElements.define("multi-recipient-row", MultiRecipientRow, {
+    extends: "div",
+  });
+
+  class HeaderRecipient extends HTMLLIElement {
+    /**
+     * The object holding the recipient information.
+     *
+     * @type {Object}
+     * @property {String} displayName - The recipient display name.
+     * @property {String} [emailAddress] - The recipient email address.
+     * @property {String} [fullAddress] - The recipient full address.
+     */
+    #recipient = {};
+
+    /**
+     * The Card object if the recipients is saved in the address book.
+     *
+     * @type {Object}
+     * @property {?Object} book - The address book in which the contact is
+     *   saved, if we have a card.
+     * @property {?Object} card - The saved contact card, if present.
+     */
+    cardDetails = {};
+
+    connectedCallback() {
+      if (this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.classList.add("header-recipient");
+      this.tabIndex = 0;
+
+      this.email = document.createElement("span");
+      this.appendChild(this.email);
+
+      this.abIndicator = document.createElement("button");
+      this.abIndicator.classList.add(
+        "recipient-address-book-button",
+        "plain-button"
+      );
+      this.abIndicator.tabIndex = -1;
+      this.abIndicator.addEventListener("click", event => {
+        event.stopPropagation();
+        if (this.cardDetails.card) {
+          gMessageHeader.editContact(this);
+          return;
+        }
+
+        this.addToAddressBook();
+      });
+
+      let img = document.createElement("img");
+      img.src = "chrome://messenger/skin/icons/new/not-in-address-book.svg";
+      document.l10n.setAttributes(
+        img,
+        "message-header-address-not-in-address-book-icon2"
+      );
+
+      this.abIndicator.appendChild(img);
+      this.appendChild(this.abIndicator);
+
+      this.addEventListener("contextmenu", event => {
+        gMessageHeader.openEmailAddressPopup(event, this);
+      });
+      this.addEventListener("click", event => {
+        gMessageHeader.openEmailAddressPopup(event, this);
+      });
+      this.addEventListener("keypress", event => {
+        if (event.key == "Enter") {
+          gMessageHeader.openEmailAddressPopup(event, this);
+        }
+      });
+    }
+
+    set recipient(recipient) {
+      this.#recipient = recipient;
+      this.updateRecipient();
+    }
+
+    get displayName() {
+      return this.#recipient.displayName;
+    }
+
+    get emailAddress() {
+      return this.#recipient.emailAddress;
+    }
+
+    get fullAddress() {
+      return this.#recipient.fullAddress;
+    }
+
+    updateRecipient() {
+      if (!this.emailAddress) {
+        this.abIndicator.hidden = true;
+        this.email.textContent = this.displayName;
+        this.cardDetails = {};
+        return;
+      }
+
+      this.abIndicator.hidden = false;
+      this.cardDetails = DisplayNameUtils.getCardForEmail(
+        this.#recipient.emailAddress
+      );
+
+      let displayName = DisplayNameUtils.formatDisplayName(
+        this.emailAddress,
+        this.displayName,
+        this.dataset.headerName,
+        this.cardDetails.card
+      );
+
+      // Show only the display name if we have a valid card and the user wants
+      // to show a condensed header (without the full email address) for saved
+      // contacts.
+      if (gShowCondensedEmailAddresses && displayName) {
+        this.email.textContent = displayName;
+      } else {
+        this.email.textContent =
+          this.#recipient.fullAddress || this.#recipient.displayName;
+      }
+
+      let hasCard = this.cardDetails.card;
+      // Update the style of the indicator button.
+      this.abIndicator.classList.toggle("in-address-book", hasCard);
+      document.l10n.setAttributes(
+        this.abIndicator,
+        hasCard
+          ? "message-header-address-in-address-book-button"
+          : "message-header-address-not-in-address-book-button"
+      );
+      document.l10n.setAttributes(
+        this.abIndicator.querySelector("img"),
+        hasCard
+          ? "message-header-address-in-address-book-icon2"
+          : "message-header-address-not-in-address-book-icon2"
+      );
+    }
+
+    addToAddressBook() {
+      let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+        Ci.nsIAbCard
+      );
+      card.displayName = this.#recipient.displayName;
+      card.primaryEmail = this.#recipient.emailAddress;
+
+      let addressBook = MailServices.ab.getDirectory(
+        "jsaddrbook://abook.sqlite"
+      );
+      addressBook.addCard(card);
+    }
+  }
+  customElements.define("header-recipient", HeaderRecipient, {
+    extends: "li",
+  });
+
+  class SimpleHeaderRow extends HTMLDivElement {
+    constructor() {
+      super();
+
+      this.addEventListener("contextmenu", event => {
+        gMessageHeader.openCopyPopup(event, this);
+      });
+    }
+
+    connectedCallback() {
+      if (this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.heading = document.createElement("span");
+      this.heading.id = `${this.dataset.headerName}Heading`;
+      let sep = document.createElement("span");
+      sep.classList.add("screen-reader-only");
+      sep.setAttribute("data-l10n-name", "field-separator");
+      this.heading.appendChild(sep);
+      this.heading.hidden = true;
+
+      if (
+        ["organization", "subject", "date", "user-agent"].includes(
+          this.dataset.headerName
+        )
+      ) {
+        // message-header-organization-field
+        // message-header-subject-field
+        // message-header-date-field
+        // message-header-user-agent-field
+        document.l10n.setAttributes(
+          this.heading,
+          `message-header-${this.dataset.headerName}-field`
+        );
+      } else {
+        // If this simple row is used by an autogenerated custom header,
+        // use directly that header value as label.
+        this.heading.textContent = this.dataset.headerName;
+      }
+      this.appendChild(this.heading);
+
+      this.classList.add("header-row");
+      this.tabIndex = 0;
+
+      this.value = document.createElement("span");
+      this.value.id = `${this.dataset.headerName}Value`;
+      this.appendChild(this.value);
+
+      this.setAttribute(
+        "aria-labelledby",
+        `${this.heading.id} ${this.value.id}`
+      );
+    }
+
+    /**
+     * Set the text content for this row.
+     *
+     * @param {string} val - The content string to be added to this row.
+     */
+    set headerValue(val) {
+      this.value.textContent = val;
+    }
+  }
+  customElements.define("simple-header-row", SimpleHeaderRow, {
+    extends: "div",
+  });
+
+  class HeaderNewsgroupsRow extends HTMLDivElement {
+    /**
+     * The array of all the newsgroups that need to be shown in this row.
+     *
+     * @type {Array<Object>}
+     */
+    #newsgroups = [];
+
+    connectedCallback() {
+      if (this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.classList.add("header-newsgroups-row");
+
+      this.heading = document.createElement("span");
+      this.heading.id = `${this.dataset.headerName}Heading`;
+      let sep = document.createElement("span");
+      sep.classList.add("screen-reader-only");
+      sep.setAttribute("data-l10n-name", "field-separator");
+      this.heading.appendChild(sep);
+      this.heading.hidden = true;
+      document.l10n.setAttributes(
+        this.heading,
+        "message-header-newsgroups-field"
+      );
+      this.appendChild(this.heading);
+
+      this.newsgroupsList = document.createElement("ol");
+      this.newsgroupsList.classList.add("newsgroups-list");
+      this.appendChild(this.newsgroupsList);
+    }
+
+    addNewsgroup(newsgroup) {
+      this.#newsgroups.push(newsgroup);
+    }
+
+    buildView() {
+      this.newsgroupsList.replaceChildren();
+      for (let newsgroup of this.#newsgroups) {
+        let li = document.createElement("li", { is: "header-newsgroup" });
+        this.newsgroupsList.appendChild(li);
+        li.textContent = newsgroup;
+        // Set a proper accessible label by combining the row label and the
+        // newsgroup name.
+        li.setAttribute(
+          "aria-label",
+          `${this.heading.textContent} ${li.textContent}`
+        );
+      }
+    }
+
+    clear() {
+      this.#newsgroups = [];
+      this.newsgroupsList.replaceChildren();
+    }
+  }
+  customElements.define("header-newsgroups-row", HeaderNewsgroupsRow, {
+    extends: "div",
+  });
+
+  class HeaderNewsgroup extends HTMLLIElement {
+    connectedCallback() {
+      if (this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      this.classList.add("header-newsgroup");
+      this.tabIndex = 0;
+
+      this.addEventListener("contextmenu", event => {
+        gMessageHeader.openNewsgroupPopup(event, this);
+      });
+      this.addEventListener("click", event => {
+        gMessageHeader.openNewsgroupPopup(event, this);
+      });
+      this.addEventListener("keypress", event => {
+        if (event.key == "Enter") {
+          gMessageHeader.openNewsgroupPopup(event, this);
+        }
+      });
+    }
+  }
+  customElements.define("header-newsgroup", HeaderNewsgroup, {
+    extends: "li",
+  });
+}
--- a/mail/base/content/widgets/mailWidgets.js
+++ b/mail/base/content/widgets/mailWidgets.js
@@ -7,19 +7,17 @@
 
 /* global MozElements */
 /* global MozXULElement */
 /* global openUILink */
 /* global MessageIdClick */
 /* global EditContact */
 /* global AddContact */
 /* global gFolderDisplay */
-/* global UpdateEmailNodeDetails */
 /* global PluralForm */
-/* global UpdateExtraAddressProcessing */
 /* global onRecipientsChanged */
 
 // Wrap in a block to prevent leaking to window scope.
 {
   const { Services } = ChromeUtils.import(
     "resource://gre/modules/Services.jsm"
   );
   const { MailServices } = ChromeUtils.import(
@@ -157,55 +155,16 @@
 
         this.appendChild(label);
         setHeaderAriaLabel(label, tagName);
       }
     }
   }
   customElements.define("mail-tagfield", MozMailHeaderfieldTags);
 
-  class MozMailNewsgroup extends MozXULElement {
-    connectedCallback() {
-      this.classList.add("emailDisplayButton");
-      this.setAttribute("context", "newsgroupPopup");
-      this.setAttribute("popup", "newsgroupPopup");
-      let newsgroup = this.getAttribute("newsgroup");
-      this.textContent = newsgroup;
-      setHeaderAriaLabel(this, newsgroup);
-    }
-  }
-  customElements.define("mail-newsgroup", MozMailNewsgroup);
-
-  class MozMailNewsgroupsHeaderfield extends MozXULElement {
-    connectedCallback() {
-      this.mNewsgroups = [];
-    }
-
-    addNewsgroupView(aNewsgroup) {
-      this.mNewsgroups.push(aNewsgroup);
-    }
-
-    buildViews() {
-      for (let newsgroup of this.mNewsgroups) {
-        const newNode = document.createXULElement("mail-newsgroup");
-        newNode.setAttribute("newsgroup", newsgroup);
-        this.appendChild(newNode);
-      }
-    }
-
-    clearHeaderValues() {
-      this.mNewsgroups = [];
-      this.replaceChildren();
-    }
-  }
-  customElements.define(
-    "mail-newsgroups-headerfield",
-    MozMailNewsgroupsHeaderfield
-  );
-
   class MozMailMessageid extends MozXULElement {
     static get observedAttributes() {
       return ["label"];
     }
 
     constructor() {
       super();
       this.addEventListener("click", event => {
@@ -367,165 +326,16 @@
       }
     }
   }
   customElements.define(
     "mail-messageids-headerfield",
     MozMailMessageidsHeaderfield
   );
 
-  class MozMailEmailaddress extends MozXULElement {
-    static get observedAttributes() {
-      return ["label", "crop"];
-    }
-
-    connectedCallback() {
-      if (this.hasChildNodes() || this.delayConnectedCallback()) {
-        return;
-      }
-      this.classList.add("emailDisplayButton");
-      this.setAttribute("context", "emailAddressPopup");
-      // FIXME: popup is not accessible to keyboard users.
-      this.setAttribute("popup", "emailAddressPopup");
-      this.setAttribute("align", "center");
-
-      const label = document.createXULElement("label");
-      label.classList.add("emaillabel");
-
-      // FIXME: The star button uses "title" to describe its action, but the
-      // tooltip is not currently accessible to keyboard users and doesn't
-      // appear as a node in the accessibility tree.
-      this.starButton = document.createElement("button");
-      this.starButton.classList.add("plain-button", "email-action-button");
-      this.starButton.setAttribute("contextmenu", "emailAddressPopup");
-      this.starIcon = document.createElement("img");
-      this.starIcon.classList.add("emailStar");
-      this.starButton.appendChild(this.starIcon);
-
-      this.starButton.addEventListener("mousedown", event => {
-        // Don't trigger popup.
-        event.preventDefault();
-      });
-      this.starButton.addEventListener("click", this.onClickStar.bind(this));
-
-      this.appendChild(label);
-      this.appendChild(this.starButton);
-
-      this.createdStarButton = true;
-
-      this._updateStarButton();
-      this._update();
-    }
-
-    onClickStar(event) {
-      // Only care about left-click events
-      if (event.button != 0) {
-        return;
-      }
-
-      // FIXME: both methods use properties set outside of this class in
-      // msgHdrView.js. Would be cleaner if the logic could be brought within
-      // this class since they are currently quite interdependent.
-      if (this.hasCard) {
-        EditContact(this);
-      } else {
-        AddContact(this);
-      }
-    }
-
-    _updateStarButton() {
-      let src;
-      let title;
-      if (this.hasCard) {
-        src = "chrome://messenger/skin/icons/starred.svg";
-        // Set the alt text.
-        document.l10n.setAttributes(
-          this.starIcon,
-          "message-header-address-in-address-book-icon"
-        );
-        title = document.getElementById("editContactItem").label;
-      } else {
-        src = "chrome://messenger/skin/icons/star.svg";
-        // Set the alt text.
-        document.l10n.setAttributes(
-          this.starIcon,
-          "message-header-address-not-in-address-book-icon"
-        );
-        title = document.getElementById("addToAddressBookItem").label;
-      }
-      this.starIcon.setAttribute("src", src);
-      this.starIcon.classList.toggle("starredFill", this.hasCard);
-      this.starButton.setAttribute("title", title);
-    }
-
-    /**
-     * Set the address book action for the star button depending on whether the
-     * shown address exists in the address book.
-     *
-     * @param {boolean} hasCard - Whether the shown address is already in the
-     *   address book.
-     */
-    setAddressBookState(hasCard) {
-      if (hasCard === this.hasCard) {
-        return;
-      }
-      this.hasCard = hasCard;
-      if (this.createdStarButton) {
-        this._updateStarButton();
-      }
-    }
-
-    attributeChangedCallback() {
-      this._update();
-    }
-
-    _update() {
-      if (!this.isConnectedAndReady) {
-        return;
-      }
-      const emailLabel = this.querySelector(".emaillabel");
-
-      this._updateNodeAttributes(emailLabel, "crop");
-      this._updateNodeAttributes(emailLabel, "value", "label");
-      setHeaderAriaLabel(this, this.getAttribute("label"));
-    }
-
-    _updateNodeAttributes(attrNode, attr, mappedAttr) {
-      mappedAttr = mappedAttr || attr;
-
-      if (
-        this.hasAttribute(mappedAttr) &&
-        this.getAttribute(mappedAttr) != null
-      ) {
-        attrNode.setAttribute(attr, this.getAttribute(mappedAttr));
-      } else {
-        attrNode.removeAttribute(attr);
-      }
-    }
-  }
-  customElements.define("mail-emailaddress", MozMailEmailaddress);
-
-  class MozMailEmailheaderfield extends MozXULElement {
-    connectedCallback() {
-      if (this.hasChildNodes() || this.delayConnectedCallback()) {
-        return;
-      }
-      this._mailEmailAddress = document.createXULElement("mail-emailaddress");
-      this._mailEmailAddress.classList.add("headerValue");
-      this._mailEmailAddress.setAttribute("containsEmail", "true");
-
-      this.appendChild(this._mailEmailAddress);
-    }
-
-    get emailAddressNode() {
-      return this._mailEmailAddress;
-    }
-  }
-  customElements.define("mail-emailheaderfield", MozMailEmailheaderfield);
-
   // NOTE: Icon column headers should have their "label" attribute set to
   // describe the icon for the accessibility tree.
   //
   // NOTE: Ideally we could listen for the "alt" attribute and pass it on to the
   // contained <img>, but the accessibility tree only seems to read the "label"
   // for a <treecol>, and ignores the alt text.
   class MozTreecolImage extends customElements.get("treecol") {
     static get observedAttributes() {
@@ -946,299 +756,16 @@
     ]);
 
     customElements.define("menulist-editable", MozMenulistEditable, {
       extends: "menulist",
     });
   }
 
   /**
-   * The MozMailMultiEmailheaderfield widget shows multiple emails. It collapses
-   * long rows and allows toggling the full view open. This widget is typically
-   * used in the message header pane to show addresses for To, Cc, Bcc, and any
-   * other addressing type header that can contain more than one mailbox.
-   *
-   * extends {MozXULElement}
-   */
-  class MozMailMultiEmailheaderfield extends MozXULElement {
-    constructor() {
-      super();
-
-      // The number of lines of addresses we will display before adding a (more)
-      // indicator to the widget. This can be increased using the preference
-      // mailnews.headers.show_n_lines_before_more.
-      this.maxLinesBeforeMore = 1;
-
-      // The maximum number of addresses in the more button tooltip text.
-      this.tooltipLength = 20;
-
-      this.addresses = [];
-    }
-
-    connectedCallback() {
-      if (this.delayConnectedCallback() || this.hasChildNodes()) {
-        return;
-      }
-
-      this.classList.add("message-header-multi-field");
-
-      this.longEmailAddresses = document.createXULElement("hbox");
-      this.longEmailAddresses.classList.add("headerValueBox");
-      this.longEmailAddresses.setAttribute("flex", "1");
-      this.longEmailAddresses.setAttribute("align", "baseline");
-
-      this.emailAddresses = document.createXULElement("description");
-      this.emailAddresses.classList.add("headerValue");
-      this.emailAddresses.setAttribute("containsEmail", "true");
-      this.emailAddresses.setAttribute("flex", "1");
-      this.emailAddresses.setAttribute("orient", "vertical");
-      this.emailAddresses.setAttribute("pack", "start");
-
-      this.more = document.createXULElement("label");
-      this.more.classList.add("moreIndicator");
-      this.more.addEventListener("click", this.toggleWrap.bind(this));
-      this.more.setAttribute("collapsed", "true");
-
-      this.longEmailAddresses.appendChild(this.emailAddresses);
-      this.appendChild(this.longEmailAddresses);
-      this.appendChild(this.more);
-    }
-
-    set maxAddressesInMoreTooltipValue(val) {
-      this.tooltipLength = val;
-    }
-
-    get maxAddressesInMoreTooltipValue() {
-      return this.tooltipLength;
-    }
-
-    /**
-     * Add an address to be shown in this widget.
-     *
-     * @param {Object} address                address to be added
-     * @param {String} address.displayName    display name of the address
-     * @param {String} address.emailAddress   email address of the address
-     * @param {String} address.fullAddress    full address of the address
-     */
-    addAddressView(address) {
-      this.addresses.push(address);
-    }
-
-    /**
-     * Method used to reset addresses shown by this widget.
-     */
-    resetAddressView() {
-      this.addresses.length = 0;
-    }
-
-    /**
-     * Private method used to set properties on an address node.
-     */
-    _updateEmailAddressNode(emailNode, address) {
-      emailNode.setAttribute(
-        "label",
-        address.fullAddress || address.displayName || ""
-      );
-      emailNode.removeAttribute("tooltiptext");
-      emailNode.setAttribute("emailAddress", address.emailAddress || "");
-      emailNode.setAttribute("fullAddress", address.fullAddress || "");
-      emailNode.setAttribute("displayName", address.displayName || "");
-
-      if ("UpdateEmailNodeDetails" in top && address.emailAddress) {
-        UpdateEmailNodeDetails(address.emailAddress, emailNode);
-      }
-    }
-
-    /**
-     * Private method used to create email address nodes for either our short or
-     * long view.
-     *
-     * @param {boolean} all - If false, show only a few addresses + "more".
-     * @return {integer} The number of addresses we have put into the list.
-     */
-    _fillAddressesNode(all) {
-      this.emailAddresses.replaceChildren();
-
-      // This ensures that the worst-case "n more" width is considered.
-      this.setNMore(this.addresses.length);
-      this.more.collapsed = false;
-      let availableWidth =
-        this.emailAddresses.clientWidth - this.more.clientWidth;
-
-      // Add addresses until we're done, or we overflow the allowed lines.
-      let addrCount = 0;
-      for (let i = 0, line = 0, lineWidth = 0; i < this.addresses.length; i++) {
-        let newAddressNode = document.createXULElement("mail-emailaddress");
-        // Stash the headerName somewhere that UpdateEmailNodeDetails will be
-        // able to find it.
-        newAddressNode.setAttribute("headerName", this.headerName);
-
-        this._updateEmailAddressNode(newAddressNode, this.addresses[i]);
-        newAddressNode = this.emailAddresses.appendChild(newAddressNode);
-        addrCount++;
-
-        if (all) {
-          continue;
-        }
-
-        // Reading .clientWidth triggers an expensive reflow, so only do it
-        // when necessary for possible early loop exit to display (X more).
-        // Calculate width and lines.
-        // <http://www.w3.org/TR/cssom-view/#client-attributes>
-        // <https://developer.mozilla.org/en/Determining_the_dimensions_of_elements>
-        let newLineWidth = newAddressNode.clientWidth;
-        lineWidth += newLineWidth;
-        let overLineWidth = lineWidth - availableWidth;
-        if (overLineWidth > 0 && i > 0) {
-          line++;
-          lineWidth = newLineWidth;
-        }
-
-        if (line >= this.maxLinesBeforeMore) {
-          // Hide the last node spanning into the additional line (n>1)
-          // also hide it if <50px left after sliding the address (n=1)
-          // or if the last address would be truncated without "more"
-          if (
-            this.maxLinesBeforeMore > 1 ||
-            (i + 1 == this.addresses.length && overLineWidth > 50) ||
-            newLineWidth - overLineWidth < 50
-          ) {
-            this.emailAddresses.lastElementChild.remove(); // last addr
-            addrCount--;
-          }
-          break;
-        }
-      }
-
-      this.more.collapsed = all || addrCount == this.addresses.length;
-
-      // If there are addresses we're not showing, set up the (N more) widget.
-      if (!this.more.collapsed) {
-        let remainingAddresses = this.addresses.length - addrCount;
-        this.setNMore(remainingAddresses);
-        this.setNMoreTooltiptext(this.addresses.slice(-remainingAddresses));
-      }
-
-      return addrCount; // number of addresses shown
-    }
-
-    /**
-     * Public method to build the DOM nodes for display, to be called after all
-     * the addresses have been added to the widget. It uses _fillAddressesNode
-     * to display at most maxLinesBeforeMore lines of addresses plus the (more)
-     * widget which can be clicked to reveal the rest.
-     */
-    buildViews() {
-      this.maxLinesBeforeMore = Services.prefs.getIntPref(
-        "mailnews.headers.show_n_lines_before_more"
-      );
-      let headerchoice = Services.prefs.getIntPref("mail.show_headers");
-      let showAllHeaders =
-        this.maxLinesBeforeMore < 1 ||
-        headerchoice == Ci.nsMimeHeaderDisplayTypes.AllHeaders;
-      this._fillAddressesNode(showAllHeaders);
-    }
-
-    /**
-     * Set up a (N more) widget which can be clicked to reveal the rest.
-     * @param {integer} number - the number of addresses "more" will reveal
-     */
-    setNMore(number) {
-      // Figure out the right plural for the language we're using
-      let words = document
-        .getElementById("bundle_messenger")
-        .getString("headerMoreAddrs");
-      let moreForm = PluralForm.get(number, words).replace("#1", number);
-
-      // Set the "n more" text node.
-      this.more.setAttribute("value", moreForm);
-      // Remove the tooltip text of the more widget.
-      this.more.removeAttribute("tooltiptext");
-    }
-
-    /**
-     * Populate the tooltiptext of the (N more) widget with hidden email addresses.
-     */
-    setNMoreTooltiptext(addresses) {
-      if (addresses.length == 0) {
-        return;
-      }
-
-      let tttArray = [];
-      for (let i = 0; i < addresses.length && i < this.tooltipLength; i++) {
-        tttArray.push(addresses[i].fullAddress);
-      }
-      let ttText = tttArray.join(", ");
-
-      let remainingAddresses = addresses.length - tttArray.length;
-      // Not all missing addresses fit in the tooltip.
-      if (remainingAddresses > 0) {
-        // Figure out the right plural for the language we're using,
-        let words = document
-          .getElementById("bundle_messenger")
-          .getString("headerMoreAddrsTooltip");
-        let moreForm = PluralForm.get(remainingAddresses, words).replace(
-          "#1",
-          remainingAddresses
-        );
-        ttText += moreForm;
-      }
-      this.more.setAttribute("tooltiptext", ttText);
-    }
-
-    /**
-     * Updates the nodes of this field with a call to UpdateExtraAddressProcessing. The parameters are
-     * optional fields that can contain extra information to be passed to
-     * UpdateExtraAddressProcessing, the implementation of that function should be checked to
-     * determine what it requires
-     */
-    updateExtraAddressProcessing(param1, param2, param3) {
-      customElements.upgrade(this);
-      if (UpdateExtraAddressProcessing) {
-        const children = this.emailAddresses.children;
-        for (let i = 0; i < this.addresses.length; i++) {
-          UpdateExtraAddressProcessing(
-            this.addresses[i],
-            children[i],
-            param1,
-            param2,
-            param3
-          );
-        }
-      }
-    }
-
-    /**
-     * Called when the (more) indicator has been clicked on; re-renders the
-     * widget with all the addresses.
-     */
-    toggleWrap() {
-      // Fake the "All Headers" mode, so that we get a scroll bar.
-      // Will be reset when a new message loads.
-      document
-        .getElementById("messageHeader")
-        .setAttribute("show_header_mode", "all");
-
-      // Re-render the node, this time with all the addresses.
-      this._fillAddressesNode(true);
-      // This attribute will be reinit in the 'UpdateExpandedMessageHeaders()' method.
-    }
-
-    clearHeaderValues() {
-      // Clear out our local state.
-      this.addresses = [];
-      this.emailAddresses.replaceChildren();
-    }
-  }
-  customElements.define(
-    "mail-multi-emailheaderfield",
-    MozMailMultiEmailheaderfield
-  );
-
-  /**
    * The MozAttachmentlist widget lists attachments for a mail. This is typically used to show
    * attachments while writing a new mail as well as when reading mails.
    *
    * @extends {MozElements.RichListBox}
    */
   class MozAttachmentlist extends MozElements.RichListBox {
     constructor() {
       super();
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -96,16 +96,17 @@ messenger.jar:
     content/messenger/viewSource.js                 (content/viewSource.js)
 *   content/messenger/viewSource.xhtml              (content/viewSource.xhtml)
     content/messenger/viewZoomOverlay.js            (content/viewZoomOverlay.js)
     content/messenger/button-menu-button.js         (content/widgets/button-menu-button.js)
     content/messenger/customizable-toolbar.js       (content/widgets/customizable-toolbar.js)
     content/messenger/foldersummary.js              (content/widgets/foldersummary.js)
     content/messenger/gloda-autocomplete-input.js   (content/widgets/gloda-autocomplete-input.js)
     content/messenger/glodaFacet.js                 (content/widgets/glodaFacet.js)
+    content/messenger/header-fields.js              (content/widgets/header-fields.js)
     content/messenger/mailWidgets.js                (content/widgets/mailWidgets.js)
     content/messenger/pane-splitter.js              (content/widgets/pane-splitter.js)
     content/messenger/statuspanel.js                (content/widgets/statuspanel.js)
     content/messenger/tabmail-tab.js                (content/widgets/tabmail-tab.js)
     content/messenger/tabmail-tabs.js               (content/widgets/tabmail-tabs.js)
     content/messenger/toolbarbutton-menu-button.js  (content/widgets/toolbarbutton-menu-button.js)
     content/messenger/tree-listbox.js               (content/widgets/tree-listbox.js)
 
--- a/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
+++ b/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
@@ -2723,25 +2723,21 @@ Enigmail.msg = {
       let info = await document.l10n.formatValue("decrypt-and-copy-failures", {
         failures,
         total,
       });
       Services.prompt.alert(null, document.title, info);
     }
   },
 
-  async searchKeysOnInternet(aHeaderNode) {
-    let address = aHeaderNode
-      .closest("mail-emailaddress")
-      .getAttribute("emailAddress");
-
+  async searchKeysOnInternet(event) {
     return KeyLookupHelper.lookupAndImportByEmail(
       "interactive-import",
       window,
-      address,
+      event.currentTarget.parentNode.headerField?.emailAddress,
       true
     );
   },
 
   importKeyFromKeyserver() {
     var pubKeyId = "0x" + Enigmail.msg.securityInfo.keyId;
     var inputObj = {
       searchList: [pubKeyId],
--- a/mail/extensions/smime/content/msgHdrViewSMIMEOverlay.js
+++ b/mail/extensions/smime/content/msgHdrViewSMIMEOverlay.js
@@ -225,17 +225,17 @@ var smimeHeaderSink = {
       .extractHeaderAddressMailboxes(currentHeaderData.from.headerValue)
       .split(",");
     for (let i = 0; i < fromMailboxes.length; i++) {
       if (gSignerCert.containsEmailAddress(fromMailboxes[i])) {
         return; // It's signed by a From. Nothing more to do
       }
     }
 
-    let senderInfo = { name: "sender", outputFunction: OutputEmailAddresses };
+    let senderInfo = { name: "sender", outputFunction: outputEmailAddresses };
     let senderEntry = new MsgHeaderEntry("expanded", senderInfo);
 
     gExpandedHeaderView[senderInfo.name] = senderEntry;
     UpdateExpandedMessageHeaders();
   },
 
   encryptionStatus(
     aNestingLevel,
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/messenger/messageheader/headerFields.ftl
@@ -0,0 +1,58 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## Message headers
+
+# The field-separator is for screen readers to separate the field name from the field value.
+
+message-header-to-field = To<span data-l10n-name="field-separator">:</span>
+
+message-header-from-field = From<span data-l10n-name="field-separator">:</span>
+
+message-header-sender-field = Sender<span data-l10n-name="field-separator">:</span>
+
+message-header-author-field = Author<span data-l10n-name="field-separator">:</span>
+
+message-header-organization-field = Organization<span data-l10n-name="field-separator">:</span>
+
+message-header-reply-to-field = Reply to<span data-l10n-name="field-separator">:</span>
+
+message-header-subject-field = Subject<span data-l10n-name="field-separator">:</span>
+
+message-header-cc-field = Cc<span data-l10n-name="field-separator">:</span>
+
+message-header-bcc-field = Bcc<span data-l10n-name="field-separator">:</span>
+
+message-header-newsgroups-field = Newsgroups<span data-l10n-name="field-separator">:</span>
+
+message-header-followup-to-field = Followup to<span data-l10n-name="field-separator">:</span>
+
+message-header-tags-field = Tags<span data-l10n-name="field-separator">:</span>
+
+message-header-date-field = Date<span data-l10n-name="field-separator">:</span>
+
+message-header-user-agent-field = User agent<span data-l10n-name="field-separator">:</span>
+
+message-header-references-field = References<span data-l10n-name="field-separator">:</span>
+
+message-header-message-id-field = Message ID<span data-l10n-name="field-separator">:</span>
+
+message-header-in-reply-to-field = In reply to<span data-l10n-name="field-separator">:</span>
+
+message-header-website-field = Website<span data-l10n-name="field-separator">:</span>
+
+message-header-address-in-address-book-icon2 =
+  .alt = In the Address Book
+
+message-header-address-not-in-address-book-icon2 =
+  .alt = Not in the Address Book
+
+message-header-address-not-in-address-book-button =
+    .title = Save this address in the Address Book
+
+message-header-address-in-address-book-button =
+    .title = Edit contact
+
+message-header-field-show-more = More
+    .title = Show all recipients
--- a/mail/locales/en-US/messenger/messenger.ftl
+++ b/mail/locales/en-US/messenger/messenger.ftl
@@ -173,24 +173,16 @@ message-header-large-subject =
 
 toolbar-context-menu-manage-extension =
     .label = Manage Extension
     .accesskey = E
 toolbar-context-menu-remove-extension =
     .label = Remove Extension
     .accesskey = v
 
-## Message headers
-
-message-header-address-in-address-book-icon =
-  .alt = Address is in the Address Book
-
-message-header-address-not-in-address-book-icon =
-  .alt = Address is not in the Address Book
-
 ## Add-on removal warning
 
 # Variables:
 #  $name (String): The name of the addon that will be removed.
 addon-removal-title = Remove { $name }?
 addon-removal-confirmation-button = Remove
 addon-removal-confirmation-message = Remove { $name } as well as its configuration and data from { -brand-short-name }?
 
--- a/mail/test/browser/message-header/browser_messageHeader.js
+++ b/mail/test/browser/message-header/browser_messageHeader.js
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
- * Test functionality in the message header, e.g. tagging, contact editing,
- * the more button ...
+ * Test functionality in the message header,
  */
 
 "use strict";
 
 var {
   create_address_book,
   create_mailing_list,
   ensure_no_card_exists,
@@ -45,1152 +44,819 @@ var { resize_to } = ChromeUtils.import(
   "resource://testing-common/mozmill/WindowHelpers.jsm"
 );
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { MailServices } = ChromeUtils.import(
   "resource:///modules/MailServices.jsm"
 );
 
-var folder, folderMore;
+const LINES_PREF = "mailnews.headers.show_n_lines_before_more";
+
+// Used to get the accessible object for a DOM node.
+const gAccService = Cc["@mozilla.org/accessibilityService;1"].getService(
+  Ci.nsIAccessibilityService
+);
+
+var folder;
+var folderMore;
 var gInterestingMessage;
 
 add_setup(async function() {
   folder = await create_folder("MessageWindowA");
   folderMore = await create_folder("MesageHeaderMoreButton");
 
-  // create a message that has the interesting headers that commonly
-  // show up in the message header pane for testing
+  // Create a message that has the interesting headers that commonly shows up in
+  // the message header pane for testing.
   gInterestingMessage = create_message({
-    cc: msgGen.makeNamesAndAddresses(20), // YYY
+    cc: msgGen.makeNamesAndAddresses(20),
     subject:
       "This is a really, really, really, really, really, really, really, really, long subject.",
     clobberHeaders: {
       Newsgroups: "alt.test",
       "Reply-To": "J. Doe <j.doe@momo.invalid>",
       "Content-Base": "http://example.com/",
       Bcc: "Richard Roe <richard.roe@momo.invalid>",
     },
   });
 
   await add_message_to_folder([folder], gInterestingMessage);
 
-  // create a message that has more to and cc addresses than visible in the
-  // tooltip text of the more button
+  // Create a message that has multiple to and cc addresses.
   let msgMore1 = create_message({
     to: msgGen.makeNamesAndAddresses(40),
     cc: msgGen.makeNamesAndAddresses(40),
   });
   await add_message_to_folder([folderMore], msgMore1);
 
-  // create a message that has more to and cc addresses than visible in the
-  // header
+  // Create a message that has multiple to and cc addresses.
   let msgMore2 = create_message({
     to: msgGen.makeNamesAndAddresses(20),
     cc: msgGen.makeNamesAndAddresses(20),
   });
   await add_message_to_folder([folderMore], msgMore2);
 
-  // create a message that has boring headers to be able to switch to and
-  // back from, to force the more button to collapse again.
+  // Create a regular message with one recipient.
   let msg = create_message();
   await add_message_to_folder([folder], msg);
 
   // Some of these tests critically depends on the window width, collapse
-  // everything that might be in the way
-  collapse_panes(mc.e("folderpane_splitter"), true);
-  collapse_panes(mc.e("tabmail-container"), true);
+  // everything that might be in the way.
+  collapse_panes(document.getElementById("folderpane_splitter"), true);
+  collapse_panes(document.getElementById("tabmail-container"), true);
 
   // Disable animations on the panel, so that we don't have to deal with
   // async openings.
-  let contactPanel = mc.e("editContactPanel");
-  contactPanel.setAttribute("animate", false);
+  document.getElementById("editContactPanel").setAttribute("animate", false);
 });
 
 registerCleanupFunction(function() {
-  let contactPanel = mc.e("editContactPanel");
-  contactPanel.removeAttribute("animate");
+  // Delete created folder.
+  folder.deleteSelf(null);
+  folderMore.deleteSelf(null);
+
+  // Restore animation to the contact panel.
+  document.getElementById("editContactPanel").removeAttribute("animate");
 
   // Now restore the panes we hid in setup.
-  collapse_panes(mc.e("folderpane_splitter"), false);
-  collapse_panes(mc.e("tabmail-container"), false);
+  collapse_panes(document.getElementById("folderpane_splitter"), false);
+  collapse_panes(document.getElementById("tabmail-container"), false);
 });
 
 /**
  * Helper function that takes an array of mail-emailaddress elements and
  * returns the last one in the list that is not hidden. Returns null if no
  * such element exists.
  *
- * @param aAddrs an array of mail-emailaddress elements.
+ * @param {HTMLOListElement} recipientsList - The list element containing all
+ *   recipient addresses.
  */
-function get_last_visible_address(aAddrs) {
-  for (let i = aAddrs.length - 1; i >= 0; --i) {
-    if (!aAddrs[i].hidden) {
-      return aAddrs[i];
-    }
+function get_last_visible_address(recipientsList) {
+  let last = recipientsList.childNodes[recipientsList.childNodes.length - 1];
+  // Avoid returning the "more" button.
+  if (last.classList.contains("show-more-recipients")) {
+    return recipientsList.childNodes[recipientsList.childNodes.length - 2];
   }
-  return null;
+  return last;
 }
 
 add_task(function test_add_tag_with_really_long_label() {
   be_in_folder(folder);
 
-  // select the first message, which will display it
+  // Select the first message, which will display it.
   let curMessage = select_click_row(0);
 
   assert_selected_and_displayed(mc, curMessage);
 
-  let topLabel = mc.e("expandedfromLabel");
-  let bottomLabel = mc.e("expandedsubjectLabel");
-
+  let topLabel = document.getElementById("expandedfromLabel");
+  let bottomLabel = document.getElementById("expandedsubjectLabel");
   if (topLabel.clientWidth != bottomLabel.clientWidth) {
     throw new Error(
-      "Header columns have different widths!  " +
-        topLabel.clientWidth +
-        " != " +
-        bottomLabel.clientWidth
+      `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}`
     );
   }
   let defaultWidth = topLabel.clientWidth;
 
   // Make the tags label really long.
-  let tagsLabel = mc.e("expandedtagsLabel");
+  let tagsLabel = document.getElementById("expandedtagsLabel");
   let oldTagsValue = tagsLabel.value;
   tagsLabel.value = "taaaaaaaaaaaaaaaaaags";
-
   if (topLabel.clientWidth != bottomLabel.clientWidth) {
     tagsLabel.value = oldTagsValue;
     throw new Error(
-      "Header columns have different widths!  " +
-        topLabel.clientWidth +
-        " != " +
-        bottomLabel.clientWidth
+      `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}`
     );
   }
+
   if (topLabel.clientWidth != defaultWidth) {
     tagsLabel.value = oldTagsValue;
     throw new Error(
-      "Header columns changed width!  " +
-        topLabel.clientWidth +
-        " != " +
-        defaultWidth
+      `Header columns changed width! ${topLabel.clientWidth} != ${defaultWidth}`
     );
   }
 
+  let fromRow = document.getElementById("expandedfromRow");
   // Add the first tag, and make sure that the label are the same length.
-  mc.window.document.getElementById("expandedfromRow").focus();
+  fromRow.focus();
   EventUtils.synthesizeKey("1", {});
   if (topLabel.clientWidth != bottomLabel.clientWidth) {
     tagsLabel.value = oldTagsValue;
     throw new Error(
-      "Header columns have different widths!  " +
-        topLabel.clientWidth +
-        " != " +
-        bottomLabel.clientWidth
+      `Header columns have different widths! ${topLabel.clientWidth} != ${bottomLabel.clientWidth}`
     );
   }
+
   if (topLabel.clientWidth == defaultWidth) {
     tagsLabel.value = oldTagsValue;
     throw new Error(
-      "Header columns didn't change width!  " +
-        topLabel.clientWidth +
-        " == " +
-        defaultWidth
+      `Header columns didn't change width! ${topLabel.clientWidth} == ${defaultWidth}`
     );
   }
 
   // Remove the tag and put it back so that the a11y label gets regenerated
-  // with the normal value rather than "taaaaaaaags"
+  // with the normal value rather than "taaaaaaaags".
   tagsLabel.value = oldTagsValue;
-  mc.window.document.getElementById("expandedfromRow").focus();
+  fromRow.focus();
   EventUtils.synthesizeKey("1", {});
-  mc.window.document.getElementById("expandedfromRow").focus();
+  fromRow.focus();
   EventUtils.synthesizeKey("1", {});
 });
 
 /**
- * @param headerName used for pretty-printing in exceptions
- * @param headerValueElement  Function returning the DOM element
- *                            with the data.
- * @param expectedName  Function returning the expected value of
- *                      nsIAccessible.name for the DOM element in question
+ * Data and methods for a space.
+ *
+ * @typedef {Object} HeaderInfo
+ * @property {string} name - Used for pretty-printing in exceptions.
+ * @property {function} element - A callback returning the DOM
+ *   element with the data.
+ * @property {function} expectedName - A callback returning the expected value
+ *   of the accessible name of the DOM element.
  */
-let headersToTest = [
+/**
+ * List all header rows that we want to test.
+ *
+ * @type {HeaderInfo[]}
+ */
+const headersToTest = [
   {
-    headerName: "Subject",
-    headerValueElement(mc) {
-      return mc.e("expandedsubjectBox", { class: "headerValue" });
+    name: "Subject",
+    element() {
+      return document.getElementById("expandedsubjectBox");
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedsubjectLabel").value +
-        ": " +
-        headerValueElement.textContent
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedsubjectLabel").value}: ${
+        element.value.textContent
+      }`;
     },
   },
   {
-    headerName: "Content-Base",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
+    name: "Content-Base",
+    element() {
+      return document.querySelector(
         "#expandedcontent-baseBox.headerValue.text-link.headerValueUrl"
       );
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedcontent-baseLabel").value +
-        ": " +
-        headerValueElement.textContent
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedcontent-baseLabel").value}: ${
+        element.textContent
+      }`;
     },
   },
   {
-    headerName: "From",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandedfromBox > .headerValueBox > .headerValue > mail-emailaddress.emailDisplayButton"
-      );
-    },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedfromLabel").value +
-        ": " +
-        headerValueElement.getAttribute("fullAddress")
-      );
+    name: "From",
+    element() {
+      return document.querySelector("#expandedfromBox .header-recipient");
     },
-  },
-  {
-    headerName: "To",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandedtoBox > .headerValueBox > .headerValue > mail-emailaddress.emailDisplayButton"
-      );
-    },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedtoLabel").value +
-        ": " +
-        headerValueElement.getAttribute("fullAddress")
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedfromLabel").value}: ${
+        element.fullAddress
+      }`;
     },
   },
   {
-    headerName: "Cc",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandedccBox > .headerValueBox > .headerValue > mail-emailaddress.emailDisplayButton"
-      );
+    name: "To",
+    element() {
+      return document.querySelector("#expandedtoBox .header-recipient");
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedccLabel").value +
-        ": " +
-        headerValueElement.getAttribute("fullAddress")
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedtoLabel").value}: ${
+        element.fullAddress
+      }`;
     },
   },
   {
-    headerName: "Bcc",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandedbccBox > .headerValueBox > .headerValue > mail-emailaddress.emailDisplayButton"
-      );
+    name: "Cc",
+    element() {
+      return document.querySelector("#expandedccBox .header-recipient");
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedbccLabel").value +
-        ": " +
-        headerValueElement.getAttribute("fullAddress")
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedccLabel").value}: ${
+        element.fullAddress
+      }`;
     },
   },
   {
-    headerName: "Reply-To",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandedreply-toBox > .headerValueBox > .headerValue > mail-emailaddress.emailDisplayButton"
-      );
+    name: "Bcc",
+    element() {
+      return document.querySelector("#expandedbccBox .header-recipient");
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedreply-toLabel").value +
-        ": " +
-        headerValueElement.getAttribute("fullAddress")
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedbccLabel").value}: ${
+        element.fullAddress
+      }`;
     },
   },
   {
-    headerName: "Newsgroups",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector(
-        "#expandednewsgroupsBox > mail-newsgroup.emailDisplayButton"
-      );
+    name: "Reply-To",
+    element() {
+      return document.querySelector("#expandedreply-toBox .header-recipient");
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandednewsgroupsLabel").value +
-        ": " +
-        headerValueElement.getAttribute("newsgroup")
-      );
+    expectedName(element) {
+      return `${document.getElementById("expandedreply-toLabel").value}: ${
+        element.fullAddress
+      }`;
     },
   },
   {
-    headerName: "Tags",
-    headerValueElement(mc) {
-      return mc.window.document.querySelector("#expandedtagsBox > .tagvalue");
+    name: "Newsgroups",
+    element() {
+      return document.querySelector("#expandednewsgroupsBox .header-newsgroup");
+    },
+    expectedName(element) {
+      return `${document.getElementById("expandednewsgroupsLabel").value}: ${
+        element.textContent
+      }`;
     },
-    expectedName(mc, headerValueElement) {
-      return (
-        mc.e("expandedtagsLabel").value +
-        ": " +
-        headerValueElement.getAttribute("value")
-      );
+  },
+  {
+    name: "Tags",
+    element() {
+      return document.querySelector("#expandedtagsBox > .tagvalue");
+    },
+    expectedName(element) {
+      return `${document.getElementById("expandedtagsLabel").value}: ${
+        element.value
+      }`;
     },
   },
 ];
 
-// used to get the accessible object for a DOM node
-let gAccService = Cc["@mozilla.org/accessibilityService;1"].getService(
-  Ci.nsIAccessibilityService
-);
-
 /**
- * Use the information from aHeaderInfo to verify that screenreaders will
- * do the right thing with the given message header.
+ * Use the information from HeaderInfo to verify that screen readers will do the
+ * right thing with the given message header.
  *
- * @param {Object} aHeaderInfo  Information about how to do the verification;
- *                              See the comments above the headersToTest array
- *                              for details.
+ * @param {HeaderInfo} header - The HeaderInfo data type object.
  */
-function verify_header_a11y(aHeaderInfo) {
-  let headerValueElement = aHeaderInfo.headerValueElement(mc);
+async function verify_header_a11y(header) {
+  let element = header.element();
   Assert.notEqual(
-    headerValueElement,
+    element,
     null,
-    `element not found for header '${aHeaderInfo.headerName}'`
+    `element not found for header '${header.name}'`
   );
 
   let headerAccessible;
-  mc.waitFor(
-    () =>
-      (headerAccessible = gAccService.getAccessibleFor(headerValueElement)) !=
-      null,
-    `didn't find accessible element for header '${aHeaderInfo.headerName}'`
+  await TestUtils.waitForCondition(
+    () => (headerAccessible = gAccService.getAccessibleFor(element)) != null,
+    `didn't find accessible element for header '${header.name}'`
   );
 
-  let expectedName = aHeaderInfo.expectedName(mc, headerValueElement);
+  let expectedName = header.expectedName(element);
   Assert.equal(
     headerAccessible.name,
     expectedName,
-    `headerAccessible.name for ${aHeaderInfo.headerName} ` +
+    `headerAccessible.name for ${header.name} ` +
       `was '${headerAccessible.name}'; expected '${expectedName}'`
   );
 }
 
 /**
  * Test the accessibility attributes of the various message headers.
  *
- * XXX This test used to be after test_more_button_with_many_recipients,
- * however, there were some accessibility changes that it didn't seem to play
- * nicely with, and the toggling of the "more" button on the cc field was
- * causing this test to fail on the cc element. Tests with accessibility
- * hardware/software showed that the code was working fine. Therefore the test
- * may be suspect.
- *
- * XXX The gInterestingMessage has no tags until after
+ * INFO: The gInterestingMessage has no tags until after
  * test_add_tag_with_really_long_label, so ensure it runs after that one.
  */
 add_task(function test_a11y_attrs() {
   be_in_folder(folder);
-
-  // Convert the SyntheticMessage gInterestingMessage into an actual
-  // nsIMsgDBHdr XPCOM message.
+  // Convert the SyntheticMessage gInterestingMessage into an actual nsIMsgDBHdr
+  // XPCOM message.
   let hdr = folder.msgDatabase.getMsgHdrForMessageID(
     gInterestingMessage.messageId
   );
-
-  // select and open the interesting message
+  // Select and open the interesting message.
   let curMessage = select_click_row(mc.dbView.findIndexOfMsgHdr(hdr, false));
-
-  // make sure it loads
+  // Make sure it loads.
   assert_selected_and_displayed(mc, curMessage);
-
+  // Test all the headers with this message.
   headersToTest.forEach(verify_header_a11y);
 });
 
 add_task(function test_more_button_with_many_recipients() {
   // Start on the interesting message.
   let curMessage = select_click_row(0);
 
-  // make sure it loads
+  // Make sure it loads.
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
-  // Check the mode of the header.
-  let headerBox = mc.e("messageHeader");
-  let previousHeaderMode = headerBox.getAttribute("show_header_mode");
-
-  // Click the "more" button.
-  let moreIndicator = mc.window.document.getElementById("expandedccBox").more;
-  moreIndicator.click();
+  // Click on the "more" button.
+  EventUtils.synthesizeMouseAtCenter(
+    document.getElementById("expandedccBox").moreButton,
+    {}
+  );
 
-  // Check the new mode of the header.
-  if (headerBox.getAttribute("show_header_mode") != "all") {
-    throw new Error(
-      "Header Mode didn't change to 'all'!  old=" +
-        previousHeaderMode +
-        ", new=" +
-        headerBox.getAttribute("show_header_mode")
-    );
-  }
+  let msgHeader = document.getElementById("messageHeader");
+  // Check that the message header can scroll to fit all recipients.
+  Assert.ok(
+    msgHeader.classList.contains("scrollable"),
+    "The message header is scrollable"
+  );
 
   // Switch to the boring message, to force the more button to collapse.
   curMessage = select_click_row(1);
 
-  // make sure it loads
+  // Make sure it loads.
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
-  // Check the even newer mode of the header.
-  if (headerBox.getAttribute("show_header_mode") != previousHeaderMode) {
-    throw new Error(
-      "Header Mode changed from " +
-        previousHeaderMode +
-        " to " +
-        headerBox.getAttribute("show_header_mode") +
-        " and didn't change back."
-    );
-  }
+  // Check that the message header is not scrollable anymore
+  Assert.notEqual(
+    msgHeader.classList.contains("scrollable"),
+    "The message header is not scrollable"
+  );
 });
 
 /**
+ * Test that clicking the add to address book button updates the UI properly.
+ *
+ * @param {HTMLOListElement} recipientsList
+ */
+function subtest_more_widget_ab_button_click(recipientsList) {
+  let recipient = get_last_visible_address(recipientsList);
+  ensure_no_card_exists(recipient.emailAddress);
+
+  // Scroll to the bottom first so the address is in view.
+  let view = document.getElementById("messageHeader");
+  view.scrollTop = view.scrollHeight - view.clientHeight;
+
+  EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {});
+
+  Assert.ok(
+    recipient.abIndicator.classList.contains("in-address-book"),
+    "The recipient was added to the Address Book"
+  );
+}
+
+/**
  * Test that we can open up the inline contact editor when we
- * click on the star.
+ * click on the address book button.
  */
-add_task(async function test_clicking_star_opens_inline_contact_editor() {
-  // Make sure we're in the right folder
+add_task(async function test_clicking_ab_button_opens_inline_contact_editor() {
+  // Make sure we're in the right folder.
   be_in_folder(folder);
-  // Add a new message
+  // Add a new message.
   let msg = create_message();
   await add_message_to_folder([folder], msg);
-  // Open the latest message
+  // Open the latest message.
   select_click_row(-1);
   wait_for_message_display_completion(mc);
-  // Make sure the star is clicked, and we add the
-  // new contact to our address book
-  let toDescription = mc.window.document.getElementById("expandedtoBox")
-    .emailAddresses;
 
   // Ensure that the inline contact editing panel is not open
-  let contactPanel = mc.e("editContactPanel");
+  let contactPanel = document.getElementById("editContactPanel");
   Assert.notEqual(contactPanel.state, "open");
-  subtest_more_widget_star_click(toDescription);
+
+  let recipientsList = document.getElementById("expandedtoBox").recipientsList;
+  subtest_more_widget_ab_button_click(recipientsList);
 
   // Ok, if we're here, then the star has been clicked, and
   // the contact has been added to our AB.
-  let addrs = toDescription.getElementsByTagName("mail-emailaddress");
-  let lastAddr = get_last_visible_address(addrs);
+  let recipient = get_last_visible_address(recipientsList);
 
-  // Click on the star, and ensure that the inline contact
-  // editing panel opens
-  mc.click(lastAddr.querySelector(".emailStar"));
-  mc.waitFor(
+  let panelOpened = TestUtils.waitForCondition(
     () => contactPanel.state == "open",
-    () =>
-      "Timeout waiting for contactPanel to open; state=" + contactPanel.state
+    "The contactPanel was opened"
   );
+  // Click on the star, and ensure that the inline contact editing panel opens.
+  EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {});
+  await panelOpened;
 
-  let detailsButton = mc.e("editContactPanelEditDetailsButton");
-  mc.click(detailsButton);
+  EventUtils.synthesizeMouseAtCenter(
+    document.getElementById("editContactPanelEditDetailsButton"),
+    {}
+  );
   wait_for_content_tab_load(undefined, "about:addressbook");
-  // TODO check the card
+  // TODO check the card.
   mc.tabmail.closeTab();
 });
 
 /**
- * Ensure that the specified element is visible/hidden
- *
- * @param id the id of the element to check
- * @param visible true if the element should be visible, false otherwise
- */
-function assert_shown(id, visible) {
-  if (mc.e(id).hidden == visible) {
-    throw new Error(
-      '"' + id + '" should be ' + (visible ? "visible" : "hidden")
-    );
-  }
-}
-
-/**
  * Test that clicking references context menu works properly.
  */
 add_task(async function test_msg_id_context_menu() {
   Services.prefs.setBoolPref("mailnews.headers.showReferences", true);
 
-  // Add a new message
+  // Add a new message.
   let msg = create_message({
     clobberHeaders: {
       References:
         "<4880C986@example.com> <4880CAB2@example.com> <4880CC76@example.com>",
     },
   });
   await add_message_to_folder([folder], msg);
   be_in_folder(folder);
 
   // Open the latest message.
   select_click_row(-1);
 
   // Right click to show the context menu.
   EventUtils.synthesizeMouseAtCenter(
-    mc.window.document.querySelector("#expandedreferencesBox mail-messageid"),
+    document.querySelector("#expandedreferencesBox mail-messageid"),
     { type: "contextmenu" },
     window
   );
-  await wait_for_popup_to_open(mc.e("messageIdContext"));
+  await wait_for_popup_to_open(document.getElementById("messageIdContext"));
 
-  // Ensure Open Message For ID is shown... and that Open Browser With Message-ID
+  // Ensure Open Message For ID is shown and that Open Browser With Message-ID
   // isn't shown.
-  assert_shown("messageIdContext-openMessageForMsgId", true);
-  assert_shown("messageIdContext-openBrowserWithMsgId", false);
+  Assert.ok(
+    !document.getElementById("messageIdContext-openMessageForMsgId").hidden,
+    "The menu item is hidden"
+  );
+  Assert.ok(
+    document.getElementById("messageIdContext-openBrowserWithMsgId").hidden,
+    "The menu item is visible"
+  );
 
-  await close_popup(mc, mc.e("messageIdContext"));
+  await close_popup(mc, document.getElementById("messageIdContext"));
 
+  // Reset the preferences.
   Services.prefs.setBoolPref("mailnews.headers.showReferences", false);
 });
 
 /**
- * Test that if a contact belongs to a mailing list within their
- * address book, then the inline contact editor will not allow
- * the user to change what address book the contact belongs to.
- * The editor should also show a message to explain why the
- * contact cannot be moved.
+ * Test that if a contact belongs to a mailing list within their address book,
+ * then the inline contact editor will not allow the user to change what address
+ * book the contact belongs to. The editor should also show a message to explain
+ * why the contact cannot be moved.
  */
 add_task(
   async function test_address_book_switch_disabled_on_contact_in_mailing_list() {
     const MAILING_LIST_DIRNAME = "Some Mailing List";
     const ADDRESS_BOOK_NAME = "Some Address Book";
-    // Add a new message
+    // Add a new message.
     let msg = create_message();
     await add_message_to_folder([folder], msg);
 
-    // Make sure we're in the right folder
+    // Make sure we're in the right folder.
     be_in_folder(folder);
 
-    // Open the latest message
+    // Open the latest message.
     select_click_row(-1);
 
-    // Make sure the star is clicked, and we add the
-    // new contact to our address book
-    let toDescription = mc.window.document.getElementById("expandedtoBox")
-      .emailAddresses;
-
     // Ensure that the inline contact editing panel is not open
-    let contactPanel = mc.e("editContactPanel");
+    let contactPanel = document.getElementById("editContactPanel");
     Assert.notEqual(contactPanel.state, "open");
 
-    subtest_more_widget_star_click(toDescription);
+    let recipientsList = document.getElementById("expandedtoBox")
+      .recipientsList;
+    subtest_more_widget_ab_button_click(recipientsList);
 
     // Ok, if we're here, then the star has been clicked, and
     // the contact has been added to our AB.
-    let addrs = toDescription.getElementsByTagName("mail-emailaddress");
-    let lastAddr = get_last_visible_address(addrs);
+    let recipient = get_last_visible_address(recipientsList);
 
-    // Click on the star, and ensure that the inline contact
-    // editing panel opens
-    mc.click(lastAddr.querySelector(".emailStar"));
-    mc.waitFor(
+    let panelOpened = TestUtils.waitForCondition(
       () => contactPanel.state == "open",
-      () =>
-        "Timeout waiting for contactPanel to open; state=" + contactPanel.state
+      "The contactPanel was opened"
     );
+    // Click on the address book button, and ensure that the inline contact
+    // editing panel opens.
+    EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {});
+    await panelOpened;
 
-    let abDrop = mc.e("editContactAddressBookList");
-    let warningMsg = mc.e("contactMoveDisabledText");
-
+    let abDrop = document.getElementById("editContactAddressBookList");
     // Ensure that the address book dropdown is not disabled
     Assert.ok(!abDrop.disabled);
+
+    let warningMsg = document.getElementById("contactMoveDisabledText");
     // We should not be displaying any warning
     Assert.ok(warningMsg.hidden);
 
-    // Now close the popup
+    // Now close the popup.
     contactPanel.hidePopup();
 
-    // For the contact that was added, create a mailing list in the
-    // address book it resides in, and then add that contact to the
-    // mailing list
-    addrs = toDescription.getElementsByTagName("mail-emailaddress");
-    let targetAddr = get_last_visible_address(addrs).getAttribute(
-      "emailAddress"
+    // For the contact that was added, create a mailing list in the address book
+    // it resides in, and then add that contact to the mailing list.
+    let cards = get_cards_in_all_address_books_for_email(
+      recipient.emailAddress
     );
 
-    let cards = get_cards_in_all_address_books_for_email(targetAddr);
-
-    // There should be only one copy of this email address
-    // in the address books.
+    // There should be only one copy of this email address in the address books.
     Assert.equal(cards.length, 1);
-    let card = cards[0];
 
-    // Remove the card from any of the address books
-    ensure_no_card_exists(targetAddr);
+    // Remove the card from any of the address books.s
+    ensure_no_card_exists(recipient.emailAddress);
 
-    // Add the card to a new address book, and insert it
-    // into a mailing list under that address book
+    // Add the card to a new address book, and insert it into a mailing list
+    // under that address book.
     let ab = create_address_book(ADDRESS_BOOK_NAME);
-    ab.dropCard(card, false);
+    ab.dropCard(cards[0], false);
     let ml = create_mailing_list(MAILING_LIST_DIRNAME);
     ab.addMailList(ml);
 
-    // Now we have to retrieve the mailing list from
-    // the address book, in order for us to add and
-    // delete cards from it.
+    // Now we have to retrieve the mailing list from the address book, in order
+    // for us to add and delete cards from it.
     ml = get_mailing_list_from_address_book(ab, MAILING_LIST_DIRNAME);
-    ml.addCard(card);
+    ml.addCard(cards[0]);
 
-    // Re-open the inline contact editing panel
-    mc.click(lastAddr.querySelector(".emailStar"));
-    mc.waitFor(
-      () => contactPanel.state == "open",
-      () =>
-        "Timeout waiting for contactPanel to open; state=" + contactPanel.state
-    );
+    // Click on the address book button, and ensure that the inline contact
+    // editing panel opens.
+    EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {});
+    await panelOpened;
 
     // The dropdown should be disabled now
     Assert.ok(abDrop.disabled);
     // We should be displaying a warning
     Assert.ok(!warningMsg.hidden);
 
     contactPanel.hidePopup();
 
-    // And if we remove the contact from the mailing list, the
-    // warning should be gone and the address book switching
-    // menu re-enabled.
-
-    ml.deleteCards([card]);
+    // And if we remove the contact from the mailing list, the warning should be
+    // gone and the address book switching menu re-enabled.
+    ml.deleteCards([cards[0]]);
 
-    // Re-open the inline contact editing panel
-    mc.click(lastAddr.querySelector(".emailStar"));
-    mc.waitFor(
-      () => contactPanel.state == "open",
-      () =>
-        "Timeout waiting for contactPanel to open; state=" + contactPanel.state
-    );
+    // Click on the address book button, and ensure that the inline contact
+    // editing panel opens.
+    EventUtils.synthesizeMouseAtCenter(recipient.abIndicator, {});
+    await panelOpened;
 
     // Ensure that the address book dropdown is not disabled
     Assert.ok(!abDrop.disabled);
     // We should not be displaying any warning
     Assert.ok(warningMsg.hidden);
 
     contactPanel.hidePopup();
   }
 );
 
 /**
  * Test that clicking the adding an address node adds it to the address book.
  */
 add_task(async function test_add_contact_from_context_menu() {
+  let popup = document.getElementById("emailAddressPopup");
+  let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
   // Click the contact to show the emailAddressPopup popup menu.
-  mc.click(
-    mc.window.document.querySelector("#expandedfromBox mail-emailaddress")
-  );
+  let recipient = document.querySelector("#expandedfromBox .header-recipient");
+  EventUtils.synthesizeMouseAtCenter(recipient, {});
+  await popupShown;
+
+  const addToAddressBookItem = document.getElementById("addToAddressBookItem");
+  Assert.ok(!addToAddressBookItem.hidden, "addToAddressBookItem is not hidden");
 
-  var addToAddressBookItem = mc.window.document.getElementById(
-    "addToAddressBookItem"
+  const editContactItem = document.getElementById("editContactItem");
+  Assert.ok(editContactItem.hidden, "editContactItem is hidden");
+
+  let recipientAdded = TestUtils.waitForCondition(
+    () => recipient.abIndicator.classList.contains("in-address-book"),
+    "The recipient was added to the address book"
   );
-  if (addToAddressBookItem.hidden) {
-    throw new Error("addToAddressBookItem is hidden for unknown contact");
-  }
-  var editContactItem = mc.window.document.getElementById("editContactItem");
-  if (!editContactItem.hidden) {
-    throw new Error("editContactItem is NOT hidden for unknown contact");
-  }
 
   // Click the Add to Address Book context menu entry.
-  mc.click(mc.e("addToAddressBookItem"));
-  // (for reasons unknown, the pop-up does not close itself)
-  await close_popup(mc, mc.e("emailAddressPopup"));
-
-  // Now click the contact again, the context menu should now show the
-  // Edit Contact menu instead.
-  mc.click(
-    mc.window.document.querySelector("#expandedfromBox mail-emailaddress")
-  );
+  // NOTE: Use activateItem because macOS uses native context menus.
+  popup.activateItem(addToAddressBookItem);
   // (for reasons unknown, the pop-up does not close itself)
-  await close_popup(mc, mc.e("emailAddressPopup"));
+  await close_popup(mc, popup);
+  await recipientAdded;
+
+  // NOTE: We need to redefine these selectors otherwise the popup will not
+  // properly close for some reason.
+  let popup2 = document.getElementById("emailAddressPopup");
+  let popupShown2 = BrowserTestUtils.waitForEvent(popup, "popupshown");
 
-  addToAddressBookItem = mc.window.document.getElementById(
-    "addToAddressBookItem"
-  );
-  if (!addToAddressBookItem.hidden) {
-    throw new Error("addToAddressBookItem is NOT hidden for known contact");
-  }
-  editContactItem = mc.window.document.getElementById("editContactItem");
-  if (editContactItem.hidden) {
-    throw new Error("editContactItem is hidden for known contact");
-  }
+  // Now click the contact again, the context menu should now show the Edit
+  // Contact menu instead.
+  EventUtils.synthesizeMouseAtCenter(recipient, {});
+  await popupShown2;
+  // (for reasons unknown, the pop-up does not close itself)
+  await close_popup(mc, popup2);
+
+  Assert.ok(addToAddressBookItem.hidden, "addToAddressBookItem is hidden");
+  Assert.ok(!editContactItem.hidden, "editContactItem is not hidden");
 });
 
 add_task(async function test_that_msg_without_date_clears_previous_headers() {
   be_in_folder(folder);
 
-  // create a message: with descritive subject
+  // Create a message with a descriptive subject.
   let msg = create_message({ subject: "this is without date" });
 
-  // ensure that this message doesn't have a Date header
+  // Ensure that this message doesn't have a Date header.
   delete msg.headers.Date;
 
-  // this will add the message to the end of the folder
+  // Sdd the message to the end of the folder.
   await add_message_to_folder([folder], msg);
 
   // Not the first anymore. The timestamp is that of "NOW".
-  // select and open the LAST message
+  // Select and open the LAST message.
   let curMessage = select_click_row(-1);
 
-  // make sure it loads
+  // Make sure it loads.
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
   // Since we didn't give create_message an argument that would create a
   // Newsgroups header, the newsgroups <row> element should be collapsed.
   // However, since the previously displayed message _did_ have such a header,
   // certain bugs in the display of this header could cause the collapse
   // never to have happened.
-  if (!mc.e("expandednewsgroupsRow").hasAttribute("hidden")) {
-    throw new Error(
-      "Expected <row> element for Newsgroups header to be " +
-        "collapsed, but it wasn't\n!"
-    );
-  }
+  Assert.ok(
+    document.getElementById("expandednewsgroupsRow").hidden,
+    "The Newsgroups header row is hidden."
+  );
 });
 
 /**
- * Test various aspects of the (n more) widgetry.
+ * Get the number of lines in one of the multi-recipient-row fields.
+ * @param {HTMLOListElement} node  - The recipients container of a header row.
+ * @return {int} - The number of rows.
+ */
+function help_get_num_lines(node) {
+  let style = getComputedStyle(node.firstElementChild);
+  return Math.round(
+    parseFloat(getComputedStyle(node).height) /
+      parseFloat(style.height + style.paddingTop + style.paddingBottom)
+  );
+}
+
+/**
+ * Test that the "more" button displays when it should.
+ *
+ * @param {HTMLOListElement} node - The recipients container of a header row.
  */
-async function test_more_widget() {
-  // generate message with 35 recips (effectively guarantees overflow for n=3)
+function subtest_more_widget_display(node, showAll = false) {
+  // Test that the to element doesn't have more than max lines.
+  let numLines = help_get_num_lines(node);
+  // Get the max line pref.
+  let maxLines = Services.prefs.getIntPref(LINES_PREF);
+
+  if (showAll) {
+    Assert.ok(
+      numLines > maxLines,
+      `Currently visible lines are more than the number of max lines. ${numLines} > ${maxLines}`
+    );
+    Assert.ok(
+      !document
+        .getElementById("expandedtoBox")
+        .querySelector(".show-more-recipients"),
+      "The `more` button doesn't exist."
+    );
+  } else {
+    Assert.ok(
+      numLines <= maxLines,
+      `Currently visible lines are fewer than the number of max lines. ${numLines} <= ${maxLines}`
+    );
+    // Test that we've got a "more" button and that it's visible
+    Assert.ok(
+      !document.getElementById("expandedtoBox").moreButton.hidden,
+      "The `more` button is visible."
+    );
+  }
+}
+
+/**
+ * Test that clicking the "more" button displays all the addresses.
+ *
+ * @param {HTMLOListElement} node - The recipients container of a header row.
+ */
+function subtest_more_widget_click(node) {
+  let oldNumLines = help_get_num_lines(node);
+  let moreButton = document.getElementById("expandedtoBox").moreButton;
+  // Click on the "more" button.
+  EventUtils.synthesizeMouseAtCenter(moreButton, {});
+
+  // Make sure that the "more" button was removed when showing all addresses.
+  Assert.ok(!moreButton.hidden, "The `more` button doesn't exist anymore.");
+
+  // Rest that we actually have more lines than we did before!
+  let newNumLines = help_get_num_lines(node);
+  Assert.ok(
+    newNumLines > oldNumLines,
+    `Number of address lines present after more clicked = ${newNumLines} > number of lines present beforehand = ${oldNumLines}`
+  );
+}
+
+/**
+ * Test the behavior of the "more" button.
+ */
+add_task(async function test_view_more_button() {
+  // Generate message with 35 recipients to guarantee overflow.
   be_in_folder(folder);
   let msg = create_message({
     toCount: 35,
     subject: "Many To addresses to test_more_widget",
   });
 
-  // add the message to the end of the folder
+  // Add the message to the end of the folder.
   await add_message_to_folder([folder], msg);
 
-  // Select and open the injected message;
+  // Select and open the injected message.
   // It is at the second last message in the display list.
   let curMessage = select_click_row(-2);
+  // FIXME: Switch between a couple of messages to allow the UI to properly
+  // refresh and fetch the proper recipients row width in order to avoid an
+  // unexpected recipients wrapping. This happens because the width calculation
+  // happens before the message header layout is fully generated.
+  let prevMessage = select_click_row(-1);
+  wait_for_message_display_completion(mc);
+  assert_selected_and_displayed(mc, prevMessage);
 
-  // make sure it loads
+  curMessage = select_click_row(-2);
+
+  // Make sure it loads.
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
-  // get the description element containing the addresses
-  let toDescription = mc.window.document.getElementById("expandedtoBox")
-    .emailAddresses;
-
-  subtest_more_widget_display(toDescription);
-  subtest_more_widget_click(toDescription);
-  subtest_more_widget_star_click(toDescription);
-
-  let showNLinesPref = Services.prefs.getIntPref(
-    "mailnews.headers.show_n_lines_before_more"
-  );
-  Services.prefs.clearUserPref("mailnews.headers.show_n_lines_before_more");
-  change_to_header_normal_mode();
-  be_in_folder(folderMore);
-
-  // first test a message with so many addresses that they don't fit in the
-  // more widget's tooltip text
-  msg = select_click_row(0);
-  wait_for_message_display_completion(mc);
-  assert_selected_and_displayed(mc, msg);
-  subtest_more_button_tooltip(msg);
-
-  // then test a message with so many addresses that they do fit in the
-  // more widget's tooltip text
-  msg = select_click_row(1);
-  wait_for_message_display_completion(mc);
-  assert_selected_and_displayed(mc, msg);
-  subtest_more_button_tooltip(msg);
-  Services.prefs.setIntPref(
-    "mailnews.headers.show_n_lines_before_more",
-    showNLinesPref
-  );
-}
-add_task(test_more_widget);
+  // Get the sender address.
+  let node = document.getElementById("expandedtoBox").recipientsList;
+  subtest_more_widget_display(node);
+  subtest_more_widget_click(node);
+});
 
 /**
- * Test that all addresses are shown in show all header mode
+ * Test that all addresses are shown in show all header mode.
  */
 add_task(async function test_show_all_header_mode() {
-  // generate message with 35 recips (effectively guarantees overflow for n=3)
+  function toggle_header_mode(show) {
+    mc.click_through_appmenu(
+      [{ id: "appmenu_View" }, { id: "appmenu_viewHeadersMenu" }],
+      { id: show ? "appmenu_viewallheaders" : "appmenu_viewnormalheaders" }
+    );
+  }
+
+  // Generate message with 35 recipients.
   be_in_folder(folder);
   let msg = create_message({
     toCount: 35,
     subject: "many To addresses for test_show_all_header_mode",
   });
 
-  // add the message to the end of the folder
+  // Add the message to the end of the folder.
   await add_message_to_folder([folder], msg);
 
-  // select and open the added message.
+  // Select and open the added message.
   // It is at the second last position in the display list.
   let curMessage = select_click_row(-2);
 
-  // make sure it loads
+  // Make sure it loads.
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
-  // get the description element containing the addresses
-  let toDescription = mc.window.document.getElementById("expandedtoBox")
-    .emailAddresses;
-
-  change_to_header_normal_mode();
-  subtest_more_widget_display(toDescription);
-  subtest_change_to_all_header_mode(toDescription);
-  change_to_header_normal_mode();
-  subtest_more_widget_click(toDescription);
-});
-
-function change_to_header_normal_mode() {
-  // XXX Clicking on check menu items doesn't work in 1.4.1b1 (bug 474486)...
-  //  mc.click(mc.menus.View.viewheadersmenu.viewnormalheaders);
-  // ... so call the function instead.
-  mc.window.MsgViewNormalHeaders();
-  mc.sleep(0);
-}
-
-function change_to_all_header_mode() {
-  // XXX Clicking on check menu items doesn't work in 1.4.1b1 (bug 474486)...
-  //  mc.click(mc.menus.View.viewheadersmenu.viewallheaders);
-  // ... so call the function instead.
-  mc.window.MsgViewAllHeaders();
-  mc.sleep(0);
-}
-
-/**
- * Get the number of lines in one of the multi-address fields
- * @param node the description element containing the addresses
- * @return the number of lines
- */
-function help_get_num_lines(node) {
-  let style = mc.window.getComputedStyle(node);
-  return style.height / style.lineHeight;
-}
-
-/**
- * Test that the "more" widget displays when it should.
- * @param toDescription the description node for the "to" field
- */
-function subtest_more_widget_display(toDescription) {
-  // test that the to element doesn't have more than max lines
-  let numLines = help_get_num_lines(toDescription);
-
-  // get maxline pref
-  let maxLines = Services.prefs.getIntPref(
-    "mailnews.headers.show_n_lines_before_more"
-  );
-
-  // allow for a 15% tolerance for any padding that may be applied
-  if (numLines < 0.85 * maxLines || numLines > 1.15 * maxLines) {
-    throw new Error("expected == " + maxLines + "lines; found " + numLines);
-  }
-
-  // test that we've got a (more) node and that it's expanded
-  let moreNode = mc.window.document.getElementById("expandedtoBox").more;
-  if (!moreNode) {
-    throw new Error("more node not found before activation");
-  }
-  if (moreNode.collapsed) {
-    throw new Error("more node was collapsed when it should have been visible");
-  }
-}
-
-/**
- * Test that clicking the "more" widget displays all the addresses.
- * @param toDescription the description node for the "to" field
- */
-function subtest_more_widget_click(toDescription) {
-  let oldNumLines = help_get_num_lines(toDescription);
-
-  // activate (n more)
-  let moreNode = mc.window.document.getElementById("expandedtoBox").more;
-  mc.click(moreNode);
-
-  // test that (n more) is gone
-  moreNode = mc.window.document.getElementById("expandedtoBox").more;
-  if (!moreNode.collapsed) {
-    throw new Error("more node should be collapsed after activation");
-  }
-
-  // test that we actually have more lines than we did before!
-  let newNumLines = help_get_num_lines(toDescription);
-  if (newNumLines <= oldNumLines) {
-    throw new Error(
-      "number of address lines present after more clicked = " +
-        newNumLines +
-        "<= number of lines present beforehand = " +
-        oldNumLines
-    );
-  }
-}
-
-/**
- * Test that changing to all header lines mode displays all the addresses.
- * @param toDescription the description node for the "to" field
- */
-function subtest_change_to_all_header_mode(toDescription) {
-  let oldNumLines = help_get_num_lines(toDescription);
-
-  change_to_all_header_mode();
-  mc.sleep(500);
-  // test that (n more) is gone
-  let moreNode = mc.window.document.getElementById("expandedtoBox").more;
-  if (!moreNode.collapsed) {
-    throw new Error("more node should be collapsed in all header lines mode");
-  }
-
-  // test that we actually have more lines than we did before!
-  let newNumLines = help_get_num_lines(toDescription);
-  if (newNumLines <= oldNumLines) {
-    throw new Error(
-      "number of address lines present in all header lines mode = " +
-        newNumLines +
-        "<= number of lines present beforehand = " +
-        oldNumLines
-    );
-  }
-}
-
-/**
- * Test that clicking the star updates the UI properly (see bug 563612).
- * @param toDescription the description node for the "to" field
- */
-function subtest_more_widget_star_click(toDescription) {
-  let addrs = toDescription.getElementsByTagName("mail-emailaddress");
-  let lastAddr = get_last_visible_address(addrs);
-  ensure_no_card_exists(lastAddr.getAttribute("emailAddress"));
-
-  // scroll to the bottom first so the address is in view
-  let view = mc.e("messageHeader");
-  view.scrollTop = view.scrollHeight - view.clientHeight;
-  let star = lastAddr.querySelector(".emailStar");
-  let src = star.getAttribute("src");
-
-  mc.click(star);
-  if (star.getAttribute("src") === src) {
-    throw new Error("address not updated after clicking star");
-  }
-}
-
-/**
- * Make sure the (more) widget hidden pref actually works with a
- * non-default value.
- */
-add_task(async function test_more_widget_with_maxlines_of_3() {
-  // set maxLines to 3
-  Services.prefs.setIntPref("mailnews.headers.show_n_lines_before_more", 3);
-
-  // call test_more_widget again
-  // We need to look at the second last article in the display list.
-  await test_more_widget();
-});
-
-/**
- * Make sure the (more) widget hidden pref also works with an
- * "all" (0) non-default value.
- */
-add_task(async function test_more_widget_with_disabled_more() {
-  // set maxLines to 0
-  Services.prefs.setIntPref("mailnews.headers.show_n_lines_before_more", 0);
-
-  // generate message with 35 recips (effectively guarantees overflow for n=3)
-  be_in_folder(folder);
-  let msg = create_message({ toCount: 35 });
+  toggle_header_mode(true);
+  let node = document.getElementById("expandedtoBox").recipientsList;
+  subtest_more_widget_display(node, true);
 
-  // add the message to the end of the folder
-  await add_message_to_folder([folder], msg);
-
-  // select and open the last message
-  let curMessage = select_click_row(-1);
-
-  // make sure it loads
-  wait_for_message_display_completion(mc);
-  assert_selected_and_displayed(mc, curMessage);
-
-  // test that (n more) is gone
-  let moreNode = mc.window.document.getElementById("expandedtoBox").more;
-  if (!moreNode.collapsed) {
-    throw new Error("more node should be collapsed in n=0 case");
-  }
-
-  // get the description element containing the addresses
-  let toDescription = mc.window.document.getElementById("expandedtoBox")
-    .emailAddresses;
-
-  // test that we actually have more lines than the 3 we know are filled
-  let newNumLines = help_get_num_lines(toDescription);
-  if (newNumLines <= 3) {
-    throw new Error(
-      "number of address lines present in all addresses mode = " +
-        newNumLines +
-        "<= number of expected minimum of 3 lines filled"
-    );
-  }
+  toggle_header_mode(false);
+  subtest_more_widget_display(node);
+  subtest_more_widget_click(node);
+  subtest_more_widget_display(node, true);
 });
 
 /**
- * Test if the tooltip text of the more widget contains the correct addresses
- * not shown in the header and the number of addresses also hidden in the
- * tooltip text.
- * @param aMsg the message for which the subtest should be performed
- */
-function subtest_more_button_tooltip(aMsg) {
-  // check for more indicator number of the more widget
-  let ccAddrs = MailServices.headerParser.parseEncodedHeader(aMsg.ccList);
-  let toAddrs = MailServices.headerParser.parseEncodedHeader(aMsg.recipients);
-
-  let shownToAddrNum = get_number_of_addresses_in_header("expandedtoBox");
-  let shownCCAddrNum = get_number_of_addresses_in_header("expandedccBox");
-
-  // first check the number of addresses in the more widget
-  let hiddenCCAddrsNum = ccAddrs.length - shownCCAddrNum;
-  let hiddenToAddrsNum = toAddrs.length - shownToAddrNum;
-
-  let moreNumberTo = get_number_of_more_button("expandedtoBox");
-  Assert.notEqual(NaN, moreNumberTo);
-  Assert.equal(hiddenToAddrsNum, moreNumberTo);
-
-  let moreNumberCC = get_number_of_more_button("expandedccBox");
-  Assert.notEqual(NaN, moreNumberCC);
-  Assert.equal(hiddenCCAddrsNum, moreNumberCC);
-
-  subtest_addresses_in_tooltip_text(
-    aMsg.recipients,
-    "expandedtoBox",
-    shownToAddrNum,
-    hiddenToAddrsNum
-  );
-  subtest_addresses_in_tooltip_text(
-    aMsg.ccList,
-    "expandedccBox",
-    shownCCAddrNum,
-    hiddenCCAddrsNum
-  );
-}
-
-/**
- * Return the number of addresses visible in headerBox.
- * @param aHeaderBox the id of the header box element for which to look for
- *                   visible addresses
- * @return           the number of visible addresses in the header box
- */
-function get_number_of_addresses_in_header(aHeaderBox) {
-  let headerBoxElement = mc.e(aHeaderBox, { class: "headerValue" });
-  let addrs = headerBoxElement.getElementsByTagName("mail-emailaddress");
-  let addrNum = 0;
-  for (let i = 0; i < addrs.length; i++) {
-    // check that the address is really visible and not just a cached
-    // element
-    if (element_visible_recursive(addrs[i])) {
-      addrNum += 1;
-    }
-  }
-  return addrNum;
-}
-
-/**
- * Return the number shown in the more widget.
- * @param aHeaderBox the id of the header box element for which to look for
- *                   the number in the more widget
- * @return           the number shown in the more widget
- */
-function get_number_of_more_button(aHeaderBox) {
-  let moreNumber = 0;
-  let headerBoxElement = mc.e(aHeaderBox);
-  let moreIndicator = headerBoxElement.more;
-  if (element_visible_recursive(moreIndicator)) {
-    let moreText = moreIndicator.getAttribute("value");
-    let moreSplit = moreText.split(" ");
-    moreNumber = parseInt(moreSplit[0]);
-  }
-  return moreNumber;
-}
-
-/**
- * Check if hidden addresses are part of more tooltip text.
- * @param aRecipients     an array containing the addresses to look for in the
- *                        header or the tooltip text
- * @param aHeaderBox      the id of the header box element for which to look
- *                        for hidden addresses
- * @param aShownAddrsNum  the number of addresses shown in the header
- * @param aHiddenAddrsNum the number of addresses not shown in the header
- */
-function subtest_addresses_in_tooltip_text(
-  aRecipients,
-  aHeaderBox,
-  aShownAddrsNum,
-  aHiddenAddrsNum
-) {
-  // check for more indicator number of the more widget
-  let addresses = MailServices.headerParser.parseEncodedHeader(aRecipients);
-
-  let headerBoxElement = mc.e(aHeaderBox);
-  let moreIndicator = headerBoxElement.more;
-  let tooltipText = moreIndicator.getAttribute("tooltiptext");
-  let maxTooltipAddrsNum = headerBoxElement.maxAddressesInMoreTooltipValue;
-  let addrsNumInTooltip = 0;
-
-  for (
-    let i = aShownAddrsNum;
-    i < addresses.length && i < maxTooltipAddrsNum + aShownAddrsNum;
-    i++
-  ) {
-    Assert.ok(
-      tooltipText.includes(addresses[i].toString()),
-      addresses[i].toString()
-    );
-    addrsNumInTooltip += 1;
-  }
-
-  if (aHiddenAddrsNum < maxTooltipAddrsNum) {
-    Assert.equal(aHiddenAddrsNum, addrsNumInTooltip);
-  } else {
-    Assert.equal(maxTooltipAddrsNum, addrsNumInTooltip);
-    // check if ", and X more" shows the correct number
-    let moreTooltipSplit = tooltipText.split(", ");
-    let words = mc.window.document
-      .getElementById("bundle_messenger")
-      .getString("headerMoreAddrsTooltip");
-    let remainingAddresses =
-      addresses.length - aShownAddrsNum - maxTooltipAddrsNum;
-    let moreForm = mc.window.PluralForm.get(remainingAddresses, words).replace(
-      "#1",
-      remainingAddresses
-    );
-    Assert.equal(
-      moreForm,
-      ", " + moreTooltipSplit[moreTooltipSplit.length - 1]
-    );
-  }
-}
-
-/**
  * Test the marking of a message as starred, be sure the header is properly
  * updated and changing selected message doesn't affect the state of others.
  */
 add_task(async function test_starred_message() {
   be_in_folder(folder);
 
   // Select the last message, which will display it.
   let curMessage = select_click_row(-1);
   wait_for_message_display_completion(mc);
   assert_selected_and_displayed(mc, curMessage);
 
-  let starButton = mc.window.document.getElementById("starMessageButton");
+  let starButton = document.getElementById("starMessageButton");
   // The message shouldn't be starred.
   Assert.ok(
     !starButton.classList.contains("flagged"),
     "The message is not starred"
   );
 
   // Press s to mark the message as starred.
   EventUtils.synthesizeKey("s", {});
--- a/mail/test/browser/multiple-identities/browser_displayNames.js
+++ b/mail/test/browser/multiple-identities/browser_displayNames.js
@@ -115,23 +115,22 @@ function ensure_multiple_identities() {
 
 function help_test_display_name(message, field, expectedValue) {
   // Switch to a decoy folder first to ensure that we refresh the message we're
   // looking at in order to update information changed in address book entries.
   be_in_folder(decoyFolder);
   be_in_folder(folder);
   select_click_row(message);
 
-  let value = mc
-    .e("expanded" + field + "Box", { tagName: "mail-emailaddress" })
-    .querySelector(".emaillabel").value;
-
-  if (value != expectedValue) {
-    throw new Error("got '" + value + "' but expected '" + expectedValue + "'");
-  }
+  Assert.equal(
+    document.querySelector(`#expanded${field}Box .header-recipient`)
+      .textContent,
+    expectedValue,
+    "The expected value matches the found value"
+  );
 }
 
 add_task(function test_single_identity() {
   ensure_no_card_exists(myEmail);
   ensure_single_identity();
   help_test_display_name(0, "to", headertoFieldMe);
 });
 
--- a/mail/themes/shared/jar.inc.mn
+++ b/mail/themes/shared/jar.inc.mn
@@ -522,8 +522,10 @@
   skin/classic/messenger/icons/new/touch/address-book.svg     (../shared/mail/icons/new/touch/address-book.svg)
   skin/classic/messenger/icons/new/touch/calendar.svg         (../shared/mail/icons/new/touch/calendar.svg)
   skin/classic/messenger/icons/new/touch/chat.svg             (../shared/mail/icons/new/touch/chat.svg)
   skin/classic/messenger/icons/new/touch/collapse.svg         (../shared/mail/icons/new/touch/collapse.svg)
   skin/classic/messenger/icons/new/touch/mail.svg             (../shared/mail/icons/new/touch/mail.svg)
   skin/classic/messenger/icons/new/touch/overflow.svg         (../shared/mail/icons/new/touch/overflow.svg)
   skin/classic/messenger/icons/new/touch/settings.svg         (../shared/mail/icons/new/touch/settings.svg)
   skin/classic/messenger/icons/new/touch/tasks.svg            (../shared/mail/icons/new/touch/tasks.svg)
+
+  skin/classic/messenger/icons/new/not-in-address-book.svg    (../shared/mail/icons/new/not-in-address-book.svg)
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/new/not-in-address-book.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+  - License, v. 2.0. If a copy of the MPL was not distributed with this
+  - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" fill-opacity="context-fill-opacity" viewBox="0 0 12 12">
+  <path d="M6 0C2.692 0 0 2.692 0 6s2.692 6 6 6 6-2.692 6-6-2.692-6-6-6Zm0 1c2.767 0 5 2.233 5 5a4.983 4.983 0 0 1-1 3.004V8.5C10 7.678 9.322 7 8.5 7h-.373c.516-.549.87-1.233.873-1.998A.5.5 0 0 0 9 5v-.006a.5.5 0 0 0 0-.002A3.006 3.006 0 0 0 5.996 2 3.008 3.008 0 0 0 3 5.002a.5.5 0 0 0 0 .002c.004.765.358 1.448.873 1.996H3.5C2.678 7 2 7.678 2 8.5v.504A4.983 4.983 0 0 1 1 6c0-2.767 2.233-5 5-5Zm-.004 2A1.994 1.994 0 0 1 8 4.996v.002a1.999 1.999 0 0 1-.8 1.592.5.5 0 0 0-.2.4v.51a.5.5 0 0 0 .5.5h1c.286 0 .5.214.5.5V10a.5.5 0 0 0 0 .002A4.982 4.982 0 0 1 6 11a4.982 4.982 0 0 1-3-.998A.5.5 0 0 0 3 10V8.5c0-.286.214-.5.5-.5h1a.5.5 0 0 0 .5-.5v-.51a.5.5 0 0 0-.2-.4A1.998 1.998 0 0 1 4 5.002V5c0-1.109.887-1.998 1.996-2Z" fill="context-stroke"/>
+  <path d="M6 1a5 5 0 0 0-4.207 7.703A5 5 0 0 0 2 8.98V8.5C2 7.678 2.678 7 3.5 7h.373c-.515-.548-.87-1.231-.873-1.996v-.002A3.008 3.008 0 0 1 5.996 2 3.006 3.006 0 0 1 9 4.994v.008c-.003.765-.357 1.45-.873 1.998H8.5c.822 0 1.5.678 1.5 1.5v.479c.072-.09.141-.182.207-.276a5 5 0 0 0 .334-.613l.008-.014.004-.008a5 5 0 0 0 .24-.65c0-.003.003-.006.004-.01l.002-.01a5 5 0 0 0 .148-.677l.002-.01V6.7a5.01 5.01 0 0 0-.045-1.676 5 5 0 0 0-4.392-4c-.007 0-.015 0-.022-.002a5.006 5.006 0 0 0-.232-.017h-.012A4.996 4.996 0 0 0 6 1Z" fill="context-fill"/>
+</svg>
+
--- a/mail/themes/shared/mail/messageHeader.css
+++ b/mail/themes/shared/mail/messageHeader.css
@@ -7,28 +7,25 @@
 .main-header-area {
   border-bottom-style: none;
   display: block;
 }
 
 .message-header-container,
 .message-header-extra-container {
   display: grid;
-  row-gap: 3px;
+  row-gap: 6px;
 }
 
 .message-header-container {
   padding: 3px;
 }
 
 .message-header-row:not([hidden]) {
   display: flex;
-  align-items: baseline;
-  min-height: 1.7em;
-  line-height: 1.3;
 }
 
 .message-header-wrap {
   flex-wrap: wrap;
 }
 
 .message-header-row.header-row-reverse {
   flex-direction: row-reverse;
@@ -41,32 +38,29 @@
 
 .header-buttons-container {
   display: flex;
   justify-content: end;
   flex-wrap: wrap;
 }
 
 .header-row-grow:not([hidden]) {
-  flex: 1;
+  flex: 1 1 auto;
   display: flex;
-  align-items: baseline;
-}
-
-.message-header-multi-field {
-  flex: 1;
+  align-items: center;
 }
 
 #mail-notification-top {
   border-bottom: 1px solid var(--splitter-color);
 }
 
 /* ::::: msg header toolbars ::::: */
 
-#messageHeader[show_header_mode="all"] {
+#messageHeader[show_header_mode="all"],
+#messageHeader.scrollable {
   overflow-y: auto;
   overflow-x: hidden;
   max-height: 14em;
 }
 
 #expandedBoxSpacer {
   display: block;
   height: 4px;
@@ -308,26 +302,25 @@ mail-tagfield[collapsed="true"] {
 /* ::::: msg header captions ::::: */
 
 .message-header-label {
   padding: 0;
   margin-block: 0;
   margin-inline: 6px 8px;
   text-align: end;
   flex-shrink: 0;
-  align-self: flex-start;
+  align-self: baseline;
 }
 
 .message-header-label.header-pill-label {
-  /* 2px is to match the sum of the padding-block-start: 1px and
-    border-block-start-width: 1px of .emailDisplayButton. */
+  /* 2px is to match the padding of the .header-recipient */
   padding-block-start: 2px;
 }
 
-mail-multi-emailheaderfield, mail-newsgroups-headerfield {
+mail-newsgroups-headerfield {
   /* The margin by 3px is to 'undo' the padding-inline-start: 2px and
     border-inline-start-width: 1px of .emailDisplayButton. */
   margin-inline-start: -3px;
 }
 
 .message-header-label,
 #attachmentSize {
   opacity: 0.7;
@@ -339,18 +332,21 @@ mail-multi-emailheaderfield, mail-newsgr
 
 .headerValue {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   margin: 0;
 }
 
-mail-headerfield.headerValue:focus-visible:not(:hover) {
-  outline: 1px solid var(--focus-outline-color);
+.header-row:focus-visible:not(:hover),
+.header-recipient:focus-visible:not(:hover),
+.header-newsgroup:focus-visible:not(:hover) {
+  outline: 2px solid var(--focus-outline-color);
+  outline-offset: -1px;
 }
 
 /* Separator for multifield emailaddress/newsgroup and messageid header rows. */
 .emailDisplayButton:not(:last-child):after,
 .messageIdDisplayButton:not(:last-child):after {
   content: ",";
 }
 
@@ -397,16 +393,17 @@ mail-newsgroups-headerfield {
   border-color: color-mix(in srgb, currentColor 50%, transparent);
 }
 
 .message-header-datetime {
   user-select: text;
   -moz-user-focus: normal;
   cursor: text;
   margin: 0 6px;
+  white-space: nowrap;
 }
 
 #expandedtoRow .message-header-datetime {
   align-self: flex-start;
   margin-block: 2px;
 }
 
 /* ::::: msg header email addresses ::::: */
@@ -524,26 +521,26 @@ mail-messageids-headerfield button.email
 }
 
 @media (prefers-reduced-motion: no-preference) {
   .emailToggleHeaderfield {
     transition: transform 200ms ease;
   }
 }
 
-mail-headerfield {
+.header-row {
   -moz-user-focus: normal;
   user-select: text;
   word-wrap: anywhere;
   display: inherit;
 }
 
-mail-headerfield:focus-visible:not(:hover) {
-  outline: 1px solid var(--focus-outline-color);
-  outline-offset: -1px;
+.screen-reader-only {
+  position: absolute;
+  clip-path: inset(50%);
 }
 
 #attachmentView {
   display: flex;
   flex-direction: column;
   justify-content: stretch;
   /* Allow the area to shrink. */
   min-width: 0;
@@ -798,24 +795,126 @@ button.email-action-flagged.flagged {
 
 .message-header-large-subject #expandedsubjectBox {
   font-weight: 500;
   font-size: 1.35em;
   line-height: 1.35em;
 }
 
 .message-header-large-subject #expandedsubjectLabel {
-  margin-top: 3px;
+  margin-top: 5px;
 }
 
 .message-header-buttons-only-icons .toolbarbutton-text,
 .message-header-buttons-only-text .toolbarbutton-icon {
   display: none !important;
 }
 
 .message-header-buttons-only-text .toolbarbutton-text {
   margin: 0 !important;
   padding-inline: 2px !important;
 }
 
 .message-header-buttons-only-icons .toolbarbutton-menu-dropmarker {
   margin-inline: 3px;
 }
+
+/* Header row widgets */
+
+.multi-recipient-row {
+  flex: 1 1 auto;
+}
+
+.header-recipient:not(:last-child,.last-before-button):after,
+.header-newsgroup:not(:last-child):after {
+  content: ",";
+}
+
+.header-recipient,
+.header-newsgroup {
+  display: flow-root;
+  padding: 2px;
+  border-radius: 3px;
+}
+
+.header-recipient {
+  white-space: nowrap;
+}
+
+.header-recipient:hover,
+.header-newsgroup:hover {
+  color: SelectedItemText;
+  background-color: SelectedItem;
+  cursor: pointer;
+}
+
+.header-recipient span,
+.header-recipient img {
+  pointer-events: none;
+}
+
+.header-recipient span {
+  word-break: break-word;
+  white-space: break-spaces;
+}
+
+.recipients-list,
+.newsgroups-list {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  row-gap: 2px;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+  margin-inline-start: -2px; /* Matches the 2px padding of the .header-recipient */
+  min-width: 100px;
+}
+
+.recipient-address-book-button {
+  margin: 0;
+  margin-inline-start: 3px;
+  margin-block-start: -2px;
+  padding: 1px;
+  border-radius: 3px;
+  cursor: pointer;
+  vertical-align: middle;
+}
+
+.recipient-address-book-button:not([disabled]):hover {
+  background-color: transparent;
+}
+
+.recipient-address-book-button:not([disabled]):hover img {
+  fill: color-mix(in srgb, SelectedItemText 20%, transparent);
+  stroke: -moz-hyperlinktext;
+}
+
+.recipient-address-book-button img {
+  -moz-context-properties: fill, stroke;
+  fill: color-mix(in srgb, currentColor 20%, transparent);
+  stroke: currentColor;
+  display: block;
+}
+
+.recipient-address-book-button.in-address-book img {
+  fill: color-mix(in srgb, var(--toolbarbutton-icon-fill-attention) 20%, transparent);
+  stroke: var(--toolbarbutton-icon-fill-attention);
+}
+
+.header-recipient:hover .recipient-address-book-button.in-address-book img {
+  fill: color-mix(in srgb, SelectedItemText 20%, transparent);
+  stroke: SelectedItemText;
+}
+
+.show-more-recipients {
+  padding: 3px 9px;
+  margin-inline-start: 6px !important;
+  min-height: auto;
+  min-width: auto;
+  border-radius: 12px;
+  line-height: 1;
+  font-size: 0.9rem;
+  text-transform: uppercase;
+  font-weight: 600;
+  background-color: SelectedItem;
+  color: SelectedItemText;
+}
--- a/mail/themes/shared/mail/popupPanel.css
+++ b/mail/themes/shared/mail/popupPanel.css
@@ -8,24 +8,32 @@
   text-align: end;
 }
 
 #editContactHeader {
   display: flex;
 }
 
 #editContactPanelIcon {
-  -moz-context-properties: fill;
-  fill: var(--toolbarbutton-icon-fill-attention);
+  -moz-context-properties: fill, stroke, stroke-opacity;
+  fill: color-mix(in srgb, var(--toolbarbutton-icon-fill-attention) 20%, transparent);
+  stroke: var(--toolbarbutton-icon-fill-attention);
   width: 20px;
   height: 20px;
   margin-block: 0 15px;
   margin-inline: 4px 10px;
 }
 
+#editContactPanelTitle {
+  font-size: 130%;
+  font-weight: bold;
+  margin-inline-start: 9px;
+  margin-inline-end: 12px;
+}
+
 #editContactContent {
   margin-block: 6px 15px;
   display: grid;
   grid-template-columns: auto 1fr;
   align-items: center;
 }
 
 #editContactEmail {