summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/msgHdrView.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mail/base/content/msgHdrView.js4501
1 files changed, 4501 insertions, 0 deletions
diff --git a/comm/mail/base/content/msgHdrView.js b/comm/mail/base/content/msgHdrView.js
new file mode 100644
index 0000000000..1579cddbfe
--- /dev/null
+++ b/comm/mail/base/content/msgHdrView.js
@@ -0,0 +1,4501 @@
+/* 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/. */
+
+/**
+ * Functions related to displaying the headers for a selected message in the
+ * message pane.
+ */
+
+/* import-globals-from ../../../../toolkit/content/contentAreaUtils.js */
+/* import-globals-from ../../../calendar/base/content/imip-bar.js */
+/* import-globals-from ../../../mailnews/extensions/newsblog/newsblogOverlay.js */
+/* import-globals-from ../../extensions/smime/content/msgHdrViewSMIMEOverlay.js */
+/* import-globals-from aboutMessage.js */
+/* import-globals-from editContactPanel.js */
+/* import-globals-from globalOverlay.js */
+/* import-globals-from mailContext.js */
+/* import-globals-from mail-offline.js */
+/* import-globals-from mailCore.js */
+/* import-globals-from msgSecurityPane.js */
+
+/* globals MozElements */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AttachmentInfo: "resource:///modules/AttachmentInfo.sys.mjs",
+ PluralForm: "resource://gre/modules/PluralForm.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ calendarDeactivator:
+ "resource:///modules/calendar/calCalendarDeactivator.jsm",
+ Gloda: "resource:///modules/gloda/GlodaPublic.jsm",
+ GlodaUtils: "resource:///modules/gloda/GlodaUtils.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ MessageArchiver: "resource:///modules/MessageArchiver.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gDbService",
+ "@mozilla.org/msgDatabase/msgDBService;1",
+ "nsIMsgDBService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gEncryptedSMIMEURIsService",
+ "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1",
+ Ci.nsIEncryptedSMIMEURIsService
+);
+
+// Warning: It's critical that the code in here for displaying the message
+// headers for a selected message remain as fast as possible. In particular,
+// right now, we only introduce one reflow per message. i.e. if you click on
+// a message in the thread pane, we batch up all the changes for displaying
+// the header pane (to, cc, attachments button, etc.) and we make a single
+// pass to display them. It's critical that we maintain this one reflow per
+// message view in the message header pane.
+
+var gViewAllHeaders = false;
+var gMinNumberOfHeaders = 0;
+var gDummyHeaderIdIndex = 0;
+var gBuildAttachmentsForCurrentMsg = false;
+var gBuiltExpandedView = false;
+var gHeadersShowReferences = false;
+
+/**
+ * Show the friendly display names for people I know,
+ * instead of the name + email address.
+ */
+var gShowCondensedEmailAddresses;
+
+/**
+ * Other components may listen to on start header & on end header notifications
+ * for each message we display: to do that you need to add yourself to our
+ * gMessageListeners array with an object that supports the three properties:
+ * onStartHeaders, onEndHeaders and onEndAttachments.
+ *
+ * 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 = [];
+
+/**
+ * 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. 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.
+ */
+const gExpandedHeaderList = [
+ { name: "subject" },
+ { 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: "content-base" },
+ { name: "tags", outputFunction: outputTags },
+];
+
+/**
+ * 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",
+ "toCcBcc",
+];
+
+/**
+ * Now, for each view the message pane can generate, we need a global table of
+ * headerEntries. These header entry objects are generated dynamically based on
+ * the static data in the header lists (see above) and elements we find in the
+ * DOM based on properties in the header lists.
+ */
+var gExpandedHeaderView = {};
+
+/**
+ * This is an array of header name and value pairs for the currently displayed
+ * message. It's purely a data object and has no view information. View
+ * information is contained in the view objects.
+ * For a given entry in this array you can ask for:
+ * .headerName name of the header (i.e. 'to'). Always stored in lower case
+ * .headerValue value of the header "johndoe@example.com"
+ */
+var currentHeaderData = {};
+
+/**
+ * CurrentAttachments is an array of AttachmentInfo objects.
+ */
+var currentAttachments = [];
+
+/**
+ * The character set of the message, according to the MIME parser.
+ */
+var currentCharacterSet = "";
+
+/**
+ * Folder database listener object. This is used alongside the
+ * nsIDBChangeListener implementation in order to listen for the changes of the
+ * messages' flags that don't trigger a messageHeaderSink.processHeaders().
+ * For now, it's used only for the flagged/marked/starred flag, but it could be
+ * extended to handle other flags changes and remove the full header reload.
+ */
+var gFolderDBListener = null;
+
+// Timer to mark read, if the user has configured the app to mark a message as
+// read if it is viewed for more than n seconds.
+var gMarkViewedMessageAsReadTimer = null;
+
+// Per message header flags to keep track of whether the user is allowing remote
+// content for a particular message.
+// if you change or add more values to these constants, be sure to modify
+// the corresponding definitions in nsMsgContentPolicy.cpp
+var kNoRemoteContentPolicy = 0;
+var kBlockRemoteContent = 1;
+var kAllowRemoteContent = 2;
+
+class FolderDBListener {
+ constructor(folder) {
+ // Keep a record of the currently selected folder to check when the
+ // selection changes to avoid initializing the DBListener in case the same
+ // folder is selected.
+ this.selectedFolder = folder;
+ this.isRegistered = false;
+ }
+
+ register() {
+ gDbService.registerPendingListener(this.selectedFolder, this);
+ this.isRegistered = true;
+ }
+
+ unregister() {
+ gDbService.unregisterPendingListener(this);
+ this.isRegistered = false;
+ }
+
+ /** @implements {nsIDBChangeListener} */
+ onHdrFlagsChanged(hdrChanged, oldFlags, newFlags, instigator) {
+ // Bail out if the changed message isn't the one currently displayed.
+ if (hdrChanged != gMessage) {
+ return;
+ }
+
+ // Check if the flagged/marked/starred state was changed.
+ if (
+ newFlags & Ci.nsMsgMessageFlags.Marked ||
+ oldFlags & Ci.nsMsgMessageFlags.Marked
+ ) {
+ updateStarButton();
+ }
+ }
+ onHdrDeleted(hdrChanged, parentKey, flags, instigator) {}
+ onHdrAdded(hdrChanged, parentKey, flags, instigator) {}
+ onParentChanged(keyChanged, oldParent, newParent, instigator) {}
+ onAnnouncerGoingAway(instigator) {}
+ onReadChanged(instigator) {}
+ onJunkScoreChanged(instigator) {}
+ onHdrPropertyChanged(hdrToChange, property, preChange, status, instigator) {
+ // Not interested before a change, or if the message isn't the one displayed,
+ // or an .eml file from disk or an attachment.
+ if (preChange || gMessage != hdrToChange) {
+ return;
+ }
+ switch (property) {
+ case "keywords":
+ OnTagsChange();
+ break;
+ case "junkscore":
+ HandleJunkStatusChanged(hdrToChange);
+ break;
+ }
+ }
+ onEvent(db, event) {}
+}
+
+/**
+ * Initialize the nsIDBChangeListener when a new folder is selected in order to
+ * listen for any flags change happening in the currently displayed messages.
+ */
+function initFolderDBListener() {
+ // Bail out if we don't have a selected message, or we already have a
+ // DBListener initialized and the folder didn't change.
+ if (
+ !gFolder ||
+ (gFolderDBListener?.isRegistered &&
+ gFolderDBListener.selectedFolder == gFolder)
+ ) {
+ return;
+ }
+
+ // Clearly we are viewing a different message in a different folder, so clear
+ // any remaining of the old DBListener.
+ clearFolderDBListener();
+
+ gFolderDBListener = new FolderDBListener(gFolder);
+ gFolderDBListener.register();
+}
+
+/**
+ * Unregister the listener and clear the object if we already have one, meaning
+ * the user just changed folder or deselected all messages.
+ */
+function clearFolderDBListener() {
+ if (gFolderDBListener?.isRegistered) {
+ gFolderDBListener.unregister();
+ gFolderDBListener = null;
+ }
+}
+
+/**
+ * Our class constructor method which creates a header Entry based on an entry
+ * in one of the header lists. A header entry is different from a header list.
+ * A header list just describes how you want a particular header to be
+ * 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) {
+ this.enclosingBox = document.getElementById(
+ `${prefix}${headerListInfo.name}Box`
+ );
+ this.enclosingRow = this.enclosingBox.closest(".message-header-row");
+ this.isNewHeader = false;
+ this.valid = false;
+ 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) {
+ gExpandedHeaderView[header.name] = new MsgHeaderEntry("expanded", header);
+ }
+
+ let extraHeaders = Services.prefs
+ .getCharPref("mailnews.headers.extraExpandedHeaders")
+ .split(" ");
+ for (let extraHeaderName of extraHeaders) {
+ if (!extraHeaderName.trim()) {
+ continue;
+ }
+ gExpandedHeaderView[extraHeaderName.toLowerCase()] = new HeaderView(
+ extraHeaderName,
+ extraHeaderName
+ );
+ }
+
+ let otherHeaders = Services.prefs
+ .getCharPref("mail.compose.other.header", "")
+ .split(",")
+ .map(h => h.trim())
+ .filter(Boolean);
+
+ for (let otherHeaderName of otherHeaders) {
+ gExpandedHeaderView[otherHeaderName.toLowerCase()] = new HeaderView(
+ otherHeaderName,
+ otherHeaderName
+ );
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showOrganization")) {
+ var organizationEntry = {
+ name: "organization",
+ outputFunction: updateHeaderValue,
+ };
+ gExpandedHeaderView[organizationEntry.name] = new MsgHeaderEntry(
+ "expanded",
+ organizationEntry
+ );
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showUserAgent")) {
+ var userAgentEntry = {
+ name: "user-agent",
+ outputFunction: updateHeaderValue,
+ };
+ gExpandedHeaderView[userAgentEntry.name] = new MsgHeaderEntry(
+ "expanded",
+ userAgentEntry
+ );
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showMessageId")) {
+ var messageIdEntry = {
+ name: "message-id",
+ outputFunction: outputMessageIds,
+ };
+ gExpandedHeaderView[messageIdEntry.name] = new MsgHeaderEntry(
+ "expanded",
+ messageIdEntry
+ );
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showSender")) {
+ let senderEntry = {
+ name: "sender",
+ outputFunction: outputEmailAddresses,
+ };
+ gExpandedHeaderView[senderEntry.name] = new MsgHeaderEntry(
+ "expanded",
+ senderEntry
+ );
+ }
+}
+
+async function OnLoadMsgHeaderPane() {
+ // Load any preferences that at are global with regards to
+ // displaying a message...
+ gMinNumberOfHeaders = Services.prefs.getIntPref(
+ "mailnews.headers.minNumHeaders"
+ );
+ gShowCondensedEmailAddresses = Services.prefs.getBoolPref(
+ "mail.showCondensedAddresses"
+ );
+ gHeadersShowReferences = Services.prefs.getBoolPref(
+ "mailnews.headers.showReferences"
+ );
+
+ Services.obs.addObserver(MsgHdrViewObserver, "remote-content-blocked");
+ Services.prefs.addObserver("mail.showCondensedAddresses", MsgHdrViewObserver);
+ Services.prefs.addObserver(
+ "mailnews.headers.showReferences",
+ MsgHdrViewObserver
+ );
+
+ initializeHeaderViewTables();
+
+ // Add the keyboard shortcut event listener for the message header.
+ // Ctrl+Alt+S / Cmd+Control+S. We don't use the Alt/Option key on macOS
+ // because it alters the pressed key to an ASCII character. See bug 1692263.
+ let shortcut = await document.l10n.formatValue(
+ "message-header-show-security-info-key"
+ );
+ document.addEventListener("keypress", event => {
+ if (
+ event.ctrlKey &&
+ (event.altKey || event.metaKey) &&
+ event.key.toLowerCase() == shortcut.toLowerCase()
+ ) {
+ showMessageReadSecurityInfo();
+ }
+ });
+
+ headerToolbarNavigation.init();
+
+ // Set up event listeners for the encryption technology button and panel.
+ document
+ .getElementById("encryptionTechBtn")
+ .addEventListener("click", showMessageReadSecurityInfo);
+ let panel = document.getElementById("messageSecurityPanel");
+ panel.addEventListener("popuphidden", onMessageSecurityPopupHidden);
+
+ // Set the flag/star button on click listener.
+ document
+ .getElementById("starMessageButton")
+ .addEventListener("click", MsgMarkAsFlagged);
+
+ // Dispatch an event letting any listeners know that we have loaded
+ // the message pane.
+ let headerViewElement = document.getElementById("msgHeaderView");
+ headerViewElement.loaded = true;
+ headerViewElement.dispatchEvent(
+ new Event("messagepane-loaded", { bubbles: false, cancelable: true })
+ );
+
+ getMessagePaneBrowser().addProgressListener(
+ messageProgressListener,
+ Ci.nsIWebProgress.NOTIFY_STATE_ALL
+ );
+
+ gHeaderCustomize.init();
+}
+
+function OnUnloadMsgHeaderPane() {
+ let headerViewElement = document.getElementById("msgHeaderView");
+ if (!headerViewElement.loaded) {
+ // We're unloading, but we never loaded.
+ return;
+ }
+
+ Services.obs.removeObserver(MsgHdrViewObserver, "remote-content-blocked");
+ Services.prefs.removeObserver(
+ "mail.showCondensedAddresses",
+ MsgHdrViewObserver
+ );
+ Services.prefs.removeObserver(
+ "mailnews.headers.showReferences",
+ MsgHdrViewObserver
+ );
+
+ 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 })
+ );
+}
+
+var MsgHdrViewObserver = {
+ observe(subject, topic, data) {
+ // verify that we're changing the mail pane config pref
+ if (topic == "nsPref:changed") {
+ // We don't need to call ReloadMessage() in either of these conditions
+ // because a preference observer for these preferences already does it.
+ if (data == "mail.showCondensedAddresses") {
+ gShowCondensedEmailAddresses = Services.prefs.getBoolPref(
+ "mail.showCondensedAddresses"
+ );
+ } else if (data == "mailnews.headers.showReferences") {
+ gHeadersShowReferences = Services.prefs.getBoolPref(
+ "mailnews.headers.showReferences"
+ );
+ }
+ } else if (topic == "remote-content-blocked") {
+ let browser = getMessagePaneBrowser();
+ if (
+ browser.browsingContext.id == data ||
+ browser.browsingContext == BrowsingContext.get(data)?.top
+ ) {
+ gMessageNotificationBar.setRemoteContentMsg(
+ null,
+ subject,
+ !gEncryptedSMIMEURIsService.isEncrypted(browser.currentURI.spec)
+ );
+ }
+ }
+ },
+};
+
+/**
+ * Receives a message's headers as we display the message through our mime converter.
+ *
+ * @see {nsIMailChannel}
+ * @implements {nsIMailProgressListener}
+ * @implements {nsIWebProgressListener}
+ * @implements {nsISupportsWeakReference}
+ */
+var messageProgressListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMailProgressListener",
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ /**
+ * Step 1: A message has started loading (if the flags include STATE_START).
+ *
+ * @param {nsIWebProgress} webProgress
+ * @param {nsIRequest} request
+ * @param {integer} stateFlags
+ * @param {nsresult} status
+ * @see {nsIWebProgressListener}
+ */
+ onStateChange(webProgress, request, stateFlags, status) {
+ if (
+ !(request instanceof Ci.nsIMailChannel) ||
+ !(stateFlags & Ci.nsIWebProgressListener.STATE_START)
+ ) {
+ return;
+ }
+
+ // Clear the previously displayed message.
+ const previousDocElement =
+ getMessagePaneBrowser().contentDocument?.documentElement;
+ if (previousDocElement) {
+ previousDocElement.style.display = "none";
+ }
+ ClearAttachmentList();
+ gMessageNotificationBar.clearMsgNotifications();
+
+ request.listener = this;
+ request.smimeHeaderSink = smimeHeaderSink;
+ this.onStartHeaders();
+ },
+
+ /**
+ * Step 2: The message headers are available on the channel.
+ *
+ * @param {nsIMailChannel} mailChannel
+ * @see {nsIMailProgressListener}
+ */
+ onHeadersComplete(mailChannel) {
+ const domWindow = getMessagePaneBrowser().docShell.DOMWindow;
+ domWindow.addEventListener(
+ "DOMContentLoaded",
+ event => this.onDOMContentLoaded(event),
+ { once: true }
+ );
+ this.processHeaders(mailChannel.headerNames, mailChannel.headerValues);
+ },
+
+ /**
+ * Step 3: The parser has finished reading the body of the message.
+ *
+ * @param {nsIMailChannel} mailChannel
+ * @see {nsIMailProgressListener}
+ */
+ onBodyComplete(mailChannel) {
+ autoMarkAsRead();
+ },
+
+ /**
+ * Step 4: The attachment information is available on the channel.
+ *
+ * @param {nsIMailChannel} mailChannel
+ * @see {nsIMailProgressListener}
+ */
+ onAttachmentsComplete(mailChannel) {
+ for (const attachment of mailChannel.attachments) {
+ this.handleAttachment(
+ attachment.getProperty("contentType"),
+ attachment.getProperty("url"),
+ attachment.getProperty("displayName"),
+ attachment.getProperty("uri"),
+ attachment.getProperty("notDownloaded")
+ );
+ for (const key of [
+ "X-Mozilla-PartURL",
+ "X-Mozilla-PartSize",
+ "X-Mozilla-PartDownloaded",
+ "Content-Description",
+ "Content-Type",
+ "Content-Encoding",
+ ]) {
+ if (attachment.hasKey(key)) {
+ this.addAttachmentField(key, attachment.getProperty(key));
+ }
+ }
+ }
+ },
+
+ /**
+ * Step 5: The message HTML is complete, but external resources such as may
+ * not have loaded yet. The docShell will handle them – for our purposes,
+ * message loading has finished.
+ */
+ onDOMContentLoaded(event) {
+ const { docShell } = event.target.ownerGlobal;
+ if (!docShell.isTopLevelContentDocShell) {
+ return;
+ }
+
+ const channel = docShell.currentDocumentChannel;
+ channel.QueryInterface(Ci.nsIMailChannel);
+ currentCharacterSet = channel.mailCharacterSet;
+ channel.smimeHeaderSink = null;
+ if (channel.imipItem) {
+ calImipBar.showImipBar(channel.imipItem, channel.imipMethod);
+ }
+ this.onEndAllAttachments();
+ const uri = channel.URI.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ this.onEndMsgHeaders(uri);
+ this.onEndMsgDownload(uri);
+ },
+
+ onStartHeaders() {
+ // Every time we start to redisplay a message, check the view all headers
+ // pref...
+ let showAllHeadersPref = Services.prefs.getIntPref("mail.show_headers");
+ if (showAllHeadersPref == 2) {
+ // eslint-disable-next-line no-global-assign
+ gViewAllHeaders = true;
+ } else {
+ if (gViewAllHeaders) {
+ // If we currently are in view all header mode, rebuild our header
+ // view so we remove most of the header data.
+ hideHeaderView(gExpandedHeaderView);
+ RemoveNewHeaderViews(gExpandedHeaderView);
+ gDummyHeaderIdIndex = 0;
+ // eslint-disable-next-line no-global-assign
+ gExpandedHeaderView = {};
+ initializeHeaderViewTables();
+ }
+
+ // eslint-disable-next-line no-global-assign
+ gViewAllHeaders = false;
+ }
+
+ document.title = "";
+ ClearCurrentHeaders();
+ gBuiltExpandedView = false;
+ gBuildAttachmentsForCurrentMsg = false;
+ ClearAttachmentList();
+ gMessageNotificationBar.clearMsgNotifications();
+
+ // Reset the blocked hosts so we can populate it again for this message.
+ document.getElementById("remoteContentOptions").value = "";
+
+ for (let listener of gMessageListeners) {
+ listener.onStartHeaders();
+ }
+ },
+
+ onEndHeaders() {
+ if (!gViewWrapper || !gMessage) {
+ // The view wrapper and/or message went away before we finished loading
+ // the message. Bail out.
+ return;
+ }
+
+ // Give add-ons a chance to modify currentHeaderData before it actually
+ // gets displayed.
+ for (let listener of gMessageListeners) {
+ if ("onBeforeShowHeaderPane" in listener) {
+ listener.onBeforeShowHeaderPane();
+ }
+ }
+
+ // Load feed web page if so configured. This entry point works for
+ // messagepane loads in 3pane folder tab, 3pane message tab, and the
+ // standalone message window.
+ if (!FeedMessageHandler.shouldShowSummary(gMessage, false)) {
+ FeedMessageHandler.setContent(gMessage, false);
+ }
+
+ ShowMessageHeaderPane();
+ // WARNING: This is the ONLY routine inside of the message Header Sink
+ // that should trigger a reflow!
+ ClearHeaderView(gExpandedHeaderView);
+
+ // Make sure there is a subject even if it's empty so we'll show the
+ // subject and the twisty.
+ EnsureSubjectValue();
+
+ // Make sure there is a from value even if empty so the header toolbar
+ // will show up.
+ EnsureFromValue();
+
+ // Only update the expanded view if it's actually selected and needs updating.
+ if (!gBuiltExpandedView) {
+ UpdateExpandedMessageHeaders();
+ }
+
+ gMessageNotificationBar.setDraftEditMessage();
+ updateHeaderToolbarButtons();
+
+ for (let listener of gMessageListeners) {
+ listener.onEndHeaders();
+ }
+ },
+
+ processHeaders(headerNames, headerValues) {
+ const kMailboxSeparator = ", ";
+ var index = 0;
+ for (let i = 0; i < headerNames.length; i++) {
+ let header = {
+ headerName: headerNames[i],
+ headerValue: headerValues[i],
+ };
+
+ // For consistency's sake, let us force all header names to be lower
+ // case so we don't have to worry about looking for: Cc and CC, etc.
+ var lowerCaseHeaderName = header.headerName.toLowerCase();
+
+ // If we have an x-mailer, x-mimeole, or x-newsreader string,
+ // put it in the user-agent slot which we know how to handle already.
+ if (/^x-(mailer|mimeole|newsreader)$/.test(lowerCaseHeaderName)) {
+ lowerCaseHeaderName = "user-agent";
+ }
+
+ // According to RFC 2822, certain headers can occur "unlimited" times.
+ if (lowerCaseHeaderName in currentHeaderData) {
+ // Sometimes, you can have multiple To or Cc lines....
+ // In this case, we want to append these headers into one.
+ if (lowerCaseHeaderName == "to" || lowerCaseHeaderName == "cc") {
+ currentHeaderData[lowerCaseHeaderName].headerValue =
+ currentHeaderData[lowerCaseHeaderName].headerValue +
+ "," +
+ header.headerValue;
+ } else {
+ // Use the index to create a unique header name like:
+ // received5, received6, etc
+ currentHeaderData[lowerCaseHeaderName + index++] = header;
+ }
+ } else {
+ currentHeaderData[lowerCaseHeaderName] = header;
+ }
+
+ // See RFC 5322 section 3.6 for min-max number for given header.
+ // If multiple headers exist we need to make sure to use the first one.
+ if (lowerCaseHeaderName == "subject" && !document.title) {
+ let fullSubject = "";
+ // Use the subject from the database, which may have been put there in
+ // decrypted form.
+ if (gMessage?.subject) {
+ if (gMessage.flags & Ci.nsMsgMessageFlags.HasRe) {
+ fullSubject = "Re: ";
+ }
+ fullSubject += gMessage.mime2DecodedSubject;
+ }
+ document.title = fullSubject || header.headerValue;
+ currentHeaderData.subject.headerValue = document.title;
+ }
+ } // while we have more headers to parse
+
+ // Process message tags as if they were headers in the message.
+ gMessageHeader.setTags();
+ updateStarButton();
+
+ if ("from" in currentHeaderData && "sender" in currentHeaderData) {
+ let senderMailbox =
+ kMailboxSeparator +
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.sender.headerValue
+ ) +
+ kMailboxSeparator;
+ let fromMailboxes =
+ kMailboxSeparator +
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.from.headerValue
+ ) +
+ kMailboxSeparator;
+ if (fromMailboxes.includes(senderMailbox)) {
+ delete currentHeaderData.sender;
+ }
+ }
+
+ // We don't need to show the reply-to header if its value is either
+ // the From field (totally pointless) or the To field (common for
+ // mailing lists, but not that useful).
+ if (
+ "from" in currentHeaderData &&
+ "to" in currentHeaderData &&
+ "reply-to" in currentHeaderData
+ ) {
+ let replyToMailbox =
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData["reply-to"].headerValue
+ );
+ let fromMailboxes =
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.from.headerValue
+ );
+ let toMailboxes = MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.to.headerValue
+ );
+
+ if (replyToMailbox == fromMailboxes || replyToMailbox == toMailboxes) {
+ delete currentHeaderData["reply-to"];
+ }
+ }
+
+ // For content-base urls stored uri encoded, we want to decode for
+ // display (and encode for external link open).
+ if ("content-base" in currentHeaderData) {
+ currentHeaderData["content-base"].headerValue = decodeURI(
+ currentHeaderData["content-base"].headerValue
+ );
+ }
+
+ let expandedfromLabel = document.getElementById("expandedfromLabel");
+ if (FeedUtils.isFeedMessage(gMessage)) {
+ expandedfromLabel.value = expandedfromLabel.getAttribute("valueAuthor");
+ } else {
+ expandedfromLabel.value = expandedfromLabel.getAttribute("valueFrom");
+ }
+
+ this.onEndHeaders();
+ },
+
+ handleAttachment(contentType, url, displayName, uri, isExternalAttachment) {
+ let newAttachment = new AttachmentInfo({
+ contentType,
+ url,
+ name: displayName,
+ uri,
+ isExternalAttachment,
+ message: gMessage,
+ updateAttachmentsDisplayFn: updateAttachmentsDisplay,
+ });
+ currentAttachments.push(newAttachment);
+
+ if (contentType == "application/pgp-keys" || displayName.endsWith(".asc")) {
+ Enigmail.msg.autoProcessPgpKeyAttachment(newAttachment);
+ }
+ },
+
+ addAttachmentField(field, value) {
+ let last = currentAttachments[currentAttachments.length - 1];
+ if (
+ field == "X-Mozilla-PartSize" &&
+ !last.isFileAttachment &&
+ !last.isDeleted
+ ) {
+ let size = parseInt(value);
+
+ if (last.isLinkAttachment) {
+ // Check if an external link attachment's reported size is sane.
+ // A size of < 2 isn't sensical so ignore such placeholder values.
+ // Don't accept a size with any non numerics. Also cap the number.
+ // We want the size to be checked again, upon user action, to make
+ // sure size is updated with an accurate value, so |sizeResolved|
+ // remains false.
+ if (isNaN(size) || size.toString().length != value.length || size < 2) {
+ last.size = -1;
+ } else if (size > Number.MAX_SAFE_INTEGER) {
+ last.size = Number.MAX_SAFE_INTEGER;
+ } else {
+ last.size = size;
+ }
+ } else {
+ // For internal or file (detached) attachments, save the size.
+ last.size = size;
+ // For external file attachments, we won't have a valid size.
+ if (!last.isFileAttachment && size > -1) {
+ last.sizeResolved = true;
+ }
+ }
+ } else if (field == "X-Mozilla-PartDownloaded" && value == "0") {
+ // We haven't downloaded the attachment, so any size we get from
+ // libmime is almost certainly inaccurate. Just get rid of it. (Note:
+ // this relies on the fact that PartDownloaded comes after PartSize from
+ // the MIME emitter.)
+ // Note: for imap parts_on_demand, a small size consisting of the part
+ // headers would have been returned above.
+ last.size = -1;
+ last.sizeResolved = false;
+ }
+ },
+
+ onEndAllAttachments() {
+ Enigmail.msg.notifyEndAllAttachments();
+
+ displayAttachmentsForExpandedView();
+
+ for (let listener of gMessageListeners) {
+ if ("onEndAttachments" in listener) {
+ listener.onEndAttachments();
+ }
+ }
+ },
+
+ /**
+ * This event is generated by nsMsgStatusFeedback when it gets an
+ * OnStateChange event for STATE_STOP. This is the same event that
+ * generates the "msgLoaded" property flag change event. This best
+ * corresponds to the end of the streaming process.
+ */
+ onEndMsgDownload(url) {
+ let browser = getMessagePaneBrowser();
+
+ // If we have no attachments, we hide the attachment icon in the message
+ // tree.
+ // PGP key attachments do not count as attachments for the purposes of the
+ // message tree, even though we still show them in the attachment list.
+ // Otherwise the attachment icon becomes less useful when someone receives
+ // lots of signed messages.
+ // We do the same if we only have text/vcard attachments because we
+ // *assume* the vcard attachment is a personal vcard (rather than an
+ // addressbook, or a shared contact) that is attached to every message.
+ // NOTE: There would be some obvious give-aways in the vcard content that
+ // this personal vcard assumption is incorrect (multiple contacts, or a
+ // contact with an address that is different from the sender address) but we
+ // do not have easy access to the attachment content here, so we just stick
+ // to the assumption.
+ // NOTE: If the message contains two vcard attachments (or more) then this
+ // would hint that one of the vcards is not personal, but we won't make an
+ // exception here to keep the implementation simple.
+ gMessage?.markHasAttachments(
+ currentAttachments.some(
+ att =>
+ att.contentType != "text/vcard" &&
+ att.contentType != "text/x-vcard" &&
+ att.contentType != "application/pgp-keys"
+ )
+ );
+
+ if (
+ currentAttachments.length &&
+ Services.prefs.getBoolPref("mail.inline_attachments") &&
+ FeedUtils.isFeedMessage(gMessage) &&
+ browser &&
+ browser.contentDocument &&
+ browser.contentDocument.body
+ ) {
+ for (let img of browser.contentDocument.body.getElementsByClassName(
+ "moz-attached-image"
+ )) {
+ for (let attachment of currentAttachments) {
+ let partID = img.src.split("&part=")[1];
+ partID = partID ? partID.split("&")[0] : null;
+ if (attachment.partID && partID == attachment.partID) {
+ img.src = attachment.url;
+ break;
+ }
+ }
+
+ img.addEventListener("load", function (event) {
+ if (this.clientWidth > this.parentNode.clientWidth) {
+ img.setAttribute("overflowing", "true");
+ img.setAttribute("shrinktofit", "true");
+ }
+ });
+ }
+ }
+
+ OnMsgParsed(url);
+ },
+
+ onEndMsgHeaders(url) {
+ if (!url.errorCode) {
+ // Should not mark a message as read if failed to load.
+ OnMsgLoaded(url);
+ }
+ },
+};
+
+/**
+ * Update the flagged (starred) state of the currently selected message.
+ */
+function updateStarButton() {
+ if (!gMessage || !gFolder) {
+ // No msgHdr to update, or we're dealing with an .eml.
+ document.getElementById("starMessageButton").hidden = true;
+ return;
+ }
+
+ let flagButton = document.getElementById("starMessageButton");
+ flagButton.hidden = false;
+
+ let isFlagged = gMessage.isFlagged;
+ flagButton.classList.toggle("flagged", isFlagged);
+ flagButton.setAttribute("aria-checked", isFlagged);
+}
+
+function EnsureSubjectValue() {
+ if (!("subject" in currentHeaderData)) {
+ let foo = {};
+ foo.headerValue = "";
+ foo.headerName = "subject";
+ currentHeaderData[foo.headerName] = foo;
+ }
+}
+
+function EnsureFromValue() {
+ if (!("from" in currentHeaderData)) {
+ let foo = {};
+ foo.headerValue = "";
+ foo.headerName = "from";
+ currentHeaderData[foo.headerName] = foo;
+ }
+}
+
+function OnTagsChange() {
+ // rebuild the tag headers
+ gMessageHeader.setTags();
+
+ // Now update the expanded header view to rebuild the tags,
+ // and then show or hide the tag header box.
+ if (gBuiltExpandedView) {
+ let headerEntry = gExpandedHeaderView.tags;
+ if (headerEntry) {
+ headerEntry.valid = "tags" in currentHeaderData;
+ if (headerEntry.valid) {
+ headerEntry.outputFunction(
+ headerEntry,
+ currentHeaderData.tags.headerValue
+ );
+ }
+
+ // we may need to collapse or show the tag header row...
+ headerEntry.enclosingRow.hidden = !headerEntry.valid;
+ // ... and ensure that all headers remain correctly aligned
+ gMessageHeader.syncLabelsColumnWidths();
+ }
+ }
+}
+
+/**
+ * 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];
+ headerEntry.enclosingBox.clearHeaderValues?.();
+ headerEntry.enclosingBox.clear?.();
+
+ headerEntry.valid = false;
+ }
+}
+
+/**
+ * Make sure that any valid header entry in the table is collapsed.
+ *
+ * @param aHeaderTable Table of header entries
+ */
+function hideHeaderView(aHeaderTable) {
+ for (let name in aHeaderTable) {
+ let headerEntry = aHeaderTable[name];
+ headerEntry.enclosingRow.hidden = true;
+ }
+}
+
+/**
+ * Make sure that any valid header entry in the table specified is visible.
+ *
+ * @param aHeaderTable Table of header entries
+ */
+function showHeaderView(aHeaderTable) {
+ for (let name in aHeaderTable) {
+ let headerEntry = aHeaderTable[name];
+ headerEntry.enclosingRow.hidden = !headerEntry.valid;
+
+ // If we're hiding the To field, we need to hide the date inline and show
+ // the duplicate on the subject line.
+ if (headerEntry.enclosingRow.id == "expandedtoRow") {
+ let dateLabel = document.getElementById("dateLabel");
+ let dateLabelSubject = document.getElementById("dateLabelSubject");
+ if (!headerEntry.valid) {
+ dateLabelSubject.setAttribute(
+ "datetime",
+ dateLabel.getAttribute("datetime")
+ );
+ dateLabelSubject.textContent = dateLabel.textContent;
+ dateLabelSubject.hidden = false;
+ } else {
+ dateLabelSubject.removeAttribute("datetime");
+ dateLabelSubject.textContent = "";
+ dateLabelSubject.hidden = true;
+ }
+ }
+ }
+}
+
+/**
+ * Enumerate through the list of headers and find the number that are visible
+ * add empty entries if we don't have the minimum number of rows.
+ */
+function EnsureMinimumNumberOfHeaders(headerTable) {
+ // 0 means we don't have a minimum... do nothing special
+ if (!gMinNumberOfHeaders) {
+ return;
+ }
+
+ var numVisibleHeaders = 0;
+ for (let name in headerTable) {
+ let headerEntry = headerTable[name];
+ if (headerEntry.valid) {
+ numVisibleHeaders++;
+ }
+ }
+
+ if (numVisibleHeaders < gMinNumberOfHeaders) {
+ // How many empty headers do we need to add?
+ var numEmptyHeaders = gMinNumberOfHeaders - numVisibleHeaders;
+
+ // We may have already dynamically created our empty rows and we just need
+ // to make them visible.
+ for (let index in headerTable) {
+ let headerEntry = headerTable[index];
+ if (index.startsWith("Dummy-Header") && numEmptyHeaders) {
+ headerEntry.valid = true;
+ numEmptyHeaders--;
+ }
+ }
+
+ // Ok, now if we have any extra dummy headers we need to add, create a new
+ // header widget for them.
+ while (numEmptyHeaders) {
+ var dummyHeaderId = "Dummy-Header" + gDummyHeaderIdIndex;
+ gExpandedHeaderView[dummyHeaderId] = new HeaderView(dummyHeaderId, "");
+ gExpandedHeaderView[dummyHeaderId].valid = true;
+
+ gDummyHeaderIdIndex++;
+ numEmptyHeaders--;
+ }
+ }
+}
+
+/**
+ * Make sure the appropriate fields in the expanded header view are collapsed
+ * or visible...
+ */
+function updateExpandedView() {
+ if (gMinNumberOfHeaders) {
+ EnsureMinimumNumberOfHeaders(gExpandedHeaderView);
+ }
+ showHeaderView(gExpandedHeaderView);
+
+ // Now that we have all the headers, ensure that the name columns of both
+ // grids are the same size so that they don't look weird.
+ gMessageHeader.syncLabelsColumnWidths();
+
+ UpdateReplyButtons();
+ updateHeaderToolbarButtons();
+ updateComposeButtons();
+ displayAttachmentsForExpandedView();
+
+ try {
+ AdjustHeaderView(Services.prefs.getIntPref("mail.show_headers"));
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+/**
+ * Default method for updating a header value into a header entry
+ *
+ * @param aHeaderEntry A single header from currentHeaderData
+ * @param aHeaderValue The new value for headerEntry
+ */
+function updateHeaderValue(aHeaderEntry, aHeaderValue) {
+ aHeaderEntry.enclosingBox.headerValue = aHeaderValue;
+}
+
+/**
+ * Create the DOM nodes (aka "View") for a non-standard header and insert them
+ * into the grid. Create and return the corresponding headerEntry object.
+ *
+ * @param {string} headerName - name of the header we're adding, used to
+ * construct the element IDs (in lower case)
+ * @param {string} label - name of the header as displayed in the UI
+ */
+class HeaderView {
+ constructor(headerName, label) {
+ headerName = headerName.toLowerCase();
+ let rowId = "expanded" + headerName + "Row";
+ let idName = "expanded" + headerName + "Box";
+ let newHeaderNode;
+ // If a row for this header already exists, do not create another one.
+ let newRowNode = document.getElementById(rowId);
+ if (!newRowNode) {
+ // Create new collapsed row.
+ newRowNode = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ newRowNode.setAttribute("id", rowId);
+ newRowNode.classList.add("message-header-row");
+ newRowNode.hidden = true;
+
+ // Create and append the label which contains the header name.
+ 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.createElement("div", {
+ is: "simple-header-row",
+ });
+ newHeaderNode.setAttribute("id", idName);
+ newHeaderNode.dataset.prettyHeaderName = label;
+ 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.outputFunction = updateHeaderValue;
+ }
+}
+
+/**
+ * Removes all non-predefined header nodes from the view.
+ *
+ * @param aHeaderTable Table of header entries.
+ */
+function RemoveNewHeaderViews(aHeaderTable) {
+ for (let name in aHeaderTable) {
+ let headerEntry = aHeaderTable[name];
+ if (headerEntry.isNewHeader) {
+ headerEntry.enclosingRow.remove();
+ }
+ }
+}
+
+/**
+ * UpdateExpandedMessageHeaders: Iterate through all the current header data
+ * we received from mime for this message for the expanded header entry table,
+ * and see if we have a corresponding entry for that header (i.e.
+ * whether the expanded header view cares about this header value)
+ * If so, then call updateHeaderEntry
+ */
+function UpdateExpandedMessageHeaders() {
+ // Iterate over each header we received and see if we have a matching entry
+ // in each header view table...
+ var headerName;
+
+ // Remove the height attr so that it redraws correctly. Works around a problem
+ // that attachment-splitter causes if it's moved high enough to affect
+ // the header box:
+ document.getElementById("msgHeaderView").removeAttribute("height");
+ // This height attribute may be set by toggleWrap() if the user clicked
+ // the "more" button" in the header.
+ // Remove it so that the height is determined automatically.
+
+ for (headerName in currentHeaderData) {
+ var headerField = currentHeaderData[headerName];
+ var headerEntry = null;
+
+ if (headerName in gExpandedHeaderView) {
+ headerEntry = gExpandedHeaderView[headerName];
+ }
+
+ if (!headerEntry && gViewAllHeaders) {
+ // For view all headers, if we don't have a header field for this
+ // value, cheat and create one then fill in a headerEntry.
+ if (headerName == "message-id" || headerName == "in-reply-to") {
+ var messageIdEntry = {
+ name: headerName,
+ outputFunction: outputMessageIds,
+ };
+ gExpandedHeaderView[headerName] = new MsgHeaderEntry(
+ "expanded",
+ messageIdEntry
+ );
+ } else if (headerName != "x-mozilla-localizeddate") {
+ // Don't bother showing X-Mozilla-LocalizedDate, since that value is
+ // displayed below the message header toolbar.
+ gExpandedHeaderView[headerName] = new HeaderView(
+ headerName,
+ currentHeaderData[headerName].headerName
+ );
+ }
+
+ headerEntry = gExpandedHeaderView[headerName];
+ }
+
+ if (headerEntry) {
+ if (
+ headerName == "references" &&
+ !(
+ gViewAllHeaders ||
+ gHeadersShowReferences ||
+ gFolder?.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)
+ )
+ ) {
+ // Hide references header if view all headers mode isn't selected, the
+ // pref show references is deactivated and the currently displayed
+ // message isn't a newsgroup posting.
+ headerEntry.valid = false;
+ } else {
+ // Set the row element visible before populating the field with addresses.
+ headerEntry.enclosingRow.hidden = false;
+ headerEntry.outputFunction(headerEntry, headerField.headerValue);
+ headerEntry.valid = true;
+ }
+ }
+ }
+
+ let otherHeaders = Services.prefs
+ .getCharPref("mail.compose.other.header", "")
+ .split(",")
+ .map(h => h.trim())
+ .filter(Boolean);
+
+ for (let otherHeaderName of otherHeaders) {
+ let toLowerCaseHeaderName = otherHeaderName.toLowerCase();
+ let headerEntry = gExpandedHeaderView[toLowerCaseHeaderName];
+ let headerData = currentHeaderData[toLowerCaseHeaderName];
+
+ if (headerEntry && headerData) {
+ headerEntry.outputFunction(headerEntry, headerData.headerValue);
+ headerEntry.valid = true;
+ }
+ }
+
+ let dateLabel = document.getElementById("dateLabel");
+ dateLabel.hidden = true;
+ if (
+ "x-mozilla-localizeddate" in currentHeaderData &&
+ currentHeaderData["x-mozilla-localizeddate"].headerValue
+ ) {
+ dateLabel.textContent =
+ currentHeaderData["x-mozilla-localizeddate"].headerValue;
+ let date = new Date(currentHeaderData.date.headerValue);
+ if (!isNaN(date)) {
+ dateLabel.setAttribute("datetime", date.toISOString());
+ dateLabel.hidden = false;
+ }
+ }
+
+ gBuiltExpandedView = true;
+
+ // Now update the view to make sure the right elements are visible.
+ updateExpandedView();
+}
+
+function ClearCurrentHeaders() {
+ gSecureMsgProbe = {};
+ // eslint-disable-next-line no-global-assign
+ currentHeaderData = {};
+ // eslint-disable-next-line no-global-assign
+ currentAttachments = [];
+ currentCharacterSet = "";
+}
+
+function ShowMessageHeaderPane() {
+ document.getElementById("msgHeaderView").collapsed = false;
+ document.getElementById("mail-notification-top").collapsed = false;
+
+ // Initialize the DBListener if we don't have one. This might happen when the
+ // message pane is hidden or no message was selected before, which caused the
+ // clearing of the the DBListener.
+ initFolderDBListener();
+}
+
+function HideMessageHeaderPane() {
+ let header = document.getElementById("msgHeaderView");
+ header.collapsed = true;
+ document.getElementById("mail-notification-top").collapsed = true;
+
+ // Disable the attachment box.
+ document.getElementById("attachmentView").collapsed = true;
+ document.getElementById("attachment-splitter").collapsed = true;
+
+ gMessageNotificationBar.clearMsgNotifications();
+ // Clear the DBListener since we don't have any visible UI to update.
+ clearFolderDBListener();
+
+ // Now let interested listeners know the pane has been hidden.
+ header.dispatchEvent(new Event("message-header-pane-hidden"));
+}
+
+/**
+ * Take a string of newsgroups separated by commas, split it into newsgroups and
+ * add them to the corresponding header-newsgroups-row element.
+ *
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} headerValue - The string of newsgroups from the message.
+ */
+function outputNewsgroups(headerEntry, headerValue) {
+ headerValue
+ .split(",")
+ .forEach(newsgroup => headerEntry.enclosingBox.addNewsgroup(newsgroup));
+ headerEntry.enclosingBox.buildView();
+}
+
+/**
+ * Take a string of tags separated by space, split them and add them to the
+ * corresponding header-tags-row element.
+ *
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} headerValue - The string of tags from the message.
+ */
+function outputTags(headerEntry, headerValue) {
+ headerEntry.enclosingBox.buildTags(headerValue.split(" "));
+}
+
+/**
+ * Take a string of message-ids separated by whitespace, split it and send them
+ * to the corresponding header-message-ids-row element.
+ *
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} headerValue - The string of message IDs from the message.
+ */
+function outputMessageIds(headerEntry, headerValue) {
+ headerEntry.enclosingBox.clear();
+
+ for (let id of headerValue.split(/\s+/)) {
+ headerEntry.enclosingBox.addId(id);
+ }
+
+ headerEntry.enclosingBox.buildView();
+}
+
+/**
+ * Take a string of addresses separated by commas, split it into separated
+ * recipient objects and add them to the related parent container row.
+ *
+ * @param {MsgHeaderEntry} headerEntry - The data structure for this header.
+ * @param {string} emailAddresses - The string of addresses from the message.
+ */
+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.
+ 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);
+ }
+
+ 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;
+ headerEntry.enclosingBox.addRecipient(address);
+ }
+
+ headerEntry.enclosingBox.buildView();
+}
+
+/**
+ * Return true if possible attachments in the currently loaded message can be
+ * deleted/detached.
+ */
+function CanDetachAttachments() {
+ var canDetach =
+ !gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false) &&
+ (!gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.ImapBox, false) ||
+ MailOfflineMgr.isOnline()) &&
+ gFolder; // We can't detach from loaded eml files yet.
+ if (canDetach && "content-type" in currentHeaderData) {
+ canDetach = !ContentTypeIsSMIME(
+ currentHeaderData["content-type"].headerValue
+ );
+ }
+ if (canDetach) {
+ canDetach = Enigmail.hdrView.enigCanDetachAttachments();
+ }
+
+ return canDetach;
+}
+
+/**
+ * Return true if the content type is an S/MIME one.
+ */
+function ContentTypeIsSMIME(contentType) {
+ // S/MIME is application/pkcs7-mime and application/pkcs7-signature
+ // - also match application/x-pkcs7-mime and application/x-pkcs7-signature.
+ return /application\/(x-)?pkcs7-(mime|signature)/.test(contentType);
+}
+
+function onShowAttachmentToolbarContextMenu() {
+ let expandBar = document.getElementById("context-expandAttachmentBar");
+ let expanded = Services.prefs.getBoolPref(
+ "mailnews.attachments.display.start_expanded"
+ );
+ expandBar.setAttribute("checked", expanded);
+}
+
+/**
+ * Set up the attachment item context menu, showing or hiding the appropriate
+ * menu items.
+ */
+function onShowAttachmentItemContextMenu() {
+ let attachmentList = document.getElementById("attachmentList");
+ let attachmentInfo = document.getElementById("attachmentInfo");
+ let attachmentName = document.getElementById("attachmentName");
+ let contextMenu = document.getElementById("attachmentItemContext");
+ let openMenu = document.getElementById("context-openAttachment");
+ let saveMenu = document.getElementById("context-saveAttachment");
+ let detachMenu = document.getElementById("context-detachAttachment");
+ let deleteMenu = document.getElementById("context-deleteAttachment");
+ let copyUrlMenuSep = document.getElementById(
+ "context-menu-copyurl-separator"
+ );
+ let copyUrlMenu = document.getElementById("context-copyAttachmentUrl");
+ let openFolderMenu = document.getElementById("context-openFolder");
+
+ // If we opened the context menu from the attachment info area (the paperclip,
+ // "1 attachment" label, filename, or file size, just grab the first (and
+ // only) attachment as our "selected" attachments.
+ var selectedAttachments;
+ if (
+ contextMenu.triggerNode == attachmentInfo ||
+ contextMenu.triggerNode.parentNode == attachmentInfo
+ ) {
+ selectedAttachments = [attachmentList.getItemAtIndex(0).attachment];
+ if (contextMenu.triggerNode == attachmentName) {
+ attachmentName.setAttribute("selected", true);
+ }
+ } else {
+ selectedAttachments = [...attachmentList.selectedItems].map(
+ item => item.attachment
+ );
+ }
+ contextMenu.attachments = selectedAttachments;
+
+ var allSelectedDetached = selectedAttachments.every(function (attachment) {
+ return attachment.isExternalAttachment;
+ });
+ var allSelectedDeleted = selectedAttachments.every(function (attachment) {
+ return !attachment.hasFile;
+ });
+ var canDetachSelected =
+ CanDetachAttachments() && !allSelectedDetached && !allSelectedDeleted;
+ let allSelectedHttp = selectedAttachments.every(function (attachment) {
+ return attachment.isLinkAttachment;
+ });
+ let allSelectedFile = selectedAttachments.every(function (attachment) {
+ return attachment.isFileAttachment;
+ });
+
+ openMenu.disabled = allSelectedDeleted;
+ saveMenu.disabled = allSelectedDeleted;
+ detachMenu.disabled = !canDetachSelected;
+ deleteMenu.disabled = !canDetachSelected;
+ copyUrlMenuSep.hidden = copyUrlMenu.hidden = !(
+ allSelectedHttp || allSelectedFile
+ );
+ openFolderMenu.hidden = !allSelectedFile;
+ openFolderMenu.disabled = allSelectedDeleted;
+
+ Enigmail.hdrView.onShowAttachmentContextMenu();
+}
+
+/**
+ * Close the attachment item context menu, performing any cleanup as necessary.
+ */
+function onHideAttachmentItemContextMenu() {
+ let attachmentName = document.getElementById("attachmentName");
+ let contextMenu = document.getElementById("attachmentItemContext");
+
+ // If we opened the context menu from the attachmentName label, we need to
+ // get rid of the "selected" attribute.
+ if (contextMenu.triggerNode == attachmentName) {
+ attachmentName.removeAttribute("selected");
+ }
+}
+
+/**
+ * Enable/disable menu items as appropriate for the single-attachment save all
+ * toolbar button.
+ */
+function onShowSaveAttachmentMenuSingle() {
+ let openItem = document.getElementById("button-openAttachment");
+ let saveItem = document.getElementById("button-saveAttachment");
+ let detachItem = document.getElementById("button-detachAttachment");
+ let deleteItem = document.getElementById("button-deleteAttachment");
+
+ let detached = currentAttachments[0].isExternalAttachment;
+ let deleted = !currentAttachments[0].hasFile;
+ let canDetach = CanDetachAttachments() && !deleted && !detached;
+
+ openItem.disabled = deleted;
+ saveItem.disabled = deleted;
+ detachItem.disabled = !canDetach;
+ deleteItem.disabled = !canDetach;
+}
+
+/**
+ * Enable/disable menu items as appropriate for the multiple-attachment save all
+ * toolbar button.
+ */
+function onShowSaveAttachmentMenuMultiple() {
+ let openAllItem = document.getElementById("button-openAllAttachments");
+ let saveAllItem = document.getElementById("button-saveAllAttachments");
+ let detachAllItem = document.getElementById("button-detachAllAttachments");
+ let deleteAllItem = document.getElementById("button-deleteAllAttachments");
+
+ let allDetached = currentAttachments.every(function (attachment) {
+ return attachment.isExternalAttachment;
+ });
+ let allDeleted = currentAttachments.every(function (attachment) {
+ return !attachment.hasFile;
+ });
+ let canDetach = CanDetachAttachments() && !allDeleted && !allDetached;
+
+ openAllItem.disabled = allDeleted;
+ saveAllItem.disabled = allDeleted;
+ detachAllItem.disabled = !canDetach;
+ deleteAllItem.disabled = !canDetach;
+}
+
+/**
+ * This is our oncommand handler for the attachment list items. A double click
+ * or enter press in an attachmentitem simulates "opening" the attachment.
+ *
+ * @param event the event object
+ */
+function attachmentItemCommand(event) {
+ HandleSelectedAttachments("open");
+}
+
+var AttachmentListController = {
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "cmd_saveAsFile":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "cmd_saveAsFile":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ doCommand(command) {
+ // If the user invoked a key short cut then it is possible that we got here
+ // for a command which is really disabled. kick out if the command should
+ // be disabled.
+ if (!this.isCommandEnabled(command)) {
+ return;
+ }
+
+ var attachmentList = document.getElementById("attachmentList");
+
+ switch (command) {
+ case "cmd_selectAll":
+ attachmentList.selectAll();
+ return;
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ HandleSelectedAttachments("delete");
+ return;
+ case "cmd_saveAsFile":
+ HandleSelectedAttachments("saveAs");
+ }
+ },
+
+ onEvent(event) {},
+};
+
+var AttachmentMenuController = {
+ canDetachFiles() {
+ let someNotDetached = currentAttachments.some(function (aAttachment) {
+ return !aAttachment.isExternalAttachment;
+ });
+
+ return (
+ CanDetachAttachments() && someNotDetached && this.someFilesAvailable()
+ );
+ },
+
+ someFilesAvailable() {
+ return currentAttachments.some(function (aAttachment) {
+ return aAttachment.hasFile;
+ });
+ },
+
+ supportsCommand(aCommand) {
+ return aCommand in this.commands;
+ },
+};
+
+function goUpdateAttachmentCommands() {
+ for (let action of ["open", "save", "detach", "delete"]) {
+ goUpdateCommand(`cmd_${action}AllAttachments`);
+ }
+}
+
+async function displayAttachmentsForExpandedView() {
+ var bundle = document.getElementById("bundle_messenger");
+ var numAttachments = currentAttachments.length;
+ var attachmentView = document.getElementById("attachmentView");
+ var attachmentSplitter = document.getElementById("attachment-splitter");
+ document
+ .getElementById("attachmentIcon")
+ .setAttribute("src", "chrome://messenger/skin/icons/attach.svg");
+
+ if (numAttachments <= 0) {
+ attachmentView.collapsed = true;
+ attachmentSplitter.collapsed = true;
+ } else if (!gBuildAttachmentsForCurrentMsg) {
+ attachmentView.collapsed = false;
+
+ var attachmentList = document.getElementById("attachmentList");
+
+ attachmentList.controllers.appendController(AttachmentListController);
+
+ toggleAttachmentList(false);
+
+ for (let attachment of currentAttachments) {
+ // Create a new attachment widget
+ var displayName = SanitizeAttachmentDisplayName(attachment);
+ var item = attachmentList.appendItem(attachment, displayName);
+ item.setAttribute("tooltiptext", attachment.name);
+ item.addEventListener("command", attachmentItemCommand);
+
+ // Get a detached file's size. For link attachments, the user must always
+ // initiate the fetch for privacy reasons.
+ if (attachment.isFileAttachment) {
+ await attachment.isEmpty();
+ }
+ }
+
+ if (
+ Services.prefs.getBoolPref("mailnews.attachments.display.start_expanded")
+ ) {
+ toggleAttachmentList(true);
+ }
+
+ let attachmentInfo = document.getElementById("attachmentInfo");
+ let attachmentCount = document.getElementById("attachmentCount");
+ let attachmentName = document.getElementById("attachmentName");
+ let attachmentSize = document.getElementById("attachmentSize");
+
+ if (numAttachments == 1) {
+ let count = bundle.getString("attachmentCountSingle");
+ let name = SanitizeAttachmentDisplayName(currentAttachments[0]);
+
+ attachmentInfo.setAttribute("contextmenu", "attachmentItemContext");
+ attachmentCount.setAttribute("value", count);
+ attachmentName.hidden = false;
+ attachmentName.setAttribute("value", name);
+ } else {
+ let words = bundle.getString("attachmentCount");
+ let count = PluralForm.get(currentAttachments.length, words).replace(
+ "#1",
+ currentAttachments.length
+ );
+
+ attachmentInfo.setAttribute("contextmenu", "attachmentListContext");
+ attachmentCount.setAttribute("value", count);
+ attachmentName.hidden = true;
+ }
+
+ attachmentSize.value = getAttachmentsTotalSizeStr();
+
+ // Extra candy for external attachments.
+ displayAttachmentsForExpandedViewExternal();
+
+ // Show the appropriate toolbar button and label based on the number of
+ // attachments.
+ updateSaveAllAttachmentsButton();
+
+ gBuildAttachmentsForCurrentMsg = true;
+ }
+}
+
+function displayAttachmentsForExpandedViewExternal() {
+ let bundleMessenger = document.getElementById("bundle_messenger");
+ let attachmentName = document.getElementById("attachmentName");
+ let attachmentList = document.getElementById("attachmentList");
+
+ // Attachment bar single.
+ let firstAttachment = attachmentList.firstElementChild.attachment;
+ let isExternalAttachment = firstAttachment.isExternalAttachment;
+ let displayUrl = isExternalAttachment ? firstAttachment.displayUrl : "";
+ let tooltiptext =
+ isExternalAttachment || firstAttachment.isDeleted
+ ? ""
+ : attachmentName.getAttribute("tooltiptextopen");
+ let externalAttachmentNotFound = bundleMessenger.getString(
+ "externalAttachmentNotFound"
+ );
+
+ attachmentName.textContent = displayUrl;
+ attachmentName.tooltipText = tooltiptext;
+ attachmentName.setAttribute(
+ "tooltiptextexternalnotfound",
+ externalAttachmentNotFound
+ );
+ attachmentName.addEventListener("mouseover", () =>
+ top.MsgStatusFeedback.setOverLink(displayUrl)
+ );
+ attachmentName.addEventListener("mouseout", () =>
+ top.MsgStatusFeedback.setOverLink("")
+ );
+ attachmentName.addEventListener("focus", () =>
+ top.MsgStatusFeedback.setOverLink(displayUrl)
+ );
+ attachmentName.addEventListener("blur", () =>
+ top.MsgStatusFeedback.setOverLink("")
+ );
+ attachmentName.classList.remove("text-link");
+ attachmentName.classList.remove("notfound");
+
+ if (firstAttachment.isDeleted) {
+ attachmentName.classList.add("notfound");
+ }
+
+ if (isExternalAttachment) {
+ attachmentName.classList.add("text-link");
+
+ if (!firstAttachment.hasFile) {
+ attachmentName.setAttribute("tooltiptext", externalAttachmentNotFound);
+ attachmentName.classList.add("notfound");
+ }
+ }
+
+ // Expanded attachment list.
+ let index = 0;
+ for (let attachmentitem of attachmentList.children) {
+ let attachment = attachmentitem.attachment;
+ if (attachment.isDeleted) {
+ attachmentitem.classList.add("notfound");
+ }
+
+ if (attachment.isExternalAttachment) {
+ displayUrl = attachment.displayUrl;
+ attachmentitem.setAttribute("tooltiptext", "");
+ attachmentitem.addEventListener("mouseover", () =>
+ top.MsgStatusFeedback.setOverLink(displayUrl)
+ );
+ attachmentitem.addEventListener("mouseout", () =>
+ top.MsgStatusFeedback.setOverLink("")
+ );
+ attachmentitem.addEventListener("focus", () =>
+ top.MsgStatusFeedback.setOverLink(displayUrl)
+ );
+ attachmentitem.addEventListener("blur", () =>
+ top.MsgStatusFeedback.setOverLink("")
+ );
+
+ attachmentitem
+ .querySelector(".attachmentcell-name")
+ .classList.add("text-link");
+ attachmentitem
+ .querySelector(".attachmentcell-extension")
+ .classList.add("text-link");
+
+ if (attachment.isLinkAttachment) {
+ if (index == 0) {
+ attachment.size = currentAttachments[index].size;
+ }
+ }
+
+ if (!attachment.hasFile) {
+ attachmentitem.setAttribute("tooltiptext", externalAttachmentNotFound);
+ attachmentitem.classList.add("notfound");
+ }
+ }
+
+ index++;
+ }
+}
+
+/**
+ * Update the "save all attachments" button in the attachment pane, showing
+ * the proper button and enabling/disabling it as appropriate.
+ */
+function updateSaveAllAttachmentsButton() {
+ let saveAllSingle = document.getElementById("attachmentSaveAllSingle");
+ let saveAllMultiple = document.getElementById("attachmentSaveAllMultiple");
+
+ // If we can't find the buttons, they're not on the toolbar, so bail out!
+ if (!saveAllSingle || !saveAllMultiple) {
+ return;
+ }
+
+ let allDeleted = currentAttachments.every(function (attachment) {
+ return !attachment.hasFile;
+ });
+ let single = currentAttachments.length == 1;
+
+ saveAllSingle.hidden = !single;
+ saveAllMultiple.hidden = single;
+ saveAllSingle.disabled = saveAllMultiple.disabled = allDeleted;
+}
+
+/**
+ * Update the attachments display info after a particular attachment's
+ * existence has been verified.
+ *
+ * @param {AttachmentInfo} attachmentInfo
+ * @param {boolean} isFetching
+ */
+function updateAttachmentsDisplay(attachmentInfo, isFetching) {
+ if (attachmentInfo.isExternalAttachment) {
+ let attachmentList = document.getElementById("attachmentList");
+ let attachmentIcon = document.getElementById("attachmentIcon");
+ let attachmentName = document.getElementById("attachmentName");
+ let attachmentSize = document.getElementById("attachmentSize");
+ let attachmentItem = attachmentList.findItemForAttachment(attachmentInfo);
+ let index = attachmentList.getIndexOfItem(attachmentItem);
+
+ if (isFetching) {
+ // Set elements busy to show the user this is potentially a long network
+ // fetch for the link attachment.
+ attachmentList.setAttachmentLoaded(attachmentItem, false);
+ return;
+ }
+
+ if (attachmentInfo.message != gMessage) {
+ // The user changed messages while fetching, reset the bar and exit;
+ // the listitems are torn down/rebuilt on each message load.
+ attachmentIcon.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/attach.svg"
+ );
+ return;
+ }
+
+ if (index == -1) {
+ // The user changed messages while fetching, then came back to the same
+ // message. The reset of busy state has already happened and anyway the
+ // item has already been torn down so the index will be invalid; exit.
+ return;
+ }
+
+ currentAttachments[index].size = attachmentInfo.size;
+ let tooltiptextExternalNotFound = attachmentName.getAttribute(
+ "tooltiptextexternalnotfound"
+ );
+
+ let sizeStr;
+ let bundle = document.getElementById("bundle_messenger");
+ if (attachmentInfo.size < 1) {
+ sizeStr = bundle.getString("attachmentSizeUnknown");
+ } else {
+ sizeStr = top.messenger.formatFileSize(attachmentInfo.size);
+ }
+
+ // The attachment listitem.
+ attachmentList.setAttachmentLoaded(attachmentItem, true);
+ attachmentList.setAttachmentSize(
+ attachmentItem,
+ attachmentInfo.hasFile ? sizeStr : ""
+ );
+
+ // FIXME: The UI logic for this should be moved to the attachment list or
+ // item itself.
+ if (attachmentInfo.hasFile) {
+ attachmentItem.removeAttribute("tooltiptext");
+ attachmentItem.classList.remove("notfound");
+ } else {
+ attachmentItem.setAttribute("tooltiptext", tooltiptextExternalNotFound);
+ attachmentItem.classList.add("notfound");
+ }
+
+ // The attachmentbar.
+ updateSaveAllAttachmentsButton();
+ attachmentSize.value = getAttachmentsTotalSizeStr();
+ if (attachmentList.isLoaded()) {
+ attachmentIcon.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/attach.svg"
+ );
+ }
+
+ // If it's the first one (and there's only one).
+ if (index == 0) {
+ if (attachmentInfo.hasFile) {
+ attachmentName.removeAttribute("tooltiptext");
+ attachmentName.classList.remove("notfound");
+ } else {
+ attachmentName.setAttribute("tooltiptext", tooltiptextExternalNotFound);
+ attachmentName.classList.add("notfound");
+ }
+ }
+
+ // Reset widths since size may have changed; ensure no false cropping of
+ // the attachment item name.
+ attachmentList.setOptimumWidth();
+ }
+}
+
+/**
+ * Calculate the total size of all attachments in the message as emitted to
+ * |currentAttachments| and return a pretty string.
+ *
+ * @returns {string} - Description of the attachment size (e.g. 123 KB or 3.1MB)
+ */
+function getAttachmentsTotalSizeStr() {
+ let bundle = document.getElementById("bundle_messenger");
+ let totalSize = 0;
+ let lastPartID;
+ let unknownSize = false;
+ for (let attachment of currentAttachments) {
+ // Check if this attachment's part ID is a child of the last attachment
+ // we counted. If so, skip it, since we already accounted for its size
+ // from its parent.
+ if (!lastPartID || attachment.partID.indexOf(lastPartID) != 0) {
+ lastPartID = attachment.partID;
+ if (attachment.size != -1) {
+ totalSize += Number(attachment.size);
+ } else if (!attachment.isDeleted) {
+ unknownSize = true;
+ }
+ }
+ }
+
+ let sizeStr = top.messenger.formatFileSize(totalSize);
+ if (unknownSize) {
+ if (totalSize == 0) {
+ sizeStr = bundle.getString("attachmentSizeUnknown");
+ } else {
+ sizeStr = bundle.getFormattedString("attachmentSizeAtLeast", [sizeStr]);
+ }
+ }
+
+ return sizeStr;
+}
+
+/**
+ * Expand/collapse the attachment list. When expanding it, automatically resize
+ * it to an appropriate height (1/4 the message pane or smaller).
+ *
+ * @param expanded True if the attachment list should be expanded, false
+ * otherwise. If |expanded| is not specified, toggle the state.
+ * @param updateFocus (optional) True if the focus should be updated, focusing
+ * on the attachmentList when expanding, or the messagepane
+ * when collapsing (but only when the attachmentList was
+ * originally focused).
+ */
+function toggleAttachmentList(expanded, updateFocus) {
+ var attachmentView = document.getElementById("attachmentView");
+ var attachmentBar = document.getElementById("attachmentBar");
+ var attachmentToggle = document.getElementById("attachmentToggle");
+ var attachmentList = document.getElementById("attachmentList");
+ var attachmentSplitter = document.getElementById("attachment-splitter");
+ var bundle = document.getElementById("bundle_messenger");
+
+ if (expanded === undefined) {
+ expanded = !attachmentToggle.checked;
+ }
+
+ attachmentToggle.checked = expanded;
+
+ if (expanded) {
+ attachmentList.collapsed = false;
+ if (!attachmentView.collapsed) {
+ attachmentSplitter.collapsed = false;
+ }
+ attachmentBar.setAttribute(
+ "tooltiptext",
+ bundle.getString("collapseAttachmentPaneTooltip")
+ );
+
+ attachmentList.setOptimumWidth();
+
+ // By design, attachmentView should not take up more than 1/4 of the message
+ // pane space
+ attachmentView.setAttribute(
+ "height",
+ Math.min(
+ attachmentList.preferredHeight,
+ document.getElementById("messagepanebox").getBoundingClientRect()
+ .height / 4
+ )
+ );
+
+ if (updateFocus) {
+ attachmentList.focus();
+ }
+ } else {
+ attachmentList.collapsed = true;
+ attachmentSplitter.collapsed = true;
+ attachmentBar.setAttribute(
+ "tooltiptext",
+ bundle.getString("expandAttachmentPaneTooltip")
+ );
+ attachmentView.removeAttribute("height");
+
+ if (updateFocus && document.activeElement == attachmentList) {
+ // TODO
+ }
+ }
+}
+
+/**
+ * Open an attachment from the attachment bar.
+ *
+ * @param event the event that triggered this action
+ */
+function OpenAttachmentFromBar(event) {
+ if (event.button == 0) {
+ // Only open on the first click; ignore double-clicks so that the user
+ // doesn't end up with the attachment opened multiple times.
+ if (event.detail == 1) {
+ TryHandleAllAttachments("open");
+ }
+ event.stopPropagation();
+ }
+}
+
+/**
+ * Handle all the attachments in this message (save them, open them, etc).
+ *
+ * @param action one of "open", "save", "saveAs", "detach", or "delete"
+ */
+function HandleAllAttachments(action) {
+ HandleMultipleAttachments(currentAttachments, action);
+}
+
+/**
+ * Try to handle all the attachments in this message (save them, open them,
+ * etc). If the action fails for whatever reason, catch the error and report it.
+ *
+ * @param action one of "open", "save", "saveAs", "detach", or "delete"
+ */
+function TryHandleAllAttachments(action) {
+ try {
+ HandleAllAttachments(action);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+/**
+ * Handle the currently-selected attachments in this message (save them, open
+ * them, etc).
+ *
+ * @param action one of "open", "save", "saveAs", "detach", or "delete"
+ */
+function HandleSelectedAttachments(action) {
+ let attachmentList = document.getElementById("attachmentList");
+ let selectedAttachments = [];
+ for (let item of attachmentList.selectedItems) {
+ selectedAttachments.push(item.attachment);
+ }
+
+ HandleMultipleAttachments(selectedAttachments, action);
+}
+
+/**
+ * Perform an action on multiple attachments (e.g. open or save)
+ *
+ * @param attachments an array of AttachmentInfo objects to work with
+ * @param action one of "open", "save", "saveAs", "detach", or "delete"
+ */
+function HandleMultipleAttachments(attachments, action) {
+ // Feed message link attachments save handling.
+ if (
+ FeedUtils.isFeedMessage(gMessage) &&
+ (action == "save" || action == "saveAs")
+ ) {
+ saveLinkAttachmentsToFile(attachments);
+ return;
+ }
+
+ // convert our attachment data into some c++ friendly structs
+ var attachmentContentTypeArray = [];
+ var attachmentUrlArray = [];
+ var attachmentDisplayUrlArray = [];
+ var attachmentDisplayNameArray = [];
+ var attachmentMessageUriArray = [];
+
+ // populate these arrays..
+ var actionIndex = 0;
+ for (let attachment of attachments) {
+ // Exclude attachment which are 1) deleted, or 2) detached with missing
+ // external files, unless copying urls.
+ if (!attachment.hasFile && action != "copyUrl") {
+ continue;
+ }
+
+ attachmentContentTypeArray[actionIndex] = attachment.contentType;
+ attachmentUrlArray[actionIndex] = attachment.url;
+ attachmentDisplayUrlArray[actionIndex] = attachment.displayUrl;
+ attachmentDisplayNameArray[actionIndex] = encodeURI(attachment.name);
+ attachmentMessageUriArray[actionIndex] = attachment.uri;
+ ++actionIndex;
+ }
+
+ // The list has been built. Now call our action code...
+ switch (action) {
+ case "save":
+ top.messenger.saveAllAttachments(
+ attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray
+ );
+ return;
+ case "detach":
+ // "detach" on a multiple selection of attachments is so far not really
+ // supported. As a workaround, resort to normal detach-"all". See also
+ // the comment on 'detaching a multiple selection of attachments' below.
+ if (attachments.length == 1) {
+ attachments[0].detach(top.messenger, true);
+ } else {
+ top.messenger.detachAllAttachments(
+ attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray,
+ true // save
+ );
+ }
+ return;
+ case "delete":
+ top.messenger.detachAllAttachments(
+ attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray,
+ false // don't save
+ );
+ return;
+ case "open":
+ // XXX hack alert. If we sit in tight loop and open multiple
+ // attachments, we get chrome errors in layout as we start loading the
+ // first helper app dialog then before it loads, we kick off the next
+ // one and the next one. Subsequent helper app dialogs were failing
+ // because we were still loading the chrome files for the first attempt
+ // (error about the xul cache being empty). For now, work around this by
+ // doing the first helper app dialog right away, then waiting a bit
+ // before we launch the rest.
+ let actionFunction = function (aAttachment) {
+ aAttachment.open(getMessagePaneBrowser().browsingContext);
+ };
+
+ for (let i = 0; i < attachments.length; i++) {
+ if (i == 0) {
+ actionFunction(attachments[i]);
+ } else {
+ setTimeout(actionFunction, 100, attachments[i]);
+ }
+ }
+ return;
+ case "saveAs":
+ // Show one save dialog at a time, which allows to adjust the file name
+ // and folder path for each attachment. For added convenience, we remember
+ // the folder path of each file for the save dialog of the next one.
+ let saveAttachments = function (attachments) {
+ if (attachments.length > 0) {
+ attachments[0].save(top.messenger).then(function () {
+ saveAttachments(attachments.slice(1));
+ });
+ }
+ };
+
+ saveAttachments(attachments);
+ return;
+ case "copyUrl":
+ // Copy external http url(s) to clipboard. The menuitem is hidden unless
+ // all selected attachment urls are http.
+ 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);
+ }
+}
+
+/**
+ * Link attachments are passed as an array of AttachmentInfo objects. This
+ * is meant to download http link content using the browser method.
+ *
+ * @param {AttachmentInfo[]} aAttachmentInfoArray - Array of attachmentInfo.
+ */
+async function saveLinkAttachmentsToFile(aAttachmentInfoArray) {
+ for (let attachment of aAttachmentInfoArray) {
+ if (!attachment.hasFile || attachment.message != gMessage) {
+ continue;
+ }
+
+ let empty = await attachment.isEmpty();
+ if (empty) {
+ continue;
+ }
+
+ // internalSave() is part of saveURL() internals...
+ internalSave(
+ attachment.url, // aURL,
+ null, // aOriginalUrl,
+ undefined, // aDocument,
+ attachment.name, // aDefaultFileName,
+ undefined, // aContentDisposition,
+ undefined, // aContentType,
+ undefined, // aShouldBypassCache,
+ undefined, // aFilePickerTitleKey,
+ undefined, // aChosenData,
+ undefined, // aReferrer,
+ undefined, // aCookieJarSettings,
+ document, // aInitiatingDocument,
+ undefined, // aSkipPrompt,
+ undefined, // aCacheKey,
+ undefined // aIsContentWindowPrivate
+ );
+ }
+}
+
+function ClearAttachmentList() {
+ // clear selection
+ var list = document.getElementById("attachmentList");
+ list.clearSelection();
+
+ while (list.hasChildNodes()) {
+ list.lastChild.remove();
+ }
+}
+
+// See attachmentBucketDNDObserver, which should have the same logic.
+let attachmentListDNDObserver = {
+ onDragStart(event) {
+ // NOTE: Starting a drag on an attachment item will normally also select
+ // the attachment item before this method is called. But this is not
+ // necessarily the case. E.g. holding Shift when starting the drag
+ // operation. When it isn't selected, we just don't transfer.
+ if (event.target.matches(".attachmentItem[selected]")) {
+ // Also transfer other selected attachment items.
+ let attachments = Array.from(
+ document.querySelectorAll("#attachmentList .attachmentItem[selected]"),
+ item => item.attachment
+ );
+ setupDataTransfer(event, attachments);
+ }
+ event.stopPropagation();
+ },
+};
+
+let attachmentNameDNDObserver = {
+ onDragStart(event) {
+ let attachmentList = document.getElementById("attachmentList");
+ setupDataTransfer(event, [attachmentList.getItemAtIndex(0).attachment]);
+ event.stopPropagation();
+ },
+};
+
+function onShowOtherActionsPopup() {
+ // Enable/disable the Open Conversation button.
+ let glodaEnabled = Services.prefs.getBoolPref(
+ "mailnews.database.global.indexer.enabled"
+ );
+
+ let openConversation = document.getElementById(
+ "otherActionsOpenConversation"
+ );
+ // Check because this menuitem element is not present in messageWindow.xhtml.
+ if (openConversation) {
+ openConversation.disabled = !(
+ glodaEnabled && Gloda.isMessageIndexed(gMessage)
+ );
+ }
+
+ let isDummyMessage = !gViewWrapper.isSynthetic && !gMessage.folder;
+ let tagsItem = document.getElementById("otherActionsTag");
+ let markAsReadItem = document.getElementById("markAsReadMenuItem");
+ let markAsUnreadItem = document.getElementById("markAsUnreadMenuItem");
+
+ if (isDummyMessage) {
+ tagsItem.disabled = true;
+ markAsReadItem.disabled = true;
+ markAsReadItem.removeAttribute("hidden");
+ markAsUnreadItem.setAttribute("hidden", true);
+ } else {
+ tagsItem.disabled = false;
+ markAsReadItem.disabled = false;
+ if (SelectedMessagesAreRead()) {
+ markAsReadItem.setAttribute("hidden", true);
+ markAsUnreadItem.removeAttribute("hidden");
+ } else {
+ markAsReadItem.removeAttribute("hidden");
+ markAsUnreadItem.setAttribute("hidden", true);
+ }
+ }
+
+ document.getElementById("otherActions-calendar-convert-menu").hidden =
+ isDummyMessage || !calendarDeactivator.isCalendarActivated;
+
+ // Check if the current message is feed or not.
+ let isFeed = FeedUtils.isFeedMessage(gMessage);
+ document.getElementById("otherActionsMessageBodyAs").hidden = isFeed;
+ document.getElementById("otherActionsFeedBodyAs").hidden = !isFeed;
+}
+
+function InitOtherActionsViewBodyMenu() {
+ let html_as = Services.prefs.getIntPref("mailnews.display.html_as");
+ let prefer_plaintext = Services.prefs.getBoolPref(
+ "mailnews.display.prefer_plaintext"
+ );
+ let disallow_classes = Services.prefs.getIntPref(
+ "mailnews.display.disallow_mime_handlers"
+ );
+ let isFeed = false; // TODO
+ const kDefaultIDs = [
+ "otherActionsMenu_bodyAllowHTML",
+ "otherActionsMenu_bodySanitized",
+ "otherActionsMenu_bodyAsPlaintext",
+ "otherActionsMenu_bodyAllParts",
+ ];
+ const kRssIDs = [
+ "otherActionsMenu_bodyFeedSummaryAllowHTML",
+ "otherActionsMenu_bodyFeedSummarySanitized",
+ "otherActionsMenu_bodyFeedSummaryAsPlaintext",
+ ];
+ let menuIDs = isFeed ? kRssIDs : kDefaultIDs;
+
+ if (disallow_classes > 0) {
+ window.top.gDisallow_classes_no_html = disallow_classes;
+ }
+ // else gDisallow_classes_no_html keeps its initial value (see top)
+
+ let AllowHTML_menuitem = document.getElementById(menuIDs[0]);
+ let Sanitized_menuitem = document.getElementById(menuIDs[1]);
+ let AsPlaintext_menuitem = document.getElementById(menuIDs[2]);
+ let AllBodyParts_menuitem = menuIDs[3]
+ ? document.getElementById(menuIDs[3])
+ : null;
+
+ document.getElementById("otherActionsMenu_bodyAllParts").hidden =
+ !Services.prefs.getBoolPref("mailnews.display.show_all_body_parts_menu");
+
+ // Clear all checkmarks.
+ AllowHTML_menuitem.removeAttribute("checked");
+ Sanitized_menuitem.removeAttribute("checked");
+ AsPlaintext_menuitem.removeAttribute("checked");
+ if (AllBodyParts_menuitem) {
+ AllBodyParts_menuitem.removeAttribute("checked");
+ }
+
+ if (
+ !prefer_plaintext &&
+ !html_as &&
+ !disallow_classes &&
+ AllowHTML_menuitem
+ ) {
+ AllowHTML_menuitem.setAttribute("checked", true);
+ } else if (
+ !prefer_plaintext &&
+ html_as == 3 &&
+ disallow_classes > 0 &&
+ Sanitized_menuitem
+ ) {
+ Sanitized_menuitem.setAttribute("checked", true);
+ } else if (
+ prefer_plaintext &&
+ html_as == 1 &&
+ disallow_classes > 0 &&
+ AsPlaintext_menuitem
+ ) {
+ AsPlaintext_menuitem.setAttribute("checked", true);
+ } else if (
+ !prefer_plaintext &&
+ html_as == 4 &&
+ !disallow_classes &&
+ AllBodyParts_menuitem
+ ) {
+ AllBodyParts_menuitem.setAttribute("checked", true);
+ }
+ // else (the user edited prefs/user.js) check none of the radio menu items
+
+ if (isFeed) {
+ AllowHTML_menuitem.hidden = !gShowFeedSummary;
+ Sanitized_menuitem.hidden = !gShowFeedSummary;
+ AsPlaintext_menuitem.hidden = !gShowFeedSummary;
+ document.getElementById(
+ "otherActionsMenu_viewFeedSummarySeparator"
+ ).hidden = !gShowFeedSummary;
+ }
+}
+
+/**
+ * Object literal to handle a few simple customization options for the message
+ * header.
+ */
+const gHeaderCustomize = {
+ docURL: "chrome://messenger/content/messenger.xhtml",
+ /**
+ * The DOM element panel collecting all customization options.
+ *
+ * @type {XULElement}
+ */
+ customizePanel: null,
+ /**
+ * The object storing all saved customization options.
+ *
+ * @note Any keys added to this object should also be added to the telemetry
+ * scalar tb.ui.configuration.message_header.
+ *
+ * @type {object}
+ * @property {boolean} showAvatar - If the profile picture of the sender
+ * should be shown.
+ * @property {boolean} showBigAvatar - If a big profile picture of the sender
+ * should be shown.
+ * @property {boolean} showFullAddress - If the sender should always be
+ * shown with the full name and email address.
+ * @property {boolean} hideLabels - If the labels column should be hidden.
+ * @property {boolean} subjectLarge - If the font size of the subject line
+ * should be increased.
+ * @property {string} buttonStyle - The style in which the buttons should be
+ * rendered:
+ * - "default" = icons+text
+ * - "only-icons" = only icons
+ * - "only-text" = only text
+ */
+ customizeData: {
+ showAvatar: true,
+ showBigAvatar: false,
+ showFullAddress: true,
+ hideLabels: true,
+ subjectLarge: true,
+ buttonStyle: "default",
+ },
+
+ /**
+ * Initialize the customizer.
+ */
+ init() {
+ this.customizePanel = document.getElementById(
+ "messageHeaderCustomizationPanel"
+ );
+
+ if (Services.xulStore.hasValue(this.docURL, "messageHeader", "layout")) {
+ this.customizeData = JSON.parse(
+ Services.xulStore.getValue(this.docURL, "messageHeader", "layout")
+ );
+ this.updateLayout();
+ }
+ },
+
+ /**
+ * Reset and update the customized style of the message header.
+ */
+ updateLayout() {
+ let header = document.getElementById("messageHeader");
+ // Always clear existing styles to avoid visual issues.
+ header.classList.remove(
+ "message-header-large-subject",
+ "message-header-buttons-only-icons",
+ "message-header-buttons-only-text",
+ "message-header-hide-label-column"
+ );
+
+ // Bail out if we don't have anything to customize.
+ if (!Object.keys(this.customizeData).length) {
+ header.classList.add(
+ "message-header-large-subject",
+ "message-header-show-recipient-avatar",
+ "message-header-show-sender-full-address",
+ "message-header-hide-label-column"
+ );
+ return;
+ }
+
+ header.classList.toggle(
+ "message-header-large-subject",
+ this.customizeData.subjectLarge || false
+ );
+
+ header.classList.toggle(
+ "message-header-hide-label-column",
+ this.customizeData.hideLabels || false
+ );
+
+ header.classList.toggle(
+ "message-header-show-recipient-avatar",
+ this.customizeData.showAvatar || false
+ );
+
+ header.classList.toggle(
+ "message-header-show-big-avatar",
+ this.customizeData.showBigAvatar || false
+ );
+
+ header.classList.toggle(
+ "message-header-show-sender-full-address",
+ this.customizeData.showFullAddress || false
+ );
+
+ switch (this.customizeData.buttonStyle) {
+ case "only-icons":
+ case "only-text":
+ header.classList.add(
+ `message-header-buttons-${this.customizeData.buttonStyle}`
+ );
+ break;
+
+ case "default":
+ default:
+ header.classList.remove(
+ "message-header-buttons-only-icons",
+ "message-header-buttons-only-text"
+ );
+ break;
+ }
+
+ gMessageHeader.syncLabelsColumnWidths();
+ },
+
+ /**
+ * Show the customization panel for the message header.
+ */
+ showPanel() {
+ this.customizePanel.openPopup(
+ document.getElementById("otherActionsButton"),
+ "after_end",
+ 6,
+ 6,
+ false
+ );
+ },
+
+ /**
+ * Update the panel's elements to reflect the users' customization.
+ */
+ onPanelShowing() {
+ document.getElementById("headerButtonStyle").value =
+ this.customizeData.buttonStyle || "default";
+
+ document.getElementById("headerShowAvatar").checked =
+ this.customizeData.showAvatar || false;
+
+ document.getElementById("headerShowBigAvatar").checked =
+ this.customizeData.showBigAvatar || false;
+
+ document.getElementById("headerShowFullAddress").checked =
+ this.customizeData.showFullAddress || false;
+
+ document.getElementById("headerHideLabels").checked =
+ this.customizeData.hideLabels || false;
+
+ document.getElementById("headerSubjectLarge").checked =
+ this.customizeData.subjectLarge || false;
+
+ let type = Ci.nsMimeHeaderDisplayTypes;
+ let pref = Services.prefs.getIntPref("mail.show_headers");
+
+ document.getElementById("headerViewAllHeaders").checked =
+ type.AllHeaders == pref;
+ },
+
+ /**
+ * Update the buttons style when the menuitem value is changed.
+ *
+ * @param {Event} event - The menuitem command event.
+ */
+ updateButtonStyle(event) {
+ this.customizeData.buttonStyle = event.target.value;
+ this.updateLayout();
+ },
+
+ /**
+ * Show or hide the profile picture of the sender recipient.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ toggleAvatar(event) {
+ const isChecked = event.target.checked;
+ this.customizeData.showAvatar = isChecked;
+ document.getElementById("headerShowBigAvatar").disabled = !isChecked;
+ this.updateLayout();
+ },
+
+ /**
+ * Show big or small profile picture of the sender recipient.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ toggleBigAvatar(event) {
+ this.customizeData.showBigAvatar = event.target.checked;
+ this.updateLayout();
+ },
+
+ /**
+ * Show or hide the sender's full address, which will show the display name
+ * and the email address on two different lines.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ toggleSenderAddress(event) {
+ this.customizeData.showFullAddress = event.target.checked;
+ this.updateLayout();
+ },
+
+ /**
+ * Show or hide the labels column.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ toggleLabelColumn(event) {
+ this.customizeData.hideLabels = event.target.checked;
+ this.updateLayout();
+ },
+
+ /**
+ * Update the subject style when the checkbox is clicked.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ updateSubjectStyle(event) {
+ this.customizeData.subjectLarge = event.target.checked;
+ this.updateLayout();
+ },
+
+ /**
+ * Show or hide all the headers of a message.
+ *
+ * @param {Event} event - The checkbox command event.
+ */
+ toggleAllHeaders(event) {
+ let mode = event.target.checked
+ ? Ci.nsMimeHeaderDisplayTypes.AllHeaders
+ : Ci.nsMimeHeaderDisplayTypes.NormalHeaders;
+ Services.prefs.setIntPref("mail.show_headers", mode);
+ AdjustHeaderView(mode);
+ ReloadMessage();
+ },
+
+ /**
+ * Close the customize panel.
+ */
+ closePanel() {
+ this.customizePanel.hidePopup();
+ },
+
+ /**
+ * Update the xulStore only when the panel is closed.
+ */
+ onPanelHidden() {
+ 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.
+ *
+ * @returns {?nsISubscribableServer} The server for the newsgroup, or null.
+ */
+ get newsgroupServer() {
+ if (gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)) {
+ return gFolder.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);
+ },
+
+ /**
+ * Ensure that the all visible labels have the same size.
+ */
+ syncLabelsColumnWidths() {
+ let allHeaderLabels = document.querySelectorAll(
+ ".message-header-row:not([hidden]) .message-header-label"
+ );
+
+ // Clear existing style.
+ for (let label of allHeaderLabels) {
+ label.style.minWidth = null;
+ }
+
+ let minWidth = Math.max(...Array.from(allHeaderLabels, i => i.clientWidth));
+ for (let label of allHeaderLabels) {
+ label.style.minWidth = `${minWidth}px`;
+ }
+ },
+
+ openCopyPopup(event, element) {
+ document.getElementById("copyCreateFilterFrom").disabled =
+ !gFolder?.server.canHaveFilters;
+
+ let popup = document.getElementById(
+ element.matches(`:scope[is="url-header-row"]`)
+ ? "copyUrlPopup"
+ : "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.
+ }
+
+ document.getElementById("createFilterFrom").disabled =
+ !gFolder?.server.canHaveFilters;
+
+ 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 subscribed = this.newsgroupServer
+ ?.QueryInterface(Ci.nsINntpIncomingServer)
+ .containsNewsgroup(element.textContent);
+ document.getElementById("subscribeToNewsgroupItem").hidden = subscribed;
+ document.getElementById("subscribeToNewsgroupSeparator").hidden =
+ subscribed;
+
+ 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);
+ },
+
+ openMessageIdPopup(event, element) {
+ document
+ .getElementById("messageIdContext-messageIdTarget")
+ .setAttribute("label", element.id);
+
+ // We don't want to show "Open Message For ID" for the same message
+ // we're viewing.
+ document.getElementById("messageIdContext-openMessageForMsgId").hidden =
+ `<${gMessage.messageId}>` == element.id;
+
+ // We don't want to show "Open Browser With Message-ID" for non-nntp
+ // messages.
+ document.getElementById("messageIdContext-openBrowserWithMsgId").hidden =
+ !gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false);
+
+ let popup = document.getElementById("messageIdContext");
+ 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 (gFolder) {
+ params.identity = MailServices.accounts.getFirstIdentityForServer(
+ gFolder.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) {
+ console.error("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 element = event.currentTarget.parentNode.headerField;
+ top.MsgFilters(
+ element.emailAddress || element.value.textContent,
+ gFolder,
+ element.dataset.headerName
+ );
+ },
+
+ /**
+ * Show the edit contact popup panel.
+ *
+ * @param {HTMLLIElement} element - The recipient element.
+ */
+ editContact(element) {
+ editContactInlineUI.showEditContactPanel(element.cardDetails, element);
+ },
+
+ /**
+ * Set the tags to the message header tag element.
+ */
+ setTags() {
+ // Bail out if we don't have a message selected.
+ if (!gMessage || !gFolder) {
+ return;
+ }
+
+ // Extract the tag keys from the message header.
+ let msgKeyArray = gMessage.getStringProperty("keywords").split(" ");
+
+ // Get the list of known tags.
+ let tagsArray = MailServices.tags.getAllTags().filter(t => t.tag);
+ let tagKeys = {};
+ for (let tagInfo of tagsArray) {
+ tagKeys[tagInfo.key] = true;
+ }
+ // Only use tags that match our saved tags.
+ let msgKeys = msgKeyArray.filter(k => k in tagKeys);
+
+ if (msgKeys.length) {
+ currentHeaderData.tags = {
+ headerName: "tags",
+ headerValue: msgKeys.join(" "),
+ };
+ return;
+ }
+
+ // No more tags, so clear out the header field.
+ delete currentHeaderData.tags;
+ },
+
+ onMessageIdClick(event) {
+ let id = event.currentTarget.closest(".header-message-id").id;
+ if (event.button == 0) {
+ // Remove the < and > symbols.
+ OpenMessageForMessageId(id.substring(1, id.length - 1));
+ }
+ },
+
+ openMessage(event) {
+ let id = event.currentTarget.parentNode.headerField.id;
+ // Remove the < and > symbols.
+ OpenMessageForMessageId(id.substring(1, id.length - 1));
+ },
+
+ openBrowser(event) {
+ let id = event.currentTarget.parentNode.headerField.id;
+ // Remove the < and > symbols.
+ OpenBrowserWithMessageId(id.substring(1, id.length - 1));
+ },
+
+ copyMessageId(event) {
+ navigator.clipboard.writeText(
+ event.currentTarget.parentNode.headerField.id
+ );
+ },
+
+ copyWebsiteUrl(event) {
+ navigator.clipboard.writeText(
+ event.currentTarget.parentNode.headerField.value.textContent
+ );
+ },
+};
+
+function MarkSelectedMessagesRead(markRead) {
+ ClearPendingReadTimer();
+ gDBView.doCommand(
+ markRead
+ ? Ci.nsMsgViewCommandType.markMessagesRead
+ : Ci.nsMsgViewCommandType.markMessagesUnread
+ );
+ if (markRead) {
+ reportMsgRead({ isNewRead: true });
+ }
+}
+
+function MarkSelectedMessagesFlagged(markFlagged) {
+ gDBView.doCommand(
+ markFlagged
+ ? Ci.nsMsgViewCommandType.flagMessages
+ : Ci.nsMsgViewCommandType.unflagMessages
+ );
+}
+
+/**
+ * Take the message id from the messageIdNode and use the url defined in the
+ * hidden pref "mailnews.messageid_browser.url" to open it in a browser window
+ * (%mid is replaced by the message id).
+ * @param {string} messageId - The message id to open.
+ */
+function OpenBrowserWithMessageId(messageId) {
+ var browserURL = Services.prefs.getComplexValue(
+ "mailnews.messageid_browser.url",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ browserURL = browserURL.replace(/%mid/, messageId);
+ try {
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(Services.io.newURI(browserURL));
+ } catch (ex) {
+ console.error(
+ "Failed to open message-id in browser; browserURL=" + browserURL
+ );
+ }
+}
+
+/**
+ * Take the message id from the messageIdNode, search for the corresponding
+ * message in all folders starting with the current selected folder, then the
+ * current account followed by the other accounts and open corresponding
+ * message if found.
+ * @param {string} messageId - The message id to open.
+ */
+function OpenMessageForMessageId(messageId) {
+ let startServer = gFolder?.server;
+
+ window.setCursor("wait");
+ let msgHdr = MailUtils.getMsgHdrForMsgId(messageId, startServer);
+ window.setCursor("auto");
+
+ // If message was found open corresponding message.
+ if (msgHdr) {
+ if (parent.location == "about:3pane") {
+ // Message in 3pane.
+ parent.selectMessage(msgHdr);
+ } else {
+ // Message in tab, standalone message window.
+ let uri = msgHdr.folder.getUriForMsg(msgHdr);
+ window.displayMessage(uri);
+ }
+ return;
+ }
+ let messageIdStr = "<" + messageId + ">";
+ let bundle = document.getElementById("bundle_messenger");
+ let errorTitle = bundle.getString("errorOpenMessageForMessageIdTitle");
+ let errorMessage = bundle.getFormattedString(
+ "errorOpenMessageForMessageIdMessage",
+ [messageIdStr]
+ );
+ Services.prompt.alert(window, errorTitle, errorMessage);
+}
+
+/**
+ * @param headermode {Ci.nsMimeHeaderDisplayTypes}
+ */
+function AdjustHeaderView(headermode) {
+ const all = Ci.nsMimeHeaderDisplayTypes.AllHeaders;
+ document
+ .getElementById("messageHeader")
+ .setAttribute("show_header_mode", headermode == all ? "all" : "normal");
+}
+
+/**
+ * Should the reply command/button be enabled?
+ *
+ * @return whether the reply command/button should be enabled.
+ */
+function IsReplyEnabled() {
+ // If we're in an rss item, we never want to Reply, because there's
+ // usually no-one useful to reply to.
+ return !FeedUtils.isFeedMessage(gMessage);
+}
+
+/**
+ * Should the reply-all command/button be enabled?
+ *
+ * @return whether the reply-all command/button should be enabled.
+ */
+function IsReplyAllEnabled() {
+ if (gFolder?.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)) {
+ // If we're in a news item, we always want ReplyAll, because we can
+ // reply to the sender and the newsgroup.
+ return true;
+ }
+ if (FeedUtils.isFeedMessage(gMessage)) {
+ // If we're in an rss item, we never want to ReplyAll, because there's
+ // usually no-one useful to reply to.
+ return false;
+ }
+
+ let addresses =
+ gMessage.author + "," + gMessage.recipients + "," + gMessage.ccList;
+
+ // If we've got any BCCed addresses (because we sent the message), add
+ // them as well.
+ if ("bcc" in currentHeaderData) {
+ addresses += currentHeaderData.bcc.headerValue;
+ }
+
+ // Check to see if my email address is in the list of addresses.
+ let [myIdentity] = MailUtils.getIdentityForHeader(gMessage);
+ let myEmail = myIdentity ? myIdentity.email : null;
+ // We aren't guaranteed to have an email address, so guard against that.
+ let imInAddresses =
+ myEmail && addresses.toLowerCase().includes(myEmail.toLowerCase());
+
+ // Now, let's get the number of unique addresses.
+ let uniqueAddresses = MailServices.headerParser.removeDuplicateAddresses(
+ addresses,
+ ""
+ );
+ let numAddresses =
+ MailServices.headerParser.parseEncodedHeader(uniqueAddresses).length;
+
+ // I don't want to count my address in the number of addresses to reply
+ // to, since I won't be emailing myself.
+ if (imInAddresses) {
+ numAddresses--;
+ }
+
+ // ReplyAll is enabled if there is more than 1 person to reply to.
+ return numAddresses > 1;
+}
+
+/**
+ * Should the reply-list command/button be enabled?
+ *
+ * @return whether the reply-list command/button should be enabled.
+ */
+function IsReplyListEnabled() {
+ // ReplyToList is enabled if there is a List-Post header
+ // with the correct format.
+ let listPost = currentHeaderData["list-post"];
+ if (!listPost) {
+ return false;
+ }
+
+ // XXX: Once Bug 496914 provides a parser, we should use that instead.
+ // Until then, we need to keep the following regex in sync with the
+ // listPost parsing in nsMsgCompose.cpp's
+ // QuotingOutputStreamListener::OnStopRequest.
+ return /<mailto:.+>/.test(listPost.headerValue);
+}
+
+/**
+ * Update the enabled/disabled states of the Reply, Reply-All, and
+ * Reply-List buttons. (After this function runs, one of the buttons
+ * should be shown, and the others should be hidden.)
+ */
+function UpdateReplyButtons() {
+ // If we have no message, because we're being called from
+ // MailToolboxCustomizeDone before someone selected a message, then just
+ // return.
+ if (!gMessage) {
+ return;
+ }
+
+ let buttonToShow;
+ if (gFolder?.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)) {
+ // News messages always default to the "followup" dual-button.
+ buttonToShow = "followup";
+ } else if (FeedUtils.isFeedMessage(gMessage)) {
+ // RSS items hide all the reply buttons.
+ buttonToShow = null;
+ } else if (IsReplyListEnabled()) {
+ // Mail messages show the "reply" button (not the dual-button) and
+ // possibly the "reply all" and "reply list" buttons.
+ buttonToShow = "replyList";
+ } else if (IsReplyAllEnabled()) {
+ buttonToShow = "replyAll";
+ } else {
+ buttonToShow = "reply";
+ }
+
+ let smartReplyButton = document.getElementById("hdrSmartReplyButton");
+ if (smartReplyButton) {
+ let replyButton = document.getElementById("hdrReplyButton");
+ let replyAllButton = document.getElementById("hdrReplyAllButton");
+ let replyListButton = document.getElementById("hdrReplyListButton");
+ let followupButton = document.getElementById("hdrFollowupButton");
+
+ replyButton.hidden = buttonToShow != "reply";
+ replyAllButton.hidden = buttonToShow != "replyAll";
+ replyListButton.hidden = buttonToShow != "replyList";
+ followupButton.hidden = buttonToShow != "followup";
+ }
+
+ let replyToSenderButton = document.getElementById("hdrReplyToSenderButton");
+ if (replyToSenderButton) {
+ if (FeedUtils.isFeedMessage(gMessage)) {
+ replyToSenderButton.hidden = true;
+ } else if (smartReplyButton) {
+ replyToSenderButton.hidden = buttonToShow == "reply";
+ } else {
+ replyToSenderButton.hidden = false;
+ }
+ }
+
+ // Run this method only after all the header toolbar buttons have been updated
+ // so we deal with the actual state.
+ headerToolbarNavigation.updateRovingTab();
+}
+
+/**
+ * Update the enabled/disabled states of the Reply, Reply-All, Reply-List,
+ * Followup, and Forward buttons based on the number of identities.
+ * If there are no identities, all of these buttons should be disabled.
+ */
+function updateComposeButtons() {
+ const hasIdentities = MailServices.accounts.allIdentities.length;
+ for (let id of [
+ "hdrReplyButton",
+ "hdrReplyAllButton",
+ "hdrReplyListButton",
+ "hdrFollowupButton",
+ "hdrForwardButton",
+ "hdrReplyToSenderButton",
+ ]) {
+ document.getElementById(id).disabled = !hasIdentities;
+ }
+}
+
+function SelectedMessagesAreJunk() {
+ try {
+ let junkScore = gMessage.getStringProperty("junkscore");
+ return junkScore != "" && junkScore != "0";
+ } catch (ex) {
+ return false;
+ }
+}
+
+function SelectedMessagesAreRead() {
+ return gMessage?.isRead;
+}
+
+function SelectedMessagesAreFlagged() {
+ return gMessage?.isFlagged;
+}
+
+function MsgReplyMessage(event) {
+ if (gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)) {
+ MsgReplyGroup(event);
+ } else {
+ MsgReplySender(event);
+ }
+}
+
+function MsgReplySender(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.ReplyToSender, event);
+}
+
+function MsgReplyGroup(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.ReplyToGroup, event);
+}
+
+function MsgReplyToAllMessage(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.ReplyAll, event);
+}
+
+function MsgReplyToListMessage(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.ReplyToList, event);
+}
+
+function MsgForwardMessage(event) {
+ var forwardType = Services.prefs.getIntPref("mail.forward_message_mode", 0);
+
+ // mail.forward_message_mode could be 1, if the user migrated from 4.x
+ // 1 (forward as quoted) is obsolete, so we treat is as forward inline
+ // since that is more like forward as quoted then forward as attachment
+ if (forwardType == 0) {
+ MsgForwardAsAttachment(event);
+ } else {
+ MsgForwardAsInline(event);
+ }
+}
+
+function MsgForwardAsAttachment(event) {
+ commandController._composeMsgByType(
+ Ci.nsIMsgCompType.ForwardAsAttachment,
+ event
+ );
+}
+
+function MsgForwardAsInline(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.ForwardInline, event);
+}
+
+function MsgRedirectMessage(event) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.Redirect, event);
+}
+
+function MsgEditMessageAsNew(aEvent) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.EditAsNew, aEvent);
+}
+
+function MsgEditDraftMessage(aEvent) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.Draft, aEvent);
+}
+
+function MsgNewMessageFromTemplate(aEvent) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.Template, aEvent);
+}
+
+function MsgEditTemplateMessage(aEvent) {
+ commandController._composeMsgByType(Ci.nsIMsgCompType.EditTemplate, aEvent);
+}
+
+function MsgComposeDraftMessage() {
+ top.ComposeMessage(
+ Ci.nsIMsgCompType.Draft,
+ Ci.nsIMsgCompFormat.Default,
+ gFolder,
+ [gMessageURI]
+ );
+}
+
+/**
+ * Update the "archive", "junk" and "delete" buttons in the message header area.
+ */
+function updateHeaderToolbarButtons() {
+ let isDummyMessage = !gViewWrapper.isSynthetic && !gMessage.folder;
+ let archiveButton = document.getElementById("hdrArchiveButton");
+ let junkButton = document.getElementById("hdrJunkButton");
+ let trashButton = document.getElementById("hdrTrashButton");
+
+ if (isDummyMessage) {
+ archiveButton.disabled = true;
+ junkButton.disabled = true;
+ trashButton.disabled = true;
+ return;
+ }
+
+ archiveButton.disabled = !MessageArchiver.canArchive([gMessage]);
+ let junkScore = gMessage.getStringProperty("junkscore");
+ let hideJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE;
+ if (!commandController._getViewCommandStatus(Ci.nsMsgViewCommandType.junk)) {
+ hideJunk = true;
+ }
+ junkButton.disabled = hideJunk;
+ trashButton.disabled = false;
+}
+
+/**
+ * Checks if the selected messages can be marked as read or unread
+ *
+ * @param markingRead true if trying to mark messages as read, false otherwise
+ * @return true if the chosen operation can be performed
+ */
+function CanMarkMsgAsRead(markingRead) {
+ return gMessage && SelectedMessagesAreRead() != markingRead;
+}
+
+/**
+ * Marks the selected messages as read or unread
+ *
+ * @param read true if trying to mark messages as read, false if marking unread,
+ * undefined if toggling the read status
+ */
+function MsgMarkMsgAsRead(read) {
+ if (read == undefined) {
+ read = !gMessage.isRead;
+ }
+ MarkSelectedMessagesRead(read);
+}
+
+function MsgMarkAsFlagged() {
+ MarkSelectedMessagesFlagged(!SelectedMessagesAreFlagged());
+}
+
+/**
+ * Extract email data and prefill the event/task dialog with that data.
+ */
+function convertToEventOrTask(isTask = false) {
+ window.top.calendarExtract.extractFromEmail(gMessage, isTask);
+}
+
+/**
+ * Triggered by the onHdrPropertyChanged notification for a single message being
+ * displayed. We handle updating the message display if our displayed message
+ * might have had its junk status change. This primarily entails updating the
+ * notification bar (that thing that appears above the message and says "this
+ * message might be junk") and (potentially) reloading the message because junk
+ * status affects the form of HTML display used (sanitized vs not).
+ * When our tab implementation is no longer multiplexed (reusing the same
+ * display widget), this must be moved into the MessageDisplayWidget or
+ * otherwise be scoped to the tab.
+ *
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the message with a junk status change.
+ */
+function HandleJunkStatusChanged(msgHdr) {
+ if (!msgHdr || !msgHdr.folder) {
+ return;
+ }
+
+ let junkBarStatus = gMessageNotificationBar.checkJunkMsgStatus(msgHdr);
+
+ // Only reload message if junk bar display state is changing and only if the
+ // reload is really needed.
+ if (junkBarStatus != 0) {
+ // We may be forcing junk mail to be rendered with sanitized html.
+ // In that scenario, we want to reload the message if the status has just
+ // changed to not junk.
+ var sanitizeJunkMail = Services.prefs.getBoolPref(
+ "mail.spam.display.sanitize"
+ );
+
+ // Only bother doing this if we are modifying the html for junk mail....
+ if (sanitizeJunkMail) {
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ let isJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE;
+
+ // If the current row isn't going to change, reload to show sanitized or
+ // unsanitized. Otherwise we wouldn't see the reloaded version anyway.
+ // 1) When marking as non-junk from the Junk folder, the msg would move
+ // back to the Inbox -> no reload needed
+ // When marking as non-junk from a folder other than the Junk folder,
+ // the message isn't moved back to Inbox -> reload needed
+ // (see nsMsgDBView::DetermineActionsForJunkChange)
+ // 2) When marking as junk, the msg will move or delete, if manualMark is set.
+ // 3) Marking as junk in the junk folder just changes the junk status.
+ if (
+ (!isJunk && !msgHdr.folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk)) ||
+ (isJunk && !msgHdr.folder.server.spamSettings.manualMark) ||
+ (isJunk && msgHdr.folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk))
+ ) {
+ ReloadMessage();
+ return;
+ }
+ }
+ }
+
+ gMessageNotificationBar.setJunkMsg(msgHdr);
+}
+
+/**
+ * Object to handle message related notifications that are showing in a
+ * notificationbox above the message content.
+ */
+var gMessageNotificationBar = {
+ get stringBundle() {
+ delete this.stringBundle;
+ return (this.stringBundle = document.getElementById("bundle_messenger"));
+ },
+
+ get brandBundle() {
+ delete this.brandBundle;
+ return (this.brandBundle = document.getElementById("bundle_brand"));
+ },
+
+ get msgNotificationBar() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "top");
+ document.getElementById("mail-notification-top").append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Check if the current status of the junk notification is correct or not.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - Information about the message
+ * @returns {integer} Tri-state status information.
+ * 1: notification is missing
+ * 0: notification is correct
+ * -1: notification must be removed
+ */
+ checkJunkMsgStatus(aMsgHdr) {
+ let junkScore = aMsgHdr ? aMsgHdr.getStringProperty("junkscore") : "";
+ let junkStatus = this.isShowingJunkNotification();
+
+ if (junkScore == "" || junkScore == Ci.nsIJunkMailPlugin.IS_HAM_SCORE) {
+ // This is not junk. The notification should not be shown.
+ return junkStatus ? -1 : 0;
+ }
+
+ // This is junk. The notification should be shown.
+ return junkStatus ? 0 : 1;
+ },
+
+ setJunkMsg(aMsgHdr) {
+ goUpdateCommand("cmd_junk");
+
+ let junkBarStatus = this.checkJunkMsgStatus(aMsgHdr);
+ if (junkBarStatus == -1) {
+ this.msgNotificationBar.removeNotification(
+ this.msgNotificationBar.getNotificationWithValue("junkContent"),
+ true
+ );
+ } else if (junkBarStatus == 1) {
+ let brandName = this.brandBundle.getString("brandShortName");
+ let junkBarMsg = this.stringBundle.getFormattedString("junkBarMessage", [
+ brandName,
+ ]);
+
+ let buttons = [
+ {
+ label: this.stringBundle.getString("junkBarInfoButton"),
+ accessKey: this.stringBundle.getString("junkBarInfoButtonKey"),
+ popup: null,
+ callback(aNotification, aButton) {
+ // TODO: This doesn't work in a message window.
+ top.openContentTab(
+ "https://support.mozilla.org/kb/thunderbird-and-junk-spam-messages"
+ );
+ return true; // keep notification open
+ },
+ },
+ {
+ label: this.stringBundle.getString("junkBarButton"),
+ accessKey: this.stringBundle.getString("junkBarButtonKey"),
+ popup: null,
+ callback(aNotification, aButton) {
+ commandController.doCommand("cmd_markAsNotJunk");
+ // Return true (=don't close) since changing junk status will fire a
+ // JunkStatusChanged notification which will make the junk bar go away
+ // for this message -> no notification to close anymore -> trying to
+ // close would just fail.
+ return true;
+ },
+ },
+ ];
+
+ this.msgNotificationBar.appendNotification(
+ "junkContent",
+ {
+ label: junkBarMsg,
+ image: "chrome://messenger/skin/icons/junk.svg",
+ priority: this.msgNotificationBar.PRIORITY_WARNING_HIGH,
+ },
+ buttons
+ );
+ }
+ },
+
+ isShowingJunkNotification() {
+ return !!this.msgNotificationBar.getNotificationWithValue("junkContent");
+ },
+
+ setRemoteContentMsg(aMsgHdr, aContentURI, aCanOverride) {
+ // update the allow remote content for sender string
+ let brandName = this.brandBundle.getString("brandShortName");
+ let remoteContentMsg = this.stringBundle.getFormattedString(
+ "remoteContentBarMessage",
+ [brandName]
+ );
+
+ let buttonLabel = this.stringBundle.getString(
+ AppConstants.platform == "win"
+ ? "remoteContentPrefLabel"
+ : "remoteContentPrefLabelUnix"
+ );
+ let buttonAccesskey = this.stringBundle.getString(
+ AppConstants.platform == "win"
+ ? "remoteContentPrefAccesskey"
+ : "remoteContentPrefAccesskeyUnix"
+ );
+
+ let buttons = [
+ {
+ label: buttonLabel,
+ accessKey: buttonAccesskey,
+ popup: "remoteContentOptions",
+ callback() {},
+ },
+ ];
+
+ // The popup value is a space separated list of all the blocked origins.
+ let popup = document.getElementById("remoteContentOptions");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ aContentURI,
+ {}
+ );
+ let origins = popup.value ? popup.value.split(" ") : [];
+ if (!origins.includes(principal.origin)) {
+ origins.push(principal.origin);
+ }
+ popup.value = origins.join(" ");
+
+ if (!this.isShowingRemoteContentNotification()) {
+ let notification = this.msgNotificationBar.appendNotification(
+ "remoteContent",
+ {
+ label: remoteContentMsg,
+ image: "chrome://messenger/skin/icons/remote-blocked.svg",
+ priority: this.msgNotificationBar.PRIORITY_WARNING_MEDIUM,
+ },
+ aCanOverride ? buttons : []
+ );
+
+ notification.buttonContainer.firstElementChild.classList.add(
+ "button-menu-list"
+ );
+ }
+ },
+
+ isShowingRemoteContentNotification() {
+ return !!this.msgNotificationBar.getNotificationWithValue("remoteContent");
+ },
+
+ setPhishingMsg() {
+ let phishingMsgNote = this.stringBundle.getString("phishingBarMessage");
+
+ let buttonLabel = this.stringBundle.getString(
+ AppConstants.platform == "win"
+ ? "phishingBarPrefLabel"
+ : "phishingBarPrefLabelUnix"
+ );
+ let buttonAccesskey = this.stringBundle.getString(
+ AppConstants.platform == "win"
+ ? "phishingBarPrefAccesskey"
+ : "phishingBarPrefAccesskeyUnix"
+ );
+
+ let buttons = [
+ {
+ label: buttonLabel,
+ accessKey: buttonAccesskey,
+ popup: "phishingOptions",
+ callback(aNotification, aButton) {},
+ },
+ ];
+
+ if (!this.isShowingPhishingNotification()) {
+ let notification = this.msgNotificationBar.appendNotification(
+ "maybeScam",
+ {
+ label: phishingMsgNote,
+ image: "chrome://messenger/skin/icons/phishing.svg",
+ priority: this.msgNotificationBar.PRIORITY_CRITICAL_MEDIUM,
+ },
+ buttons
+ );
+
+ notification.buttonContainer.firstElementChild.classList.add(
+ "button-menu-list"
+ );
+ }
+ },
+
+ isShowingPhishingNotification() {
+ return !!this.msgNotificationBar.getNotificationWithValue("maybeScam");
+ },
+
+ setMDNMsg(aMdnGenerator, aMsgHeader, aMimeHdr) {
+ this.mdnGenerator = aMdnGenerator;
+ // Return receipts can be RFC 3798 or not.
+ let mdnHdr =
+ aMimeHdr.extractHeader("Disposition-Notification-To", false) ||
+ aMimeHdr.extractHeader("Return-Receipt-To", false); // not
+ let fromHdr = aMimeHdr.extractHeader("From", false);
+
+ let mdnAddr =
+ MailServices.headerParser.extractHeaderAddressMailboxes(mdnHdr);
+ let fromAddr =
+ MailServices.headerParser.extractHeaderAddressMailboxes(fromHdr);
+
+ let authorName =
+ MailServices.headerParser.extractFirstName(
+ aMsgHeader.mime2DecodedAuthor
+ ) || aMsgHeader.author;
+
+ // If the return receipt doesn't go to the sender address, note that in the
+ // notification.
+ let mdnBarMsg =
+ mdnAddr != fromAddr
+ ? this.stringBundle.getFormattedString("mdnBarMessageAddressDiffers", [
+ authorName,
+ mdnAddr,
+ ])
+ : this.stringBundle.getFormattedString("mdnBarMessageNormal", [
+ authorName,
+ ]);
+
+ let buttons = [
+ {
+ label: this.stringBundle.getString("mdnBarSendReqButton"),
+ accessKey: this.stringBundle.getString("mdnBarSendReqButtonKey"),
+ popup: null,
+ callback(aNotification, aButton) {
+ SendMDNResponse();
+ return false; // close notification
+ },
+ },
+ {
+ label: this.stringBundle.getString("mdnBarIgnoreButton"),
+ accessKey: this.stringBundle.getString("mdnBarIgnoreButtonKey"),
+ popup: null,
+ callback(aNotification, aButton) {
+ IgnoreMDNResponse();
+ return false; // close notification
+ },
+ },
+ ];
+
+ this.msgNotificationBar.appendNotification(
+ "mdnRequested",
+ {
+ label: mdnBarMsg,
+ priority: this.msgNotificationBar.PRIORITY_INFO_MEDIUM,
+ },
+ buttons
+ );
+ },
+
+ setDraftEditMessage() {
+ if (!gMessage || !gFolder) {
+ return;
+ }
+
+ if (gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Drafts, true)) {
+ let draftMsgNote = this.stringBundle.getString("draftMessageMsg");
+
+ let buttons = [
+ {
+ label: this.stringBundle.getString("draftMessageButton"),
+ accessKey: this.stringBundle.getString("draftMessageButtonKey"),
+ popup: null,
+ callback(aNotification, aButton) {
+ MsgComposeDraftMessage();
+ return true; // keep notification open
+ },
+ },
+ ];
+
+ this.msgNotificationBar.appendNotification(
+ "draftMsgContent",
+ {
+ label: draftMsgNote,
+ priority: this.msgNotificationBar.PRIORITY_INFO_HIGH,
+ },
+ buttons
+ );
+ }
+ },
+
+ clearMsgNotifications() {
+ this.msgNotificationBar.removeAllNotifications(true);
+ },
+};
+
+/**
+ * LoadMsgWithRemoteContent
+ * Reload the current message, allowing remote content
+ */
+function LoadMsgWithRemoteContent() {
+ // we want to get the msg hdr for the currently selected message
+ // change the "remoteContentBar" property on it
+ // then reload the message
+
+ setMsgHdrPropertyAndReload("remoteContentPolicy", kAllowRemoteContent);
+ window.content?.focus();
+}
+
+/**
+ * Populate the remote content options for the current message.
+ */
+function onRemoteContentOptionsShowing(aEvent) {
+ let origins = aEvent.target.value ? aEvent.target.value.split(" ") : [];
+
+ let addresses = MailServices.headerParser.parseEncodedHeader(gMessage.author);
+ addresses = addresses.slice(0, 1);
+ // If there is an author's email, put it also in the menu.
+ let adrCount = addresses.length;
+ if (adrCount > 0) {
+ let authorEmailAddress = addresses[0].email;
+ let authorEmailAddressURI = Services.io.newURI(
+ "chrome://messenger/content/email=" + authorEmailAddress
+ );
+ let mailPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ authorEmailAddressURI,
+ {}
+ );
+ origins.push(mailPrincipal.origin);
+ }
+
+ let messengerBundle = document.getElementById("bundle_messenger");
+
+ // Out with the old...
+ let children = aEvent.target.children;
+ for (let i = children.length - 1; i >= 0; i--) {
+ if (children[i].getAttribute("class") == "allow-remote-uri") {
+ children[i].remove();
+ }
+ }
+
+ let urlSepar = document.getElementById("remoteContentAllMenuSeparator");
+
+ // ... and in with the new.
+ for (let origin of origins) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ messengerBundle.getFormattedString("remoteAllowResource", [
+ origin.replace("chrome://messenger/content/email=", ""),
+ ])
+ );
+ menuitem.setAttribute("value", origin);
+ menuitem.setAttribute("class", "allow-remote-uri");
+ menuitem.setAttribute("oncommand", "allowRemoteContentForURI(this.value);");
+ if (origin.startsWith("chrome://messenger/content/email=")) {
+ aEvent.target.appendChild(menuitem);
+ } else {
+ aEvent.target.insertBefore(menuitem, urlSepar);
+ }
+ }
+
+ let URLcount = origins.length - adrCount;
+ let allowAllItem = document.getElementById("remoteContentOptionAllowAll");
+ let allURLLabel = messengerBundle.getString("remoteAllowAll");
+ allowAllItem.label = PluralForm.get(URLcount, allURLLabel).replace(
+ "#1",
+ URLcount
+ );
+
+ allowAllItem.collapsed = URLcount < 2;
+ document.getElementById("remoteContentOriginsMenuSeparator").collapsed =
+ urlSepar.collapsed = allowAllItem.collapsed && adrCount == 0;
+}
+
+/**
+ * Add privileges to display remote content for the given uri.
+ *
+ * @param aUriSpec |String| uri for the site to add permissions for.
+ * @param aReload Reload the message display after allowing the URI.
+ */
+function allowRemoteContentForURI(aUriSpec, aReload = true) {
+ let uri = Services.io.newURI(aUriSpec);
+ Services.perms.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipal(uri, {}),
+ "image",
+ Services.perms.ALLOW_ACTION
+ );
+ if (aReload) {
+ ReloadMessage();
+ }
+}
+
+/**
+ * Add privileges to display remote content for the given uri.
+ *
+ * @param aListNode The menulist element containing the URIs to allow.
+ */
+function allowRemoteContentForAll(aListNode) {
+ let uriNodes = aListNode.querySelectorAll(".allow-remote-uri");
+ for (let uriNode of uriNodes) {
+ if (!uriNode.value.startsWith("chrome://messenger/content/email=")) {
+ allowRemoteContentForURI(uriNode.value, false);
+ }
+ }
+ ReloadMessage();
+}
+
+/**
+ * Displays fine-grained, per-site preferences for remote content.
+ */
+function editRemoteContentSettings() {
+ top.openOptionsDialog("panePrivacy", "privacyCategory");
+}
+
+/**
+ * Set the msg hdr flag to ignore the phishing warning and reload the message.
+ */
+function IgnorePhishingWarning() {
+ // This property should really be called skipPhishingWarning or something
+ // like that, but it's too late to change that now.
+ // This property is used to suppress the phishing bar for the message.
+ setMsgHdrPropertyAndReload("notAPhishMessage", 1);
+}
+
+/**
+ * Open the preferences dialog to allow disabling the scam feature.
+ */
+function OpenPhishingSettings() {
+ top.openOptionsDialog("panePrivacy", "privacySecurityCategory");
+}
+
+function setMsgHdrPropertyAndReload(aProperty, aValue) {
+ // we want to get the msg hdr for the currently selected message
+ // change the appropriate property on it then reload the message
+ if (gMessage) {
+ gMessage.setUint32Property(aProperty, aValue);
+ ReloadMessage();
+ }
+}
+
+/**
+ * Mark a specified message as read.
+ * @param msgHdr header (nsIMsgDBHdr) of the message to mark as read
+ */
+function MarkMessageAsRead(msgHdr) {
+ ClearPendingReadTimer();
+ msgHdr.folder.markMessagesRead([msgHdr], true);
+ reportMsgRead({ isNewRead: true });
+}
+
+function ClearPendingReadTimer() {
+ if (gMarkViewedMessageAsReadTimer) {
+ clearTimeout(gMarkViewedMessageAsReadTimer);
+ gMarkViewedMessageAsReadTimer = null;
+ }
+}
+
+// this is called when layout is actually finished rendering a
+// mail message. OnMsgLoaded is called when libmime is done parsing the message
+function OnMsgParsed(aUrl) {
+ // browser doesn't do this, but I thought it could be a useful thing to test out...
+ // If the find bar is visible and we just loaded a new message, re-run
+ // the find command. This means the new message will get highlighted and
+ // we'll scroll to the first word in the message that matches the find text.
+ var findBar = document.getElementById("FindToolbar");
+ if (!findBar.hidden) {
+ findBar.onFindAgainCommand(false);
+ }
+
+ let browser = getMessagePaneBrowser();
+ // Run the phishing detector on the message if it hasn't been marked as not
+ // a scam already.
+ if (
+ gMessage &&
+ !gMessage.getUint32Property("notAPhishMessage") &&
+ PhishingDetector.analyzeMsgForPhishingURLs(aUrl, browser)
+ ) {
+ gMessageNotificationBar.setPhishingMsg();
+ }
+
+ // Notify anyone (e.g., extensions) who's interested in when a message is loaded.
+ Services.obs.notifyObservers(null, "MsgMsgDisplayed", gMessageURI);
+
+ let doc = browser && browser.contentDocument ? browser.contentDocument : null;
+
+ // Rewrite any anchor elements' href attribute to reflect that the loaded
+ // document is a mailnews url. This will cause docShell to scroll to the
+ // element in the document rather than opening the link externally.
+ let links = doc && doc.links ? doc.links : [];
+ for (let linkNode of links) {
+ if (!linkNode.hash) {
+ continue;
+ }
+
+ // We have a ref fragment which may reference a node in this document.
+ // Ensure html in mail anchors work as expected.
+ let anchorId = linkNode.hash.replace("#", "");
+ // Continue if an id (html5) or name attribute value for the ref is not
+ // found in this document.
+ let selector = "#" + anchorId + ", [name='" + anchorId + "']";
+ try {
+ if (!linkNode.ownerDocument.querySelector(selector)) {
+ continue;
+ }
+ } catch (ex) {
+ continue;
+ }
+
+ // Then check if the href url matches the document baseURL.
+ if (
+ makeURI(linkNode.href).specIgnoringRef !=
+ makeURI(linkNode.baseURI).specIgnoringRef
+ ) {
+ continue;
+ }
+
+ // Finally, if the document url is a message url, and the anchor href is
+ // http, it needs to be adjusted so docShell finds the node.
+ let messageURI = makeURI(linkNode.ownerDocument.URL);
+ if (
+ messageURI instanceof Ci.nsIMsgMailNewsUrl &&
+ linkNode.href.startsWith("http")
+ ) {
+ linkNode.href = messageURI.specIgnoringRef + linkNode.hash;
+ }
+ }
+
+ // Scale any overflowing images, exclude http content.
+ let imgs = doc && !doc.URL.startsWith("http") ? doc.images : [];
+ for (let img of imgs) {
+ if (
+ img.clientWidth - doc.body.offsetWidth >= 0 &&
+ (img.clientWidth <= img.naturalWidth || !img.naturalWidth)
+ ) {
+ img.setAttribute("overflowing", "true");
+ }
+
+ // This is the default case for images when a message is loaded.
+ img.setAttribute("shrinktofit", "true");
+ }
+}
+
+function OnMsgLoaded(aUrl) {
+ if (!aUrl) {
+ return;
+ }
+
+ window.msgLoaded = true;
+ window.dispatchEvent(
+ new CustomEvent("MsgLoaded", { detail: gMessage, bubbles: true })
+ );
+ window.dispatchEvent(
+ new CustomEvent("MsgsLoaded", { detail: [gMessage], bubbles: true })
+ );
+
+ if (!gFolder) {
+ return;
+ }
+
+ gMessageNotificationBar.setJunkMsg(gMessage);
+
+ // See if MDN was requested but has not been sent.
+ HandleMDNResponse(aUrl);
+}
+
+/**
+ * Marks the message as read, optionally after a delay, if the preferences say
+ * we should do so.
+ */
+function autoMarkAsRead() {
+ if (!gMessage?.folder) {
+ // The message can't be marked read or unread.
+ return;
+ }
+
+ if (document.hidden) {
+ // We're in an inactive docShell (probably a background tab). Wait until
+ // it becomes active before marking the message as read.
+ document.addEventListener("visibilitychange", () => autoMarkAsRead(), {
+ once: true,
+ });
+ return;
+ }
+
+ let markReadAutoMode = Services.prefs.getBoolPref(
+ "mailnews.mark_message_read.auto"
+ );
+
+ // We just finished loading a message. If messages are to be marked as read
+ // automatically, set a timer to mark the message is read after n seconds
+ // where n can be configured by the user.
+ if (!gMessage.isRead && markReadAutoMode) {
+ let markReadOnADelay = Services.prefs.getBoolPref(
+ "mailnews.mark_message_read.delay"
+ );
+
+ let winType = top.document.documentElement.getAttribute("windowtype");
+ // Only use the timer if viewing using the 3-pane preview pane and the
+ // user has set the pref.
+ if (markReadOnADelay && winType == "mail:3pane") {
+ // 3-pane window
+ ClearPendingReadTimer();
+ let markReadDelayTime = Services.prefs.getIntPref(
+ "mailnews.mark_message_read.delay.interval"
+ );
+ if (markReadDelayTime == 0) {
+ MarkMessageAsRead(gMessage);
+ } else {
+ gMarkViewedMessageAsReadTimer = setTimeout(
+ MarkMessageAsRead,
+ markReadDelayTime * 1000,
+ gMessage
+ );
+ }
+ } else {
+ // standalone msg window
+ MarkMessageAsRead(gMessage);
+ }
+ }
+}
+
+/**
+ * This function handles all mdn response generation (ie, imap and pop).
+ * For pop the msg uid can be 0 (ie, 1st msg in a local folder) so no
+ * need to check uid here. No one seems to set mimeHeaders to null so
+ * no need to check it either.
+ */
+function HandleMDNResponse(aUrl) {
+ if (!aUrl) {
+ return;
+ }
+
+ var msgFolder = aUrl.folder;
+ if (
+ !msgFolder ||
+ !gMessage ||
+ gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false)
+ ) {
+ return;
+ }
+
+ // if the message is marked as junk, do NOT attempt to process a return receipt
+ // in order to better protect the user
+ if (SelectedMessagesAreJunk()) {
+ return;
+ }
+
+ var mimeHdr;
+
+ try {
+ mimeHdr = aUrl.mimeHeaders;
+ } catch (ex) {
+ return;
+ }
+
+ // If we didn't get the message id when we downloaded the message header,
+ // we cons up an md5: message id. If we've done that, we'll try to extract
+ // the message id out of the mime headers for the whole message.
+ let msgId = gMessage.messageId;
+ if (msgId.startsWith("md5:")) {
+ var mimeMsgId = mimeHdr.extractHeader("Message-Id", false);
+ if (mimeMsgId) {
+ gMessage.messageId = mimeMsgId;
+ }
+ }
+
+ // After a msg is downloaded it's already marked READ at this point so we must check if
+ // the msg has a "Disposition-Notification-To" header and no MDN report has been sent yet.
+ if (gMessage.flags & Ci.nsMsgMessageFlags.MDNReportSent) {
+ return;
+ }
+
+ var DNTHeader = mimeHdr.extractHeader("Disposition-Notification-To", false);
+ var oldDNTHeader = mimeHdr.extractHeader("Return-Receipt-To", false);
+ if (!DNTHeader && !oldDNTHeader) {
+ return;
+ }
+
+ // Everything looks good so far, let's generate the MDN response.
+ var mdnGenerator = Cc[
+ "@mozilla.org/messenger-mdn/generator;1"
+ ].createInstance(Ci.nsIMsgMdnGenerator);
+ const MDN_DISPOSE_TYPE_DISPLAYED = 0;
+ let askUser = mdnGenerator.process(
+ MDN_DISPOSE_TYPE_DISPLAYED,
+ top.msgWindow,
+ msgFolder,
+ gMessage.messageKey,
+ mimeHdr,
+ false
+ );
+ if (askUser) {
+ gMessageNotificationBar.setMDNMsg(mdnGenerator, gMessage, mimeHdr);
+ }
+}
+
+function SendMDNResponse() {
+ gMessageNotificationBar.mdnGenerator.userAgreed();
+}
+
+function IgnoreMDNResponse() {
+ gMessageNotificationBar.mdnGenerator.userDeclined();
+}
+
+// An object to help collecting reading statistics of secure emails.
+var gSecureMsgProbe = {};
+
+/**
+ * Update gSecureMsgProbe and report to telemetry if necessary.
+ */
+function reportMsgRead({ isNewRead = false, key = null }) {
+ if (isNewRead) {
+ gSecureMsgProbe.isNewRead = true;
+ }
+ if (key) {
+ gSecureMsgProbe.key = key;
+ }
+ if (gSecureMsgProbe.key && gSecureMsgProbe.isNewRead) {
+ Services.telemetry.keyedScalarAdd(
+ "tb.mails.read_secure",
+ gSecureMsgProbe.key,
+ 1
+ );
+ }
+}
+
+window.addEventListener("secureMsgLoaded", event => {
+ reportMsgRead({ key: event.detail.key });
+});
+
+/**
+ * Roving tab navigation for the header buttons.
+ */
+var headerToolbarNavigation = {
+ /**
+ * Get all currently visible buttons of the message header toolbar.
+ *
+ * @returns {Array} An array of buttons.
+ */
+ get headerButtons() {
+ return this.headerToolbar.querySelectorAll(
+ `toolbarbutton:not([hidden="true"],[is="toolbarbutton-menu-button"]),toolbaritem[id="hdrSmartReplyButton"]>toolbarbutton:not([hidden="true"])>dropmarker, button:not([hidden])`
+ );
+ },
+
+ init() {
+ this.headerToolbar = document.getElementById("header-view-toolbar");
+ this.headerToolbar.addEventListener("keypress", event => {
+ this.triggerMessageHeaderRovingTab(event);
+ });
+ },
+
+ /**
+ * Update the `tabindex` attribute of the currently visible buttons.
+ */
+ updateRovingTab() {
+ for (let button of this.headerButtons) {
+ button.tabIndex = -1;
+ }
+ // Allow focus on the first available button.
+ // We use `setAttribute` to guarantee compatibility with XUL toolbarbuttons.
+ this.headerButtons[0].setAttribute("tabindex", "0");
+ },
+
+ /**
+ * Handles the keypress event on the message header toolbar.
+ *
+ * @param {Event} event - The keypress DOMEvent.
+ */
+ triggerMessageHeaderRovingTab(event) {
+ // Expected keyboard actions are Left, Right, Home, End, Space, and Enter.
+ if (
+ !["ArrowRight", "ArrowLeft", "Home", "End", " ", "Enter"].includes(
+ event.key
+ )
+ ) {
+ return;
+ }
+
+ const headerButtons = [...this.headerButtons];
+ let focusableButton = headerButtons.find(b => b.tabIndex != -1);
+ let elementIndex = headerButtons.indexOf(focusableButton);
+
+ // TODO: Remove once the buttons are updated to not be XUL
+ // NOTE: Normally a button click handler would cover Enter and Space key
+ // events. However, we need to prevent the default behavior and explicitly
+ // trigger the button click because the XUL toolbarbuttons do not work when
+ // the Enter key is pressed. They do work when the Space key is pressed.
+ // However, if the toolbarbutton is a dropdown menu, the Space key
+ // does not open the menu.
+ if (
+ event.key == "Enter" ||
+ (event.key == " " && event.target.hasAttribute("type"))
+ ) {
+ if (
+ event.target.getAttribute("class") ==
+ "toolbarbutton-menubutton-dropmarker"
+ ) {
+ event.preventDefault();
+ event.target.parentNode
+ .querySelector("menupopup")
+ .openPopup(event.target.parentNode, "after_end", {
+ triggerEvent: event,
+ });
+ } else {
+ event.preventDefault();
+ event.target.click();
+ return;
+ }
+ }
+
+ // Find the adjacent focusable element based on the pressed key.
+ if (
+ (document.dir == "rtl" && event.key == "ArrowLeft") ||
+ (document.dir == "ltr" && event.key == "ArrowRight")
+ ) {
+ elementIndex++;
+ if (elementIndex > headerButtons.length - 1) {
+ elementIndex = 0;
+ }
+ } else if (
+ (document.dir == "ltr" && event.key == "ArrowLeft") ||
+ (document.dir == "rtl" && event.key == "ArrowRight")
+ ) {
+ elementIndex--;
+ if (elementIndex == -1) {
+ elementIndex = headerButtons.length - 1;
+ }
+ }
+
+ // Move the focus to a new toolbar button and update the tabindex attribute.
+ let newFocusableButton = headerButtons[elementIndex];
+ if (newFocusableButton) {
+ focusableButton.tabIndex = -1;
+ newFocusableButton.setAttribute("tabindex", "0");
+ newFocusableButton.focus();
+ }
+ },
+};