4545 строки
144 KiB
JavaScript
4545 строки
144 KiB
JavaScript
/* 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 */
|
|
/* global openpgpSink */ // From enigmailMsgHdrViewOverlay.js
|
|
|
|
/* globals MozElements */
|
|
|
|
var { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
var { MailServices } = ChromeUtils.importESModule(
|
|
"resource:///modules/MailServices.sys.mjs"
|
|
);
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
AttachmentInfo: "resource:///modules/AttachmentInfo.sys.mjs",
|
|
Gloda: "resource:///modules/gloda/GlodaPublic.sys.mjs",
|
|
GlodaUtils: "resource:///modules/gloda/GlodaUtils.sys.mjs",
|
|
MailUtils: "resource:///modules/MailUtils.sys.mjs",
|
|
MessageArchiver: "resource:///modules/MessageArchiver.sys.mjs",
|
|
PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.sys.mjs",
|
|
PluralForm: "resource:///modules/PluralForm.sys.mjs",
|
|
|
|
calendarDeactivator:
|
|
"resource:///modules/calendar/calCalendarDeactivator.sys.mjs",
|
|
});
|
|
|
|
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;
|
|
|
|
/**
|
|
* 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 of common headers and mapping for how they should 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=updateHeaderValue] - 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.
|
|
* @param {boolean} [hidden=false] - True if the header should normally be hidden.
|
|
* Modes and preferences may affect whether it's really displayed in the end.
|
|
*/
|
|
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: "sender", outputFunction: outputEmailAddresses, hidden: true },
|
|
{ name: "in-reply-to", outputFunction: outputMessageIds, hidden: true },
|
|
{ name: "message-id", outputFunction: outputMessageIds, hidden: true },
|
|
{ name: "content-base" },
|
|
{ name: "tags", outputFunction: outputTags },
|
|
{ name: "list-id" },
|
|
{ name: "list-help", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "list-unsubscribe", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "list-subscribe", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "list-post", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "list-owner", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "list-archive", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "archived-at", outputFunction: outputMultiURL, hidden: true },
|
|
{ name: "user-agent", hidden: true },
|
|
{ name: "organization", hidden: true },
|
|
];
|
|
|
|
/**
|
|
* 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) {
|
|
// 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() {}
|
|
onHdrAdded() {}
|
|
onParentChanged() {}
|
|
onAnnouncerGoingAway() {}
|
|
onReadChanged() {}
|
|
onJunkScoreChanged() {}
|
|
onHdrPropertyChanged(hdrToChange, property, preChange) {
|
|
// 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() {}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
class MsgHeaderEntry {
|
|
/**
|
|
* @param {string} prefix - The name of the view (e.g. "expanded").
|
|
* @param {object} headerListInfo - Entry, from gExpandedHeaderList.
|
|
*/
|
|
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;
|
|
this.hidden = !!headerListInfo.hidden;
|
|
}
|
|
}
|
|
|
|
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 (const header of gExpandedHeaderList) {
|
|
gExpandedHeaderView[header.name] = new MsgHeaderEntry("expanded", header);
|
|
}
|
|
|
|
const extraHeaders = Services.prefs
|
|
.getCharPref("mailnews.headers.extraExpandedHeaders")
|
|
.split(" ");
|
|
for (const extraHeaderName of extraHeaders) {
|
|
if (!extraHeaderName.trim()) {
|
|
continue;
|
|
}
|
|
gExpandedHeaderView[extraHeaderName.toLowerCase()] ??= new HeaderView(
|
|
extraHeaderName,
|
|
extraHeaderName
|
|
);
|
|
}
|
|
|
|
const otherHeaders = Services.prefs
|
|
.getCharPref("mail.compose.other.header", "")
|
|
.split(",")
|
|
.map(h => h.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const otherHeaderName of otherHeaders) {
|
|
gExpandedHeaderView[otherHeaderName.toLowerCase()] ??= new HeaderView(
|
|
otherHeaderName,
|
|
otherHeaderName
|
|
);
|
|
}
|
|
|
|
// Showing headers, mapped to the pref controlling display.
|
|
const headerPref = new Map([
|
|
["organization", "mailnews.headers.showOrganization"],
|
|
["user-agent", "mailnews.headers.showUserAgent"],
|
|
["message-id", "mailnews.headers.showMessageId"],
|
|
["sender", "mailnews.headers.showSender"],
|
|
|
|
// RFC 2369 headers.
|
|
["list-help", "mailnews.headers.showListHelp"],
|
|
["list-unsubscribe", "mailnews.headers.showListUnsubscribe"],
|
|
["list-subscribe", "mailnews.headers.showListSubscribe"],
|
|
["list-post", "mailnews.headers.showListPost"],
|
|
["list-owner", "mailnews.headers.showListOwner"],
|
|
["list-archive", "mailnews.headers.showListArchive"],
|
|
|
|
// RFC 5064.
|
|
["archived-at", "mailnews.headers.showArchivedAt"],
|
|
]);
|
|
|
|
for (const [header, pref] of headerPref) {
|
|
if (!Services.prefs.getBoolPref(pref, false)) {
|
|
continue;
|
|
}
|
|
const entry = gExpandedHeaderList.find(h => h.name == header);
|
|
entry.hidden = false;
|
|
gExpandedHeaderView[entry.name] = new MsgHeaderEntry("expanded", entry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show security info dialog when keyboard shortcut it invoked.
|
|
* @param {Event} event - keypress event
|
|
*/
|
|
async function msgSecurityKeypressHandler(event) {
|
|
// 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.
|
|
const shortcut = await document.l10n.formatValue(
|
|
"message-header-show-security-info-key"
|
|
);
|
|
if (
|
|
event.ctrlKey &&
|
|
(event.altKey || event.metaKey) &&
|
|
event.key.toLowerCase() == shortcut.toLowerCase()
|
|
) {
|
|
showMessageReadSecurityInfo();
|
|
}
|
|
}
|
|
|
|
async function OnLoadMsgHeaderPane() {
|
|
// Load any preferences that at are global with regards to
|
|
// displaying a message...
|
|
gMinNumberOfHeaders = Services.prefs.getIntPref(
|
|
"mailnews.headers.minNumHeaders"
|
|
);
|
|
|
|
Services.obs.addObserver(MsgHdrViewObserver, "remote-content-blocked");
|
|
|
|
initializeHeaderViewTables();
|
|
|
|
top.document.addEventListener("keypress", msgSecurityKeypressHandler);
|
|
|
|
headerToolbarNavigation.init();
|
|
|
|
// Set up event listeners for the encryption technology button and panel.
|
|
document
|
|
.getElementById("encryptionTechBtn")
|
|
.addEventListener("click", showMessageReadSecurityInfo);
|
|
const 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.
|
|
const 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() {
|
|
const headerViewElement = document.getElementById("msgHeaderView");
|
|
if (!headerViewElement.loaded) {
|
|
// We're unloading, but we never loaded.
|
|
return;
|
|
}
|
|
|
|
Services.obs.removeObserver(MsgHdrViewObserver, "remote-content-blocked");
|
|
|
|
clearFolderDBListener();
|
|
ClearPendingReadTimer();
|
|
|
|
top.document.removeEventListener("keypress", msgSecurityKeypressHandler);
|
|
|
|
// 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) {
|
|
if (topic == "remote-content-blocked") {
|
|
const 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) {
|
|
if (
|
|
!(request instanceof Ci.nsIMailChannel) ||
|
|
!(stateFlags & Ci.nsIWebProgressListener.STATE_START)
|
|
) {
|
|
return;
|
|
}
|
|
// Clear the previously displayed message.
|
|
// Note: Using .hidden = true or .style.display = "none" causes white
|
|
// flicker in dark mode.
|
|
const previousBodyElement = getMessagePaneBrowser().contentDocument?.body;
|
|
if (previousBodyElement) {
|
|
previousBodyElement.innerHTML = "";
|
|
}
|
|
ClearAttachmentList();
|
|
gMessageNotificationBar.clearMsgNotifications();
|
|
|
|
request.listener = this;
|
|
request.openpgpSink = openpgpSink;
|
|
request.smimeSink = smimeSink;
|
|
this.onStartHeaders();
|
|
},
|
|
|
|
/**
|
|
* Step 2: The message headers are available on the channel.
|
|
*
|
|
* @param {nsIMailChannel} mailChannel
|
|
* @see {nsIMailProgressListener}
|
|
*/
|
|
onHeadersComplete(mailChannel) {
|
|
window.dispatchEvent(
|
|
new CustomEvent("MsgLoading", { detail: gMessage, bubbles: true })
|
|
);
|
|
|
|
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() {
|
|
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.openpgpSink = null;
|
|
channel.smimeSink = 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...
|
|
const 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 (const 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 (const 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 (const listener of gMessageListeners) {
|
|
listener.onEndHeaders();
|
|
}
|
|
},
|
|
|
|
processHeaders(headerNames, headerValues) {
|
|
const kMailboxSeparator = ", ";
|
|
var index = 0;
|
|
for (let i = 0; i < headerNames.length; i++) {
|
|
const 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) {
|
|
const senderMailbox =
|
|
kMailboxSeparator +
|
|
MailServices.headerParser.extractHeaderAddressMailboxes(
|
|
currentHeaderData.sender.headerValue
|
|
) +
|
|
kMailboxSeparator;
|
|
const 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
|
|
) {
|
|
const replyToMailbox =
|
|
MailServices.headerParser.extractHeaderAddressMailboxes(
|
|
currentHeaderData["reply-to"].headerValue
|
|
);
|
|
const fromMailboxes =
|
|
MailServices.headerParser.extractHeaderAddressMailboxes(
|
|
currentHeaderData.from.headerValue
|
|
);
|
|
const 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
|
|
);
|
|
}
|
|
|
|
const 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) {
|
|
const 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) {
|
|
const last = currentAttachments[currentAttachments.length - 1];
|
|
if (
|
|
field == "X-Mozilla-PartSize" &&
|
|
!last.isFileAttachment &&
|
|
!last.isDeleted
|
|
) {
|
|
const 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 (const 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) {
|
|
const 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 (const img of browser.contentDocument.body.getElementsByClassName(
|
|
"moz-attached-image"
|
|
)) {
|
|
for (const 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 () {
|
|
if (this.clientWidth > this.parentNode.clientWidth) {
|
|
img.setAttribute("overflowing", "true");
|
|
img.setAttribute("shrinktofit", "true");
|
|
}
|
|
},
|
|
{ once: 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;
|
|
}
|
|
|
|
const flagButton = document.getElementById("starMessageButton");
|
|
flagButton.hidden = false;
|
|
|
|
const isFlagged = gMessage.isFlagged;
|
|
flagButton.classList.toggle("flagged", isFlagged);
|
|
flagButton.setAttribute("aria-checked", isFlagged);
|
|
}
|
|
|
|
function EnsureSubjectValue() {
|
|
if (!("subject" in currentHeaderData)) {
|
|
const foo = {};
|
|
foo.headerValue = "";
|
|
foo.headerName = "subject";
|
|
currentHeaderData[foo.headerName] = foo;
|
|
}
|
|
}
|
|
|
|
function EnsureFromValue() {
|
|
if (!("from" in currentHeaderData)) {
|
|
const 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) {
|
|
const 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 (const name in aHeaderTable) {
|
|
const 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 (const name in aHeaderTable) {
|
|
const 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 (const name in aHeaderTable) {
|
|
const 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") {
|
|
const dateLabel = document.getElementById("dateLabel");
|
|
const 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 (const name in headerTable) {
|
|
const 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 (const index in headerTable) {
|
|
const 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();
|
|
const rowId = "expanded" + headerName + "Row";
|
|
const 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.createElement("div");
|
|
newRowNode.setAttribute("id", rowId);
|
|
newRowNode.classList.add("message-header-row");
|
|
newRowNode.hidden = true;
|
|
|
|
// Create and append the label which contains the header name.
|
|
const 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 (const name in aHeaderTable) {
|
|
const 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)
|
|
*/
|
|
function UpdateExpandedMessageHeaders() {
|
|
// Iterate over each header we received and see if we have a matching entry
|
|
// in each header view table...
|
|
|
|
// 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 (const headerName in currentHeaderData) {
|
|
let headerEntry = null;
|
|
|
|
if (headerName in gExpandedHeaderView) {
|
|
headerEntry = gExpandedHeaderView[headerName];
|
|
}
|
|
|
|
if (!headerEntry && gViewAllHeaders) {
|
|
const entry = gExpandedHeaderList.find(h => h.name == headerName);
|
|
// 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 (entry) {
|
|
gExpandedHeaderView[headerName] = new MsgHeaderEntry("expanded", entry);
|
|
} 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 (gViewAllHeaders) {
|
|
headerEntry.hidden = false;
|
|
}
|
|
|
|
if (
|
|
headerName == "references" &&
|
|
!(
|
|
gViewAllHeaders ||
|
|
Services.prefs.getBoolPref("mailnews.headers.showReferences") ||
|
|
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 if (!headerEntry.hidden) {
|
|
// Set the row element visible before populating the field.
|
|
headerEntry.enclosingRow.hidden = false;
|
|
const headerField = currentHeaderData[headerName];
|
|
headerEntry.outputFunction(headerEntry, headerField.headerValue);
|
|
headerEntry.valid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const otherHeaders = Services.prefs
|
|
.getCharPref("mail.compose.other.header", "")
|
|
.split(",")
|
|
.map(h => h.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const otherHeaderName of otherHeaders) {
|
|
const toLowerCaseHeaderName = otherHeaderName.toLowerCase();
|
|
const headerEntry = gExpandedHeaderView[toLowerCaseHeaderName];
|
|
const headerData = currentHeaderData[toLowerCaseHeaderName];
|
|
|
|
if (headerEntry && headerData) {
|
|
headerEntry.outputFunction(headerEntry, headerData.headerValue);
|
|
headerEntry.valid = true;
|
|
}
|
|
}
|
|
|
|
const 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;
|
|
const 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() {
|
|
const 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 multi-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 (const id of headerValue.match(/<[^>]*>/g)) {
|
|
headerEntry.enclosingBox.addId(id);
|
|
}
|
|
headerEntry.enclosingBox.buildView();
|
|
}
|
|
|
|
/**
|
|
* Take urls separated by comma, and add them to the corresponding
|
|
* multi-url-header-row element.
|
|
*
|
|
* @param {MsgHeaderEntry} headerEntry - The data structure for this header.
|
|
* @param {string} headerValue - The string of URLs from the message header.
|
|
*/
|
|
function outputMultiURL(headerEntry, headerValue) {
|
|
headerEntry.enclosingBox.clear();
|
|
for (const url of headerValue.split(",")) {
|
|
headerEntry.enclosingBox.addURL(url.trim());
|
|
}
|
|
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.
|
|
const 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(":")) {
|
|
const address = { displayName: emailAddresses };
|
|
headerEntry.enclosingBox.addRecipient(address);
|
|
}
|
|
|
|
for (const 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.
|
|
const 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() {
|
|
const expandBar = document.getElementById("context-expandAttachmentBar");
|
|
const 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() {
|
|
const attachmentList = document.getElementById("attachmentList");
|
|
const attachmentInfo = document.getElementById("attachmentInfo");
|
|
const attachmentName = document.getElementById("attachmentName");
|
|
const contextMenu = document.getElementById("attachmentItemContext");
|
|
const openMenu = document.getElementById("context-openAttachment");
|
|
const saveMenu = document.getElementById("context-saveAttachment");
|
|
const detachMenu = document.getElementById("context-detachAttachment");
|
|
const deleteMenu = document.getElementById("context-deleteAttachment");
|
|
const copyUrlMenuSep = document.getElementById(
|
|
"context-menu-copyurl-separator"
|
|
);
|
|
const copyUrlMenu = document.getElementById("context-copyAttachmentUrl");
|
|
const 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;
|
|
const allSelectedHttp = selectedAttachments.every(function (attachment) {
|
|
return attachment.isLinkAttachment;
|
|
});
|
|
const 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() {
|
|
const attachmentName = document.getElementById("attachmentName");
|
|
const 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() {
|
|
const openItem = document.getElementById("button-openAttachment");
|
|
const saveItem = document.getElementById("button-saveAttachment");
|
|
const detachItem = document.getElementById("button-detachAttachment");
|
|
const deleteItem = document.getElementById("button-deleteAttachment");
|
|
|
|
const detached = currentAttachments[0].isExternalAttachment;
|
|
const deleted = !currentAttachments[0].hasFile;
|
|
const 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() {
|
|
const openAllItem = document.getElementById("button-openAllAttachments");
|
|
const saveAllItem = document.getElementById("button-saveAllAttachments");
|
|
const detachAllItem = document.getElementById("button-detachAllAttachments");
|
|
const deleteAllItem = document.getElementById("button-deleteAllAttachments");
|
|
|
|
const allDetached = currentAttachments.every(function (attachment) {
|
|
return attachment.isExternalAttachment;
|
|
});
|
|
const allDeleted = currentAttachments.every(function (attachment) {
|
|
return !attachment.hasFile;
|
|
});
|
|
const 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() {
|
|
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() {},
|
|
};
|
|
|
|
var AttachmentMenuController = {
|
|
canDetachFiles() {
|
|
const 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 (const 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 (const 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);
|
|
}
|
|
|
|
const attachmentInfo = document.getElementById("attachmentInfo");
|
|
const attachmentCount = document.getElementById("attachmentCount");
|
|
const attachmentName = document.getElementById("attachmentName");
|
|
const attachmentSize = document.getElementById("attachmentSize");
|
|
|
|
if (numAttachments == 1) {
|
|
const count = bundle.getString("attachmentCountSingle");
|
|
const name = SanitizeAttachmentDisplayName(currentAttachments[0]);
|
|
|
|
attachmentInfo.setAttribute("contextmenu", "attachmentItemContext");
|
|
attachmentCount.setAttribute("value", count);
|
|
attachmentName.hidden = false;
|
|
attachmentName.setAttribute("value", name);
|
|
} else {
|
|
const words = bundle.getString("attachmentCount");
|
|
const 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() {
|
|
const bundleMessenger = document.getElementById("bundle_messenger");
|
|
const attachmentName = document.getElementById("attachmentName");
|
|
const attachmentList = document.getElementById("attachmentList");
|
|
|
|
// Attachment bar single.
|
|
const firstAttachment = attachmentList.firstElementChild.attachment;
|
|
const isExternalAttachment = firstAttachment.isExternalAttachment;
|
|
let displayUrl = isExternalAttachment ? firstAttachment.displayUrl : "";
|
|
const tooltiptext =
|
|
isExternalAttachment || firstAttachment.isDeleted
|
|
? ""
|
|
: attachmentName.getAttribute("tooltiptextopen");
|
|
const 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 (const attachmentitem of attachmentList.children) {
|
|
const 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() {
|
|
const saveAllSingle = document.getElementById("attachmentSaveAllSingle");
|
|
const saveAllMultiple = document.getElementById("attachmentSaveAllMultiple");
|
|
|
|
// If we can't find the buttons, they're not on the toolbar, so bail out!
|
|
if (!saveAllSingle || !saveAllMultiple) {
|
|
return;
|
|
}
|
|
|
|
const allDeleted = currentAttachments.every(function (attachment) {
|
|
return !attachment.hasFile;
|
|
});
|
|
const 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) {
|
|
const attachmentList = document.getElementById("attachmentList");
|
|
const attachmentIcon = document.getElementById("attachmentIcon");
|
|
const attachmentName = document.getElementById("attachmentName");
|
|
const attachmentSize = document.getElementById("attachmentSize");
|
|
const attachmentItem = attachmentList.findItemForAttachment(attachmentInfo);
|
|
const 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;
|
|
const tooltiptextExternalNotFound = attachmentName.getAttribute(
|
|
"tooltiptextexternalnotfound"
|
|
);
|
|
|
|
let sizeStr;
|
|
const 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() {
|
|
const bundle = document.getElementById("bundle_messenger");
|
|
let totalSize = 0;
|
|
let lastPartID;
|
|
let unknownSize = false;
|
|
for (const 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) {
|
|
const attachmentList = document.getElementById("attachmentList");
|
|
const selectedAttachments = [];
|
|
for (const 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 (const 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.
|
|
const 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.
|
|
const 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 (const 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 (const attachment of aAttachmentInfoArray) {
|
|
if (!attachment.hasFile || attachment.message != gMessage) {
|
|
continue;
|
|
}
|
|
|
|
const 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.
|
|
const 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.
|
|
const attachments = Array.from(
|
|
document.querySelectorAll("#attachmentList .attachmentItem[selected]"),
|
|
item => item.attachment
|
|
);
|
|
setupDataTransfer(event, attachments);
|
|
}
|
|
event.stopPropagation();
|
|
},
|
|
};
|
|
|
|
const attachmentNameDNDObserver = {
|
|
onDragStart(event) {
|
|
const attachmentList = document.getElementById("attachmentList");
|
|
setupDataTransfer(event, [attachmentList.getItemAtIndex(0).attachment]);
|
|
event.stopPropagation();
|
|
},
|
|
};
|
|
|
|
function onShowOtherActionsPopup() {
|
|
// Enable/disable the Open Conversation button.
|
|
const glodaEnabled = Services.prefs.getBoolPref(
|
|
"mailnews.database.global.indexer.enabled"
|
|
);
|
|
|
|
const openConversation = document.getElementById(
|
|
"otherActionsOpenConversation"
|
|
);
|
|
// Check because this menuitem element is not present in messageWindow.xhtml.
|
|
if (openConversation) {
|
|
openConversation.disabled = !(
|
|
glodaEnabled && Gloda.isMessageIndexed(gMessage)
|
|
);
|
|
}
|
|
|
|
const isDummyMessage = !gViewWrapper.isSynthetic && !gMessage.folder;
|
|
const tagsItem = document.getElementById("otherActionsTag");
|
|
const markAsReadItem = document.getElementById("markAsReadMenuItem");
|
|
const 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.
|
|
const isFeed = FeedUtils.isFeedMessage(gMessage);
|
|
document.getElementById("otherActionsMessageBodyAs").hidden = isFeed;
|
|
document.getElementById("otherActionsFeedBodyAs").hidden = !isFeed;
|
|
}
|
|
|
|
function InitOtherActionsViewBodyMenu(isFeed = false) {
|
|
const html_as = Services.prefs.getIntPref("mailnews.display.html_as");
|
|
const prefer_plaintext = Services.prefs.getBoolPref(
|
|
"mailnews.display.prefer_plaintext"
|
|
);
|
|
const disallow_classes = Services.prefs.getIntPref(
|
|
"mailnews.display.disallow_mime_handlers"
|
|
);
|
|
const kDefaultIDs = [
|
|
"otherActionsMenu_bodyAllowHTML",
|
|
"otherActionsMenu_bodySanitized",
|
|
"otherActionsMenu_bodyAsPlaintext",
|
|
"otherActionsMenu_bodyAllParts",
|
|
];
|
|
const kRssIDs = [
|
|
"otherActionsMenu_bodyFeedSummaryAllowHTML",
|
|
"otherActionsMenu_bodyFeedSummarySanitized",
|
|
"otherActionsMenu_bodyFeedSummaryAsPlaintext",
|
|
];
|
|
const 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)
|
|
|
|
const AllowHTML_menuitem = document.getElementById(menuIDs[0]);
|
|
const Sanitized_menuitem = document.getElementById(menuIDs[1]);
|
|
const AsPlaintext_menuitem = document.getElementById(menuIDs[2]);
|
|
const AllBodyParts_menuitem = menuIDs[3]
|
|
? document.getElementById(menuIDs[3])
|
|
: null;
|
|
|
|
document.getElementById("otherActionsMenu_bodyAllParts").hidden =
|
|
!Services.prefs.getBoolPref("mailnews.display.show_all_body_parts_menu");
|
|
|
|
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) {
|
|
const viewRssMenuItemIds = [
|
|
"otherActionsMenu_bodyFeedGlobalWebPage",
|
|
"otherActionsMenu_bodyFeedGlobalSummary",
|
|
"otherActionsMenu_bodyFeedPerFolderPref",
|
|
];
|
|
const checked = FeedMessageHandler.onSelectPref;
|
|
for (const [index, id] of viewRssMenuItemIds.entries()) {
|
|
document.getElementById(id).setAttribute("checked", index == checked);
|
|
}
|
|
// Unlike the global menu we use the variable here to possibly have the
|
|
// value relevant to the current mode if the per folder option is selected.
|
|
AllowHTML_menuitem.hidden = !FeedMessageHandler.gShowSummary;
|
|
Sanitized_menuitem.hidden = !FeedMessageHandler.gShowSummary;
|
|
AsPlaintext_menuitem.hidden = !FeedMessageHandler.gShowSummary;
|
|
document.getElementById(
|
|
"otherActionsMenu_viewFeedSummarySeparator"
|
|
).hidden = !FeedMessageHandler.gShowSummary;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
const 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;
|
|
|
|
const type = Ci.nsMimeHeaderDisplayTypes;
|
|
const 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) {
|
|
const 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() {
|
|
const allHeaderLabels = document.querySelectorAll(
|
|
".message-header-row:not([hidden]) .message-header-label"
|
|
);
|
|
|
|
// Clear existing style.
|
|
for (const label of allHeaderLabels) {
|
|
label.style.minWidth = null;
|
|
}
|
|
|
|
const minWidth = Math.max(
|
|
...Array.from(allHeaderLabels, i => i.clientWidth)
|
|
);
|
|
for (const label of allHeaderLabels) {
|
|
label.style.minWidth = `${minWidth}px`;
|
|
}
|
|
},
|
|
|
|
openCopyPopup(event, element) {
|
|
document.getElementById("copyCreateFilterFrom").disabled =
|
|
!gFolder?.server.canHaveFilters;
|
|
const popup = document.getElementById(
|
|
element.matches(`:scope[is="url-header-row"],a`)
|
|
? "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;
|
|
|
|
const discoverKeyMenuItem = document.getElementById("searchKeysOpenPGP");
|
|
if (discoverKeyMenuItem) {
|
|
const hidden = await PgpSqliteDb2.hasAnyPositivelyAcceptedKeyForEmail(
|
|
element.emailAddress
|
|
);
|
|
discoverKeyMenuItem.hidden = hidden;
|
|
discoverKeyMenuItem.nextElementSibling.hidden = hidden; // Hide separator.
|
|
}
|
|
|
|
document.getElementById("createFilterFrom").disabled =
|
|
!gFolder?.server.canHaveFilters;
|
|
|
|
const 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);
|
|
|
|
const subscribed = this.newsgroupServer
|
|
?.QueryInterface(Ci.nsINntpIncomingServer)
|
|
.containsNewsgroup(element.textContent);
|
|
document.getElementById("subscribeToNewsgroupItem").hidden = subscribed;
|
|
document.getElementById("subscribeToNewsgroupSeparator").hidden =
|
|
subscribed;
|
|
|
|
const 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);
|
|
},
|
|
|
|
/**
|
|
* Show context menu for given <div is="list-id-header-row">.
|
|
*
|
|
* @param {Element} element - The {ListIdHeaderRow} element this is for.
|
|
* @param {number} screenX - Where to show it, x.
|
|
* @param {number} screenY - Where to show it, y.
|
|
*/
|
|
async openListIdPopup(element, screenX, screenY) {
|
|
document
|
|
.getElementById("listIdPlaceHolder")
|
|
.setAttribute(
|
|
"label",
|
|
element.value.textContent.replace(/.*<([^>]+)>.*/, "$1")
|
|
);
|
|
|
|
const popup = document.getElementById("listIdPopup");
|
|
popup.headerField = element;
|
|
for (const menu of popup.children) {
|
|
if (!menu.dataset.headerName) {
|
|
continue;
|
|
}
|
|
menu.hidden = !(menu.dataset.headerName in currentHeaderData);
|
|
if (!menu.hidden) {
|
|
let value = currentHeaderData[menu.dataset.headerName].headerValue;
|
|
// Prefer mailto: if that's available.
|
|
value = value.replace(/.*(<mailto:[^>]+>).*/, "$1");
|
|
if (menu.dataset.headerName == "list-post") {
|
|
// See https://datatracker.ietf.org/doc/html/rfc2369#section-3.4
|
|
// The list many not allow posting, e.g. an announcments list.
|
|
menu.disabled = value.includes("NO");
|
|
}
|
|
menu.setAttribute(
|
|
"value",
|
|
encodeURI(
|
|
value.replace(/\s*<([^>]+)>.*/, "$1").replace(/[<>\s]/g, "")
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!screenX) {
|
|
popup.openPopup(element, "after_start", 0, 0, true);
|
|
return;
|
|
}
|
|
popup.openPopupAtScreen(screenX, 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;
|
|
|
|
// Show "Open Browser With Message-ID" only for nntp messages or mailing
|
|
// lists hosted by Google.
|
|
document.getElementById("messageIdContext-openBrowserWithMsgId").hidden =
|
|
!gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, false) &&
|
|
!currentHeaderData["list-archive"]?.headerValue.includes(
|
|
"<https://groups.google.com/"
|
|
);
|
|
|
|
const 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) {
|
|
const recipient = event.currentTarget.parentNode.headerField;
|
|
|
|
const fields = Cc[
|
|
"@mozilla.org/messengercompose/composefields;1"
|
|
].createInstance(Ci.nsIMsgCompFields);
|
|
|
|
if (recipient.classList.contains("header-newsgroup")) {
|
|
fields.newsgroups = recipient.textContent;
|
|
}
|
|
|
|
if (recipient.fullAddress) {
|
|
const addresses = MailServices.headerParser.makeFromDisplayAddress(
|
|
recipient.fullAddress
|
|
);
|
|
if (addresses.length) {
|
|
fields.to = MailServices.headerParser.makeMimeHeader([addresses[0]]);
|
|
}
|
|
}
|
|
|
|
const 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) {
|
|
const 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) {
|
|
const server = this.newsgroupServer;
|
|
if (!server) {
|
|
return;
|
|
}
|
|
|
|
const 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 {
|
|
const 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) {
|
|
const server = this.newsgroupServer;
|
|
if (server) {
|
|
const 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.
|
|
const 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) {
|
|
const 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.
|
|
const msgKeyArray = gMessage.getStringProperty("keywords").split(" ");
|
|
|
|
// Get the list of known tags.
|
|
const tagsArray = MailServices.tags.getAllTags().filter(t => t.tag);
|
|
const tagKeys = {};
|
|
for (const tagInfo of tagsArray) {
|
|
tagKeys[tagInfo.key] = true;
|
|
}
|
|
// Only use tags that match our saved tags.
|
|
const 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) {
|
|
const id = event.currentTarget.closest(".header-message-id").id;
|
|
if (event.button == 0) {
|
|
// Remove the < and > symbols.
|
|
MailUtils.openMessageForMessageId(
|
|
id.substring(1, id.length - 1),
|
|
gFolder?.server,
|
|
window
|
|
);
|
|
}
|
|
},
|
|
|
|
openMessage(event) {
|
|
const id = event.currentTarget.parentNode.headerField.id;
|
|
// Remove the < and > symbols.
|
|
MailUtils.openMessageForMessageId(
|
|
id.substring(1, id.length - 1),
|
|
gFolder?.server,
|
|
window
|
|
);
|
|
},
|
|
|
|
openBrowser(event) {
|
|
const id = event.currentTarget.parentNode.headerField.id;
|
|
// Remove the < and > symbols.
|
|
MailUtils.openBrowserWithMessageId(id.substring(1, id.length - 1));
|
|
},
|
|
|
|
copyMessageId(event) {
|
|
navigator.clipboard.writeText(
|
|
event.currentTarget.parentNode.headerField.id
|
|
);
|
|
},
|
|
|
|
copyWebsiteUrl(event) {
|
|
const element = event.currentTarget.parentNode.headerField;
|
|
navigator.clipboard.writeText(
|
|
element.matches("a") ? element.href : element.value.textContent
|
|
);
|
|
},
|
|
|
|
openListURL(event) {
|
|
const url = event.target.value;
|
|
if (url.startsWith("mailto:")) {
|
|
top.composeEmailTo(url, MailUtils.getIdentityForHeader(gMessage));
|
|
return;
|
|
}
|
|
openUILink(url, event);
|
|
},
|
|
};
|
|
window.addEventListener(
|
|
"openListId",
|
|
async event => {
|
|
await gMessageHeader.openListIdPopup(
|
|
event.target,
|
|
event.detail.screenX,
|
|
event.detail.screenY
|
|
);
|
|
},
|
|
true
|
|
);
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
const [myIdentity] = MailUtils.getIdentityForHeader(gMessage);
|
|
const myEmail = myIdentity ? myIdentity.email : null;
|
|
// We aren't guaranteed to have an email address, so guard against that.
|
|
const imInAddresses =
|
|
myEmail && addresses.toLowerCase().includes(myEmail.toLowerCase());
|
|
|
|
// Now, let's get the number of unique addresses.
|
|
const 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.
|
|
const 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";
|
|
}
|
|
|
|
const smartReplyButton = document.getElementById("hdrSmartReplyButton");
|
|
if (smartReplyButton) {
|
|
const replyButton = document.getElementById("hdrReplyButton");
|
|
const replyAllButton = document.getElementById("hdrReplyAllButton");
|
|
const replyListButton = document.getElementById("hdrReplyListButton");
|
|
const followupButton = document.getElementById("hdrFollowupButton");
|
|
|
|
replyButton.hidden = buttonToShow != "reply";
|
|
replyAllButton.hidden = buttonToShow != "replyAll";
|
|
replyListButton.hidden = buttonToShow != "replyList";
|
|
followupButton.hidden = buttonToShow != "followup";
|
|
}
|
|
|
|
const 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 (const id of [
|
|
"hdrReplyButton",
|
|
"hdrReplyAllButton",
|
|
"hdrReplyListButton",
|
|
"hdrFollowupButton",
|
|
"hdrForwardButton",
|
|
"hdrReplyToSenderButton",
|
|
]) {
|
|
document.getElementById(id).disabled = !hasIdentities;
|
|
}
|
|
}
|
|
|
|
function SelectedMessagesAreJunk() {
|
|
try {
|
|
const 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() {
|
|
const isDummyMessage = !gViewWrapper.isSynthetic && !gMessage.folder;
|
|
const archiveButton = document.getElementById("hdrArchiveButton");
|
|
const junkButton = document.getElementById("hdrJunkButton");
|
|
const trashButton = document.getElementById("hdrTrashButton");
|
|
|
|
if (isDummyMessage) {
|
|
archiveButton.disabled = true;
|
|
junkButton.disabled = true;
|
|
trashButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
archiveButton.disabled = !MessageArchiver.canArchive([gMessage]);
|
|
const 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;
|
|
}
|
|
|
|
const 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) {
|
|
const junkScore = msgHdr.getStringProperty("junkscore");
|
|
const 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) {
|
|
const junkScore = aMsgHdr ? aMsgHdr.getStringProperty("junkscore") : "";
|
|
const 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;
|
|
},
|
|
|
|
async setJunkMsg(aMsgHdr) {
|
|
goUpdateCommand("cmd_junk");
|
|
|
|
// Avoid duplication by avoiding any in-progress calls to `setJunkMsg`.
|
|
await this._junkNotificationPromise;
|
|
const junkBarStatus = this.checkJunkMsgStatus(aMsgHdr);
|
|
if (junkBarStatus == -1) {
|
|
this.msgNotificationBar.removeNotification(
|
|
this.msgNotificationBar.getNotificationWithValue("junkContent"),
|
|
true
|
|
);
|
|
} else if (junkBarStatus == 1) {
|
|
const brandName = this.brandBundle.getString("brandShortName");
|
|
const junkBarMsg = this.stringBundle.getFormattedString(
|
|
"junkBarMessage",
|
|
[brandName]
|
|
);
|
|
|
|
const buttons = [
|
|
{
|
|
label: this.stringBundle.getString("junkBarInfoButton"),
|
|
accessKey: this.stringBundle.getString("junkBarInfoButtonKey"),
|
|
popup: null,
|
|
callback() {
|
|
// 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() {
|
|
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._junkNotificationPromise = this.msgNotificationBar
|
|
.appendNotification(
|
|
"junkContent",
|
|
{
|
|
label: junkBarMsg,
|
|
image: "chrome://messenger/skin/icons/junk.svg",
|
|
priority: this.msgNotificationBar.PRIORITY_WARNING_HIGH,
|
|
},
|
|
buttons
|
|
)
|
|
.catch(console.warn);
|
|
await this._junkNotificationPromise;
|
|
delete this._junkNotificationPromise;
|
|
}
|
|
},
|
|
|
|
isShowingJunkNotification() {
|
|
return !!this.msgNotificationBar.getNotificationWithValue("junkContent");
|
|
},
|
|
|
|
async setRemoteContentMsg(aMsgHdr, aContentURI, aCanOverride) {
|
|
// update the allow remote content for sender string
|
|
const brandName = this.brandBundle.getString("brandShortName");
|
|
const remoteContentMsg = this.stringBundle.getFormattedString(
|
|
"remoteContentBarMessage",
|
|
[brandName]
|
|
);
|
|
|
|
const buttonLabel = this.stringBundle.getString(
|
|
AppConstants.platform == "win"
|
|
? "remoteContentPrefLabel"
|
|
: "remoteContentPrefLabelUnix"
|
|
);
|
|
const buttonAccesskey = this.stringBundle.getString(
|
|
AppConstants.platform == "win"
|
|
? "remoteContentPrefAccesskey"
|
|
: "remoteContentPrefAccesskeyUnix"
|
|
);
|
|
|
|
const buttons = [
|
|
{
|
|
label: buttonLabel,
|
|
accessKey: buttonAccesskey,
|
|
popup: "remoteContentOptions",
|
|
callback() {},
|
|
},
|
|
];
|
|
|
|
// The popup value is a space separated list of all the blocked origins.
|
|
const popup = document.getElementById("remoteContentOptions");
|
|
const principal = Services.scriptSecurityManager.createContentPrincipal(
|
|
aContentURI,
|
|
{}
|
|
);
|
|
const origins = popup.value ? popup.value.split(" ") : [];
|
|
if (!origins.includes(principal.origin)) {
|
|
origins.push(principal.origin);
|
|
}
|
|
popup.value = origins.join(" ");
|
|
|
|
// Avoid duplication by avoiding any in-progress calls to `setRemoteContentMsg`.
|
|
await this._remoteContentNotificationPromise;
|
|
if (!this.isShowingRemoteContentNotification()) {
|
|
this._remoteContentNotificationPromise = this.msgNotificationBar
|
|
.appendNotification(
|
|
"remoteContent",
|
|
{
|
|
label: remoteContentMsg,
|
|
image: "chrome://messenger/skin/icons/remote-blocked.svg",
|
|
priority: this.msgNotificationBar.PRIORITY_WARNING_MEDIUM,
|
|
},
|
|
aCanOverride ? buttons : []
|
|
)
|
|
.then(notification => {
|
|
notification.buttonContainer.firstElementChild.classList.add(
|
|
"button-menu-list"
|
|
);
|
|
}, console.warn);
|
|
await this._remoteContentNotificationPromise;
|
|
delete this._remoteContentNotificationPromise;
|
|
}
|
|
},
|
|
|
|
isShowingRemoteContentNotification() {
|
|
return !!this.msgNotificationBar.getNotificationWithValue("remoteContent");
|
|
},
|
|
|
|
async setPhishingMsg() {
|
|
const phishingMsgNote = this.stringBundle.getString("phishingBarMessage");
|
|
|
|
const buttonLabel = this.stringBundle.getString(
|
|
AppConstants.platform == "win"
|
|
? "phishingBarPrefLabel"
|
|
: "phishingBarPrefLabelUnix"
|
|
);
|
|
const buttonAccesskey = this.stringBundle.getString(
|
|
AppConstants.platform == "win"
|
|
? "phishingBarPrefAccesskey"
|
|
: "phishingBarPrefAccesskeyUnix"
|
|
);
|
|
|
|
const buttons = [
|
|
{
|
|
label: buttonLabel,
|
|
accessKey: buttonAccesskey,
|
|
popup: "phishingOptions",
|
|
callback() {},
|
|
},
|
|
];
|
|
|
|
// Avoid duplication by avoiding any in-progress calls to `setPhishingMsg`.
|
|
await this._phishingNotificationPromise;
|
|
if (!this.isShowingPhishingNotification()) {
|
|
this._phishingNotificationPromise = this.msgNotificationBar
|
|
.appendNotification(
|
|
"maybeScam",
|
|
{
|
|
label: phishingMsgNote,
|
|
image: "chrome://messenger/skin/icons/phishing.svg",
|
|
priority: this.msgNotificationBar.PRIORITY_CRITICAL_MEDIUM,
|
|
},
|
|
buttons
|
|
)
|
|
.then(notification => {
|
|
notification.buttonContainer.firstElementChild.classList.add(
|
|
"button-menu-list"
|
|
);
|
|
}, console.warn);
|
|
await this._phishingNotificationPromise;
|
|
delete this._phishingNotificationPromise;
|
|
}
|
|
},
|
|
|
|
isShowingPhishingNotification() {
|
|
return !!this.msgNotificationBar.getNotificationWithValue("maybeScam");
|
|
},
|
|
|
|
async setMDNMsg(aMdnGenerator, aMsgHeader, aMimeHdr) {
|
|
this.mdnGenerator = aMdnGenerator;
|
|
// Return receipts can be RFC 3798 or not.
|
|
const mdnHdr =
|
|
aMimeHdr.extractHeader("Disposition-Notification-To", false) ||
|
|
aMimeHdr.extractHeader("Return-Receipt-To", false); // not
|
|
const fromHdr = aMimeHdr.extractHeader("From", false);
|
|
|
|
const mdnAddr =
|
|
MailServices.headerParser.extractHeaderAddressMailboxes(mdnHdr);
|
|
const fromAddr =
|
|
MailServices.headerParser.extractHeaderAddressMailboxes(fromHdr);
|
|
|
|
const authorName =
|
|
MailServices.headerParser.extractFirstName(
|
|
aMsgHeader.mime2DecodedAuthor
|
|
) || aMsgHeader.author;
|
|
|
|
// If the return receipt doesn't go to the sender address, note that in the
|
|
// notification.
|
|
const mdnBarMsg =
|
|
mdnAddr != fromAddr
|
|
? this.stringBundle.getFormattedString("mdnBarMessageAddressDiffers", [
|
|
authorName,
|
|
mdnAddr,
|
|
])
|
|
: this.stringBundle.getFormattedString("mdnBarMessageNormal", [
|
|
authorName,
|
|
]);
|
|
|
|
const buttons = [
|
|
{
|
|
label: this.stringBundle.getString("mdnBarSendReqButton"),
|
|
accessKey: this.stringBundle.getString("mdnBarSendReqButtonKey"),
|
|
popup: null,
|
|
callback() {
|
|
SendMDNResponse();
|
|
return false; // close notification
|
|
},
|
|
},
|
|
{
|
|
label: this.stringBundle.getString("mdnBarIgnoreButton"),
|
|
accessKey: this.stringBundle.getString("mdnBarIgnoreButtonKey"),
|
|
popup: null,
|
|
callback() {
|
|
IgnoreMDNResponse();
|
|
return false; // close notification
|
|
},
|
|
},
|
|
];
|
|
|
|
await this.msgNotificationBar.appendNotification(
|
|
"mdnRequested",
|
|
{
|
|
label: mdnBarMsg,
|
|
priority: this.msgNotificationBar.PRIORITY_INFO_MEDIUM,
|
|
},
|
|
buttons
|
|
);
|
|
},
|
|
|
|
async setDraftEditMessage() {
|
|
if (!gMessage || !gFolder) {
|
|
return;
|
|
}
|
|
|
|
if (gFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Drafts, true)) {
|
|
const draftMsgNote = this.stringBundle.getString("draftMessageMsg");
|
|
|
|
const buttons = [
|
|
{
|
|
label: this.stringBundle.getString("draftMessageButton"),
|
|
accessKey: this.stringBundle.getString("draftMessageButtonKey"),
|
|
popup: null,
|
|
callback() {
|
|
MsgComposeDraftMessage();
|
|
return true; // keep notification open
|
|
},
|
|
},
|
|
];
|
|
|
|
await 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) {
|
|
const 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.
|
|
const adrCount = addresses.length;
|
|
if (adrCount > 0) {
|
|
const authorEmailAddress = addresses[0].email;
|
|
const authorEmailAddressURI = Services.io.newURI(
|
|
"chrome://messenger/content/email=" + authorEmailAddress
|
|
);
|
|
const mailPrincipal = Services.scriptSecurityManager.createContentPrincipal(
|
|
authorEmailAddressURI,
|
|
{}
|
|
);
|
|
origins.push(mailPrincipal.origin);
|
|
}
|
|
|
|
const messengerBundle = document.getElementById("bundle_messenger");
|
|
|
|
// Out with the old...
|
|
const children = aEvent.target.children;
|
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
if (children[i].getAttribute("class") == "allow-remote-uri") {
|
|
children[i].remove();
|
|
}
|
|
}
|
|
|
|
const urlSepar = document.getElementById("remoteContentAllMenuSeparator");
|
|
|
|
// ... and in with the new.
|
|
for (const origin of origins) {
|
|
const 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);
|
|
}
|
|
}
|
|
|
|
const URLcount = origins.length - adrCount;
|
|
const allowAllItem = document.getElementById("remoteContentOptionAllowAll");
|
|
const 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) {
|
|
const 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) {
|
|
const uriNodes = aListNode.querySelectorAll(".allow-remote-uri");
|
|
for (const 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.
|
|
const findBar = document.getElementById("FindToolbar");
|
|
if (!findBar.hidden) {
|
|
findBar.onFindAgainCommand(false);
|
|
}
|
|
const 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);
|
|
|
|
const 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.
|
|
const links = doc && doc.links ? doc.links : [];
|
|
for (const 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.
|
|
const anchorId = linkNode.hash.replace("#", "");
|
|
// Continue if an id (html5) or name attribute value for the ref is not
|
|
// found in this document.
|
|
const 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.
|
|
const messageURI = makeURI(linkNode.ownerDocument.URL);
|
|
if (
|
|
messageURI instanceof Ci.nsIMsgMailNewsUrl &&
|
|
linkNode.href.startsWith("http")
|
|
) {
|
|
linkNode.href = messageURI.specIgnoringRef + linkNode.hash;
|
|
}
|
|
}
|
|
|
|
const stylesReadyPromise = new Promise(resolve => {
|
|
if (doc.readyState === "complete") {
|
|
resolve();
|
|
return;
|
|
}
|
|
browser.contentWindow.addEventListener("load", resolve, {
|
|
once: true,
|
|
});
|
|
});
|
|
|
|
const applyOverflowingToImg = async img => {
|
|
img.setAttribute("shrinktofit", "true");
|
|
if (!img.complete) {
|
|
await new Promise(resolve => {
|
|
img.addEventListener("load", resolve, { once: true });
|
|
});
|
|
}
|
|
await stylesReadyPromise;
|
|
if (img.naturalWidth > img.clientWidth) {
|
|
img.setAttribute("overflowing", "true");
|
|
}
|
|
};
|
|
|
|
// Scale any overflowing images, exclude http content.
|
|
const imgs = doc && !doc.URL.startsWith("http") ? doc.images : [];
|
|
for (const img of imgs) {
|
|
applyOverflowingToImg(img);
|
|
}
|
|
}
|
|
|
|
function OnMsgLoaded(aUrl) {
|
|
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;
|
|
}
|
|
|
|
const browser = getMessagePaneBrowser();
|
|
if (!browser.docShellIsActive) {
|
|
// We're in an inactive docShell (probably a background tab). Wait until
|
|
// it becomes active before marking the message as read.
|
|
browser.addEventListener("visibilitychange", () => autoMarkAsRead(), {
|
|
once: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const 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) {
|
|
const markReadOnADelay = Services.prefs.getBoolPref(
|
|
"mailnews.mark_message_read.delay"
|
|
);
|
|
|
|
const 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();
|
|
const 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.
|
|
* For pop the msg uid can be 0 (i.e., 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.
|
|
*
|
|
* @param {nsIMsgMailNewsUrl} url
|
|
*/
|
|
function HandleMDNResponse(url) {
|
|
const msgFolder = url.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 = url.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.
|
|
const msgId = gMessage.messageId;
|
|
if (msgId.startsWith("md5:") || msgId.startsWith("x-moz-uuid:")) {
|
|
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;
|
|
const 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) {
|
|
// The key is one of:
|
|
// - 'signed-smime'
|
|
// - 'signed-openpgp'
|
|
// - 'encrypted-smime'
|
|
// - 'encrypted-openpgp'
|
|
const is_signed = gSecureMsgProbe.key.startsWith("signed-");
|
|
const is_encrypted = gSecureMsgProbe.key.startsWith("encrypted-");
|
|
const security = gSecureMsgProbe.key.endsWith("-openpgp")
|
|
? "OpenPGP"
|
|
: "S/MIME";
|
|
Glean.mail.mailsReadSecure.record({ security, is_signed, is_encrypted });
|
|
}
|
|
}
|
|
|
|
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 (const 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];
|
|
const 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, {
|
|
position: "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.
|
|
const newFocusableButton = headerButtons[elementIndex];
|
|
if (newFocusableButton) {
|
|
focusableButton.tabIndex = -1;
|
|
newFocusableButton.setAttribute("tabindex", "0");
|
|
newFocusableButton.focus();
|
|
}
|
|
},
|
|
};
|