summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/mailCommon.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/content/mailCommon.js')
-rw-r--r--comm/mail/base/content/mailCommon.js1126
1 files changed, 1126 insertions, 0 deletions
diff --git a/comm/mail/base/content/mailCommon.js b/comm/mail/base/content/mailCommon.js
new file mode 100644
index 0000000000..b74a3d8fba
--- /dev/null
+++ b/comm/mail/base/content/mailCommon.js
@@ -0,0 +1,1126 @@
+/* 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/. */
+
+// mailContext.js
+/* globals mailContextMenu */
+
+// msgViewNavigation.js
+/* globals CrossFolderNavigation */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ TreeSelection: "chrome://messenger/content/tree-selection.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ConversationOpener: "resource:///modules/ConversationOpener.jsm",
+ DBViewWrapper: "resource:///modules/DBViewWrapper.jsm",
+ EnigmailPersistentCrypto:
+ "chrome://openpgp/content/modules/persistentCrypto.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ MessageArchiver: "resource:///modules/MessageArchiver.jsm",
+ VirtualFolderHelper: "resource:///modules/VirtualFolderWrapper.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gEncryptedURIService",
+ "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1",
+ "nsIEncryptedSMIMEURIsService"
+);
+
+const nsMsgViewIndex_None = 0xffffffff;
+const nsMsgKey_None = 0xffffffff;
+
+var gDBView, gFolder, gViewWrapper;
+
+var commandController = {
+ _composeCommands: {
+ cmd_editDraftMsg: Ci.nsIMsgCompType.Draft,
+ cmd_newMsgFromTemplate: Ci.nsIMsgCompType.Template,
+ cmd_editTemplateMsg: Ci.nsIMsgCompType.EditTemplate,
+ cmd_newMessage: Ci.nsIMsgCompType.New,
+ cmd_replyGroup: Ci.nsIMsgCompType.ReplyToGroup,
+ cmd_replySender: Ci.nsIMsgCompType.ReplyToSender,
+ cmd_replyall: Ci.nsIMsgCompType.ReplyAll,
+ cmd_replylist: Ci.nsIMsgCompType.ReplyToList,
+ cmd_forwardInline: Ci.nsIMsgCompType.ForwardInline,
+ cmd_forwardAttachment: Ci.nsIMsgCompType.ForwardAsAttachment,
+ cmd_redirect: Ci.nsIMsgCompType.Redirect,
+ cmd_editAsNew: Ci.nsIMsgCompType.EditAsNew,
+ },
+ _navigationCommands: {
+ cmd_goForward: Ci.nsMsgNavigationType.forward,
+ cmd_goBack: Ci.nsMsgNavigationType.back,
+ cmd_nextUnreadMsg: Ci.nsMsgNavigationType.nextUnreadMessage,
+ cmd_nextUnreadThread: Ci.nsMsgNavigationType.nextUnreadThread,
+ cmd_nextMsg: Ci.nsMsgNavigationType.nextMessage,
+ cmd_nextFlaggedMsg: Ci.nsMsgNavigationType.nextFlagged,
+ cmd_previousMsg: Ci.nsMsgNavigationType.previousMessage,
+ cmd_previousUnreadMsg: Ci.nsMsgNavigationType.previousUnreadMessage,
+ cmd_previousFlaggedMsg: Ci.nsMsgNavigationType.previousFlagged,
+ },
+ _viewCommands: {
+ cmd_toggleRead: Ci.nsMsgViewCommandType.toggleMessageRead,
+ cmd_markAsRead: Ci.nsMsgViewCommandType.markMessagesRead,
+ cmd_markAsUnread: Ci.nsMsgViewCommandType.markMessagesUnread,
+ cmd_markThreadAsRead: Ci.nsMsgViewCommandType.markThreadRead,
+ cmd_markAsNotJunk: Ci.nsMsgViewCommandType.unjunk,
+ cmd_watchThread: Ci.nsMsgViewCommandType.toggleThreadWatched,
+ },
+ _callbackCommands: {
+ cmd_cancel() {
+ gFolder
+ .QueryInterface(Ci.nsIMsgNewsFolder)
+ .cancelMessage(gDBView.hdrForFirstSelectedMessage, top.msgWindow);
+ },
+ cmd_openConversation() {
+ new ConversationOpener(window).openConversationForMessages(
+ gDBView.getSelectedMsgHdrs()
+ );
+ },
+ cmd_reply(event) {
+ if (gFolder?.flags & Ci.nsMsgFolderFlags.Newsgroup) {
+ commandController.doCommand("cmd_replyGroup", event);
+ } else {
+ commandController.doCommand("cmd_replySender", event);
+ }
+ },
+ cmd_forward(event) {
+ if (Services.prefs.getIntPref("mail.forward_message_mode", 0) == 0) {
+ commandController.doCommand("cmd_forwardAttachment", event);
+ } else {
+ commandController.doCommand("cmd_forwardInline", event);
+ }
+ },
+ cmd_openMessage(event) {
+ MailUtils.displayMessages(
+ gDBView.getSelectedMsgHdrs(),
+ gViewWrapper,
+ top.document.getElementById("tabmail"),
+ event?.type == "auxclick" && !event?.shiftKey
+ );
+ },
+ cmd_tag() {
+ // Does nothing, just here to enable/disable the tags sub-menu.
+ },
+ cmd_tag1: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 1),
+ cmd_tag2: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 2),
+ cmd_tag3: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 3),
+ cmd_tag4: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 4),
+ cmd_tag5: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 5),
+ cmd_tag6: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 6),
+ cmd_tag7: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 7),
+ cmd_tag8: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 8),
+ cmd_tag9: mailContextMenu._toggleMessageTagKey.bind(mailContextMenu, 9),
+ cmd_addTag() {
+ mailContextMenu.addTag();
+ },
+ cmd_manageTags() {
+ window.browsingContext.topChromeWindow.openOptionsDialog(
+ "paneGeneral",
+ "tagsCategory"
+ );
+ },
+ cmd_removeTags() {
+ mailContextMenu.removeAllMessageTags();
+ },
+ cmd_toggleTag(event) {
+ mailContextMenu._toggleMessageTag(
+ event.target.value,
+ event.target.getAttribute("checked") == "true"
+ );
+ },
+ cmd_markReadByDate() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/markByDate.xhtml",
+ "",
+ "chrome,modal,titlebar,centerscreen",
+ gFolder
+ );
+ },
+ cmd_markAsFlagged() {
+ gViewWrapper.dbView.doCommand(
+ gDBView.hdrForFirstSelectedMessage.isFlagged
+ ? Ci.nsMsgViewCommandType.unflagMessages
+ : Ci.nsMsgViewCommandType.flagMessages
+ );
+ },
+ cmd_markAsJunk() {
+ if (
+ Services.prefs.getBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead")
+ ) {
+ gViewWrapper.dbView.doCommand(Ci.nsMsgViewCommandType.markMessagesRead);
+ }
+ gViewWrapper.dbView.doCommand(Ci.nsMsgViewCommandType.junk);
+ },
+ cmd_markAllRead() {
+ if (gFolder.flags & Ci.nsMsgFolderFlags.Virtual) {
+ top.MsgMarkAllRead(
+ VirtualFolderHelper.wrapVirtualFolder(gFolder).searchFolders
+ );
+ } else {
+ top.MsgMarkAllRead([gFolder]);
+ }
+ },
+ /**
+ * Moves the selected messages to the destination folder.
+ *
+ * @param {nsIMsgFolder} destFolder - the destination folder
+ */
+ cmd_moveMessage(destFolder) {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand("cmd_moveMessage", destFolder);
+ return;
+ }
+ dbViewWrapperListener.threadPaneCommandUpdater.updateNextMessageAfterDelete();
+ gViewWrapper.dbView.doCommandWithFolder(
+ Ci.nsMsgViewCommandType.moveMessages,
+ destFolder
+ );
+ Services.prefs.setStringPref(
+ "mail.last_msg_movecopy_target_uri",
+ destFolder.URI
+ );
+ Services.prefs.setBoolPref("mail.last_msg_movecopy_was_move", true);
+ },
+ async cmd_copyDecryptedTo(destFolder) {
+ let msgHdrs = gDBView.getSelectedMsgHdrs();
+ if (!msgHdrs || msgHdrs.length === 0) {
+ return;
+ }
+
+ let total = msgHdrs.length;
+ let failures = 0;
+ for (let msgHdr of msgHdrs) {
+ await EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ destFolder.URI,
+ false, // not moving
+ false
+ ).catch(err => {
+ failures++;
+ });
+ }
+
+ if (failures) {
+ let info = await document.l10n.formatValue(
+ "decrypt-and-copy-failures-multiple",
+ {
+ failures,
+ total,
+ }
+ );
+ Services.prompt.alert(null, document.title, info);
+ }
+ },
+ /**
+ * Copies the selected messages to the destination folder.
+ *
+ * @param {nsIMsgFolder} destFolder - the destination folder
+ */
+ cmd_copyMessage(destFolder) {
+ if (window.gMessageURI?.startsWith("file:")) {
+ let file = Services.io
+ .newURI(window.gMessageURI)
+ .QueryInterface(Ci.nsIFileURL).file;
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ false,
+ Ci.nsMsgMessageFlags.Read,
+ "",
+ null,
+ top.msgWindow
+ );
+ } else {
+ gViewWrapper.dbView.doCommandWithFolder(
+ Ci.nsMsgViewCommandType.copyMessages,
+ destFolder
+ );
+ }
+ Services.prefs.setStringPref(
+ "mail.last_msg_movecopy_target_uri",
+ destFolder.URI
+ );
+ Services.prefs.setBoolPref("mail.last_msg_movecopy_was_move", false);
+ },
+ cmd_archive() {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand("cmd_archive");
+ return;
+ }
+ dbViewWrapperListener.threadPaneCommandUpdater.updateNextMessageAfterDelete();
+ let archiver = new MessageArchiver();
+ // The instance of nsITransactionManager to use here is tied to msgWindow. Set
+ // this property so the operation can be undone if requested.
+ archiver.msgWindow = top.msgWindow;
+ // Archive the selected message(s).
+ archiver.archiveMessages(gViewWrapper.dbView.getSelectedMsgHdrs());
+ },
+ cmd_moveToFolderAgain() {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand("cmd_moveToFolderAgain");
+ return;
+ }
+ let folder = MailUtils.getOrCreateFolder(
+ Services.prefs.getStringPref("mail.last_msg_movecopy_target_uri")
+ );
+ if (Services.prefs.getBoolPref("mail.last_msg_movecopy_was_move")) {
+ dbViewWrapperListener.threadPaneCommandUpdater.updateNextMessageAfterDelete();
+ commandController.doCommand("cmd_moveMessage", folder);
+ } else {
+ commandController.doCommand("cmd_copyMessage", folder);
+ }
+ },
+ cmd_deleteMessage() {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand("cmd_deleteMessage");
+ return;
+ }
+ dbViewWrapperListener.threadPaneCommandUpdater.updateNextMessageAfterDelete();
+ gViewWrapper.dbView.doCommand(Ci.nsMsgViewCommandType.deleteMsg);
+ },
+ cmd_shiftDeleteMessage() {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand("cmd_shiftDeleteMessage");
+ return;
+ }
+ dbViewWrapperListener.threadPaneCommandUpdater.updateNextMessageAfterDelete();
+ gViewWrapper.dbView.doCommand(Ci.nsMsgViewCommandType.deleteNoTrash);
+ },
+ cmd_createFilterFromMenu() {
+ let msgHdr = gDBView.hdrForFirstSelectedMessage;
+ let emailAddress =
+ MailServices.headerParser.extractHeaderAddressMailboxes(msgHdr.author);
+ if (emailAddress) {
+ top.MsgFilters(emailAddress, msgHdr.folder);
+ }
+ },
+ cmd_viewPageSource() {
+ let uris = window.gMessageURI
+ ? [window.gMessageURI]
+ : gDBView.getURIsForSelection();
+ for (let uri of uris) {
+ // Now, we need to get a URL from a URI
+ let url = MailServices.mailSession.ConvertMsgURIToMsgURL(
+ uri,
+ top.msgWindow
+ );
+
+ // Strip out the message-display parameter to ensure that attached emails
+ // display the message source, not the processed HTML.
+ url = url.replace(/type=application\/x-message-display&/, "");
+ window.openDialog(
+ "chrome://messenger/content/viewSource.xhtml",
+ "_blank",
+ "all,dialog=no",
+ { URL: url }
+ );
+ }
+ },
+ cmd_saveAsFile() {
+ let uris = window.gMessageURI
+ ? [window.gMessageURI]
+ : gDBView.getURIsForSelection();
+ top.SaveAsFile(uris);
+ },
+ cmd_saveAsTemplate() {
+ top.SaveAsTemplate(gDBView.getURIsForSelection()[0]);
+ },
+ cmd_applyFilters() {
+ let curFilterList = gFolder.getFilterList(top.msgWindow);
+ // Create a new filter list and copy over the enabled filters to it.
+ // We do this instead of having the filter after the fact code ignore
+ // disabled filters because the Filter Dialog filter after the fact
+ // code would have to clone filters to allow disabled filters to run,
+ // and we don't support cloning filters currently.
+ let tempFilterList = MailServices.filters.getTempFilterList(gFolder);
+ let numFilters = curFilterList.filterCount;
+ // Make sure the temp filter list uses the same log stream.
+ tempFilterList.loggingEnabled = curFilterList.loggingEnabled;
+ tempFilterList.logStream = curFilterList.logStream;
+ let newFilterIndex = 0;
+ for (let i = 0; i < numFilters; i++) {
+ let curFilter = curFilterList.getFilterAt(i);
+ // Only add enabled, UI visible filters that are in the manual context.
+ if (
+ curFilter.enabled &&
+ !curFilter.temporary &&
+ curFilter.filterType & Ci.nsMsgFilterType.Manual
+ ) {
+ tempFilterList.insertFilterAt(newFilterIndex, curFilter);
+ newFilterIndex++;
+ }
+ }
+ MailServices.filters.applyFiltersToFolders(
+ tempFilterList,
+ [gFolder],
+ top.msgWindow
+ );
+ },
+ cmd_applyFiltersToSelection() {
+ let selectedMessages = gDBView.getSelectedMsgHdrs();
+ if (selectedMessages.length) {
+ MailServices.filters.applyFilters(
+ Ci.nsMsgFilterType.Manual,
+ selectedMessages,
+ gFolder,
+ top.msgWindow
+ );
+ }
+ },
+ cmd_space(event) {
+ let messagePaneBrowser;
+ if (window.messageBrowser) {
+ messagePaneBrowser =
+ window.messageBrowser.contentWindow.getMessagePaneBrowser();
+ } else {
+ messagePaneBrowser = window.getMessagePaneBrowser();
+ }
+ let contentWindow = messagePaneBrowser.contentWindow;
+
+ if (event?.shiftKey) {
+ // If at the start of the message, go to the previous one.
+ if (contentWindow?.scrollY > 0) {
+ contentWindow.scrollByPages(-1);
+ } else if (Services.prefs.getBoolPref("mail.advance_on_spacebar")) {
+ top.goDoCommand("cmd_previousUnreadMsg");
+ }
+ } else if (
+ Math.ceil(contentWindow?.scrollY) < contentWindow?.scrollMaxY
+ ) {
+ // If at the end of the message, go to the next one.
+ contentWindow.scrollByPages(1);
+ } else if (Services.prefs.getBoolPref("mail.advance_on_spacebar")) {
+ top.goDoCommand("cmd_nextUnreadMsg");
+ }
+ },
+ cmd_searchMessages(folder = gFolder) {
+ // We always open a new search dialog for each search command.
+ top.openDialog(
+ "chrome://messenger/content/SearchDialog.xhtml",
+ "_blank",
+ "chrome,resizable,status,centerscreen,dialog=no",
+ { folder }
+ );
+ },
+ },
+ _isCallbackEnabled: {},
+
+ registerCallback(name, callback, isEnabled = true) {
+ this._callbackCommands[name] = callback;
+ this._isCallbackEnabled[name] = isEnabled;
+ },
+
+ supportsCommand(command) {
+ return (
+ command in this._composeCommands ||
+ command in this._navigationCommands ||
+ command in this._viewCommands ||
+ command in this._callbackCommands
+ );
+ },
+ // eslint-disable-next-line complexity
+ isCommandEnabled(command) {
+ let type = typeof this._isCallbackEnabled[command];
+ if (type == "function") {
+ return this._isCallbackEnabled[command]();
+ } else if (type == "boolean") {
+ return this._isCallbackEnabled[command];
+ }
+
+ const hasIdentities = MailServices.accounts.allIdentities.length;
+ switch (command) {
+ case "cmd_newMessage":
+ return hasIdentities;
+ case "cmd_searchMessages":
+ // TODO: This shouldn't be here, or should return false if there are no accounts.
+ return true;
+ case "cmd_space":
+ case "cmd_manageTags":
+ return true;
+ }
+
+ if (!gViewWrapper?.dbView) {
+ return false;
+ }
+
+ let isDummyMessage = !gViewWrapper.isSynthetic && !gFolder;
+
+ if (["cmd_goBack", "cmd_goForward"].includes(command)) {
+ let activeMessageHistory = (
+ window.messageBrowser?.contentWindow ?? window
+ ).messageHistory;
+ let relPos = command === "cmd_goBack" ? -1 : 1;
+ if (relPos === -1 && activeMessageHistory.canPop(0)) {
+ return !isDummyMessage;
+ }
+ return !isDummyMessage && activeMessageHistory.canPop(relPos);
+ }
+
+ if (command in this._navigationCommands) {
+ return !isDummyMessage;
+ }
+
+ let numSelectedMessages = isDummyMessage ? 1 : gDBView.numSelected;
+
+ // Evaluate these properties only if needed, not once for each command.
+ let folder = () => {
+ if (gFolder) {
+ return gFolder;
+ }
+ if (gDBView.numSelected >= 1) {
+ return gDBView.hdrForFirstSelectedMessage?.folder;
+ }
+ return null;
+ };
+ let isNewsgroup = () =>
+ folder()?.isSpecialFolder(Ci.nsMsgFolderFlags.Newsgroup, true);
+ let canMove = () =>
+ numSelectedMessages >= 1 &&
+ (folder()?.canDeleteMessages || gViewWrapper.isSynthetic);
+
+ switch (command) {
+ case "cmd_cancel":
+ if (numSelectedMessages == 1 && isNewsgroup()) {
+ // Ensure author of message matches own identity
+ let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor;
+ return MailServices.accounts
+ .getIdentitiesForServer(folder().server)
+ .some(id => id.fullAddress == author);
+ }
+ return false;
+ case "cmd_openConversation":
+ return (
+ // This (instead of numSelectedMessages) is necessary to be able to
+ // also open a collapsed thread in conversation.
+ gDBView.selection.count == 1 &&
+ ConversationOpener.isMessageIndexed(
+ gDBView.hdrForFirstSelectedMessage
+ )
+ );
+ case "cmd_replylist":
+ if (
+ !mailContextMenu.selectionIsOverridden &&
+ hasIdentities &&
+ numSelectedMessages == 1
+ ) {
+ return (window.messageBrowser?.contentWindow ?? window)
+ .currentHeaderData?.["list-post"];
+ }
+ return false;
+ case "cmd_viewPageSource":
+ case "cmd_saveAsTemplate":
+ return numSelectedMessages == 1;
+ case "cmd_reply":
+ case "cmd_replySender":
+ case "cmd_replyall":
+ case "cmd_forward":
+ case "cmd_forwardInline":
+ case "cmd_forwardAttachment":
+ case "cmd_redirect":
+ case "cmd_editAsNew":
+ return (
+ hasIdentities &&
+ (numSelectedMessages == 1 ||
+ (numSelectedMessages > 1 &&
+ // Exclude collapsed threads.
+ numSelectedMessages == gDBView.selection.count))
+ );
+ case "cmd_copyMessage":
+ case "cmd_saveAsFile":
+ return numSelectedMessages >= 1;
+ case "cmd_openMessage":
+ return (
+ (location.href == "about:3pane" ||
+ parent.location.href == "about:3pane") &&
+ numSelectedMessages >= 1 &&
+ !isDummyMessage
+ );
+ case "cmd_tag":
+ case "cmd_tag1":
+ case "cmd_tag2":
+ case "cmd_tag3":
+ case "cmd_tag4":
+ case "cmd_tag5":
+ case "cmd_tag6":
+ case "cmd_tag7":
+ case "cmd_tag8":
+ case "cmd_tag9":
+ case "cmd_addTag":
+ case "cmd_removeTags":
+ case "cmd_toggleTag":
+ case "cmd_toggleRead":
+ case "cmd_markReadByDate":
+ case "cmd_markAsFlagged":
+ case "cmd_applyFiltersToSelection":
+ return numSelectedMessages >= 1 && !isDummyMessage;
+ case "cmd_copyDecryptedTo": {
+ let showDecrypt = numSelectedMessages > 1;
+ if (numSelectedMessages == 1 && !isDummyMessage) {
+ let msgURI = gDBView.URIForFirstSelectedMessage;
+ if (msgURI) {
+ showDecrypt =
+ EnigmailURIs.isEncryptedUri(msgURI) ||
+ gEncryptedURIService.isEncrypted(msgURI);
+ }
+ }
+ return showDecrypt;
+ }
+ case "cmd_editDraftMsg":
+ return (
+ numSelectedMessages >= 1 &&
+ folder()?.isSpecialFolder(Ci.nsMsgFolderFlags.Drafts, true)
+ );
+ case "cmd_newMsgFromTemplate":
+ case "cmd_editTemplateMsg":
+ return (
+ numSelectedMessages >= 1 &&
+ folder()?.isSpecialFolder(Ci.nsMsgFolderFlags.Templates, true)
+ );
+ case "cmd_replyGroup":
+ return isNewsgroup();
+ case "cmd_markAsRead":
+ return (
+ numSelectedMessages >= 1 &&
+ !isDummyMessage &&
+ gViewWrapper.dbView.getSelectedMsgHdrs().some(msg => !msg.isRead)
+ );
+ case "cmd_markAsUnread":
+ return (
+ numSelectedMessages >= 1 &&
+ !isDummyMessage &&
+ gViewWrapper.dbView.getSelectedMsgHdrs().some(msg => msg.isRead)
+ );
+ case "cmd_markThreadAsRead": {
+ if (numSelectedMessages == 0 || isDummyMessage) {
+ return false;
+ }
+ let sel = gViewWrapper.dbView.selection;
+ for (let i = 0; i < sel.getRangeCount(); i++) {
+ let start = {};
+ let end = {};
+ sel.getRangeAt(i, start, end);
+ for (let j = start.value; j <= end.value; j++) {
+ if (
+ gViewWrapper.dbView.getThreadContainingIndex(j)
+ .numUnreadChildren > 0
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case "cmd_markAllRead":
+ return gDBView?.msgFolder?.getNumUnread(false) > 0;
+ case "cmd_markAsJunk":
+ case "cmd_markAsNotJunk":
+ return this._getViewCommandStatus(Ci.nsMsgViewCommandType.junk);
+ case "cmd_archive":
+ return (
+ !isDummyMessage &&
+ MessageArchiver.canArchive(
+ gDBView.getSelectedMsgHdrs(),
+ gViewWrapper.isSingleFolder
+ )
+ );
+ case "cmd_moveMessage": {
+ return canMove();
+ }
+ case "cmd_moveToFolderAgain": {
+ // Disable "Move to <folder> Again" for news and other read only
+ // folders since we can't really move messages from there - only copy.
+ let canMoveAgain = numSelectedMessages >= 1;
+ if (Services.prefs.getBoolPref("mail.last_msg_movecopy_was_move")) {
+ canMoveAgain = canMove() && !isNewsgroup();
+ }
+ if (canMoveAgain) {
+ let targetURI = Services.prefs.getStringPref(
+ "mail.last_msg_movecopy_target_uri"
+ );
+ canMoveAgain = targetURI && MailUtils.getExistingFolder(targetURI);
+ }
+ return !!canMoveAgain;
+ }
+ case "cmd_deleteMessage":
+ return canMove();
+ case "cmd_shiftDeleteMessage":
+ return this._getViewCommandStatus(
+ Ci.nsMsgViewCommandType.deleteNoTrash
+ );
+ case "cmd_createFilterFromMenu":
+ return (
+ numSelectedMessages == 1 &&
+ !isDummyMessage &&
+ folder()?.server.canHaveFilters
+ );
+ case "cmd_watchThread": {
+ let enabledObj = {};
+ let checkStatusObj = {};
+ gViewWrapper.dbView.getCommandStatus(
+ Ci.nsMsgViewCommandType.toggleThreadWatched,
+ enabledObj,
+ checkStatusObj
+ );
+ return enabledObj.value;
+ }
+ case "cmd_applyFilters": {
+ return this._getViewCommandStatus(Ci.nsMsgViewCommandType.applyFilters);
+ }
+ }
+
+ return false;
+ },
+ doCommand(command, ...args) {
+ if (!this.isCommandEnabled(command)) {
+ return;
+ }
+
+ if (command in this._composeCommands) {
+ this._composeMsgByType(this._composeCommands[command], ...args);
+ return;
+ }
+
+ if (command in this._navigationCommands) {
+ if (parent.location.href == "about:3pane") {
+ // If we're in about:message inside about:3pane, it's the parent
+ // window that needs to advance to the next message.
+ parent.commandController.doCommand(command, ...args);
+ } else {
+ this._navigate(this._navigationCommands[command]);
+ }
+ return;
+ }
+
+ if (command in this._viewCommands) {
+ if (command.endsWith("Read") || command.endsWith("Unread")) {
+ if (window.ClearPendingReadTimer) {
+ window.ClearPendingReadTimer();
+ } else {
+ window.messageBrowser.contentWindow.ClearPendingReadTimer();
+ }
+ }
+ gViewWrapper.dbView.doCommand(this._viewCommands[command]);
+ return;
+ }
+
+ if (command in this._callbackCommands) {
+ this._callbackCommands[command](...args);
+ }
+ },
+
+ _getViewCommandStatus(commandType) {
+ if (!gViewWrapper?.dbView) {
+ return false;
+ }
+
+ let enabledObj = {};
+ let checkStatusObj = {};
+ gViewWrapper.dbView.getCommandStatus(
+ commandType,
+ enabledObj,
+ checkStatusObj
+ );
+ return enabledObj.value;
+ },
+
+ /**
+ * Calls the ComposeMessage function with the desired type, and proper default
+ * based on the event that fired it.
+ *
+ * @param composeType the nsIMsgCompType to pass to the function
+ * @param event (optional) the event that triggered the call
+ */
+ _composeMsgByType(composeType, event) {
+ // If we're the hidden window, then we're not going to have a gFolderDisplay
+ // to work out existing folders, so just use null.
+ let msgFolder = gFolder;
+ let msgUris =
+ gFolder || gViewWrapper.isSynthetic
+ ? gDBView?.getURIsForSelection()
+ : [window.gMessageURI];
+
+ let messagePaneBrowser;
+ let autodetectCharset;
+ let selection;
+ if (!mailContextMenu.selectionIsOverridden) {
+ if (window.messageBrowser) {
+ if (!window.messageBrowser.hidden) {
+ messagePaneBrowser =
+ window.messageBrowser.contentWindow.getMessagePaneBrowser();
+ autodetectCharset =
+ window.messageBrowser.contentWindow.autodetectCharset;
+ }
+ } else {
+ messagePaneBrowser = window.getMessagePaneBrowser();
+ autodetectCharset = window.autodetectCharset;
+ }
+ selection = messagePaneBrowser?.contentWindow?.getSelection();
+ }
+
+ if (event && event.shiftKey) {
+ window.browsingContext.topChromeWindow.ComposeMessage(
+ composeType,
+ Ci.nsIMsgCompFormat.OppositeOfDefault,
+ msgFolder,
+ msgUris,
+ selection,
+ autodetectCharset
+ );
+ } else {
+ window.browsingContext.topChromeWindow.ComposeMessage(
+ composeType,
+ Ci.nsIMsgCompFormat.Default,
+ msgFolder,
+ msgUris,
+ selection,
+ autodetectCharset
+ );
+ }
+ },
+
+ _navigate(navigationType) {
+ if (
+ [Ci.nsMsgNavigationType.back, Ci.nsMsgNavigationType.forward].includes(
+ navigationType
+ )
+ ) {
+ const { messageHistory } = window.messageBrowser?.contentWindow ?? window;
+ const noCurrentMessage = messageHistory.canPop(0);
+ let relativePosition = -1;
+ if (navigationType === Ci.nsMsgNavigationType.forward) {
+ relativePosition = 1;
+ } else if (noCurrentMessage) {
+ relativePosition = 0;
+ }
+ let newMessageURI = messageHistory.pop(relativePosition)?.messageURI;
+ if (!newMessageURI) {
+ return;
+ }
+ let msgHdr =
+ MailServices.messageServiceFromURI(newMessageURI).messageURIToMsgHdr(
+ newMessageURI
+ );
+ if (msgHdr) {
+ if (window.threadPane) {
+ window.selectMessage(msgHdr);
+ } else {
+ window.displayMessage(newMessageURI);
+ }
+ }
+ return;
+ }
+
+ let resultKey = { value: nsMsgKey_None };
+ let resultIndex = { value: nsMsgViewIndex_None };
+ let threadIndex = {};
+
+ let expandCurrentThread = false;
+ let currentIndex = window.threadTree ? window.threadTree.currentIndex : -1;
+
+ // If we're doing next unread, and a collapsed thread is selected, and
+ // the top level message is unread, just set the result manually to
+ // the top level message, without using viewNavigate.
+ if (
+ navigationType == Ci.nsMsgNavigationType.nextUnreadMessage &&
+ currentIndex != -1 &&
+ gViewWrapper.isCollapsedThreadAtIndex(currentIndex) &&
+ !(
+ gViewWrapper.dbView.getFlagsAt(currentIndex) & Ci.nsMsgMessageFlags.Read
+ )
+ ) {
+ expandCurrentThread = true;
+ resultIndex.value = currentIndex;
+ resultKey.value = gViewWrapper.dbView.getKeyAt(currentIndex);
+ } else {
+ gViewWrapper.dbView.viewNavigate(
+ navigationType,
+ resultKey,
+ resultIndex,
+ threadIndex,
+ true
+ );
+ if (resultIndex.value == nsMsgViewIndex_None) {
+ if (CrossFolderNavigation(navigationType)) {
+ this._navigate(navigationType);
+ }
+ return;
+ }
+ if (resultKey.value == nsMsgKey_None) {
+ return;
+ }
+ }
+
+ if (window.threadTree) {
+ if (
+ gDBView.selection.count == 1 &&
+ window.threadTree.selectedIndex == resultIndex.value &&
+ !expandCurrentThread
+ ) {
+ return;
+ }
+
+ window.threadTree.expandRowAtIndex(resultIndex.value);
+ // Do an instant scroll before setting the index to avoid animation.
+ window.threadTree.scrollToIndex(resultIndex.value, true);
+ window.threadTree.selectedIndex = resultIndex.value;
+ // Focus the thread tree, unless the message pane has focus.
+ if (
+ Services.focus.focusedWindow !=
+ window.messageBrowser.contentWindow?.getMessagePaneBrowser()
+ .contentWindow
+ ) {
+ // There's something strange going on here – calling `focus`
+ // immediately can cause the scroll position to return to where it was
+ // before changing folders, which starts a cascade of "scroll" events
+ // until the tree scrolls to the top.
+ setTimeout(() => window.threadTree.table.body.focus());
+ }
+ } else {
+ if (window.gMessage.messageKey == resultKey.value) {
+ return;
+ }
+
+ gViewWrapper.dbView.selection.select(resultIndex.value);
+ window.displayMessage(
+ gViewWrapper.dbView.URIForFirstSelectedMessage,
+ gViewWrapper
+ );
+ }
+ },
+};
+// Add the controller to this window's controllers, so that built-in commands
+// such as cmd_selectAll run our code instead of the default code.
+window.controllers.insertControllerAt(0, commandController);
+
+var dbViewWrapperListener = {
+ _nextViewIndexAfterDelete: null,
+
+ messenger: null,
+ msgWindow: top.msgWindow,
+ threadPaneCommandUpdater: {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgDBViewCommandUpdater",
+ "nsISupportsWeakReference",
+ ]),
+ updateCommandStatus() {},
+ displayMessageChanged(folder, subject, keywords) {},
+ updateNextMessageAfterDelete() {
+ dbViewWrapperListener._nextViewIndexAfterDelete = gDBView
+ ? gDBView.msgToSelectAfterDelete
+ : null;
+ },
+ summarizeSelection() {
+ return true;
+ },
+ selectedMessageRemoved() {
+ // We need to invalidate the tree, but this method could get called
+ // multiple times, so we won't invalidate until we get to the end of the
+ // event loop.
+ if (this._timeout) {
+ return;
+ }
+ this._timeout = setTimeout(() => {
+ dbViewWrapperListener.onMessagesRemoved();
+ window.threadTree?.invalidate();
+ delete this._timeout;
+ });
+ },
+ },
+
+ get shouldUseMailViews() {
+ return !!top.ViewPickerBinding?.isVisible;
+ },
+ get shouldDeferMessageDisplayUntilAfterServerConnect() {
+ return false;
+ },
+ shouldMarkMessagesReadOnLeavingFolder(msgFolder) {
+ return false;
+ },
+ onFolderLoading(isFolderLoading) {},
+ onSearching(isSearching) {},
+ onCreatedView() {
+ if (window.threadTree) {
+ window.threadPane.setTreeView(gViewWrapper.dbView);
+ // There is no persisted thread last expanded state for synthetic views.
+ if (!gViewWrapper.isSynthetic) {
+ window.threadPane.restoreThreadState();
+ }
+ window.threadPane.isFirstScroll = true;
+ window.threadPane.scrollDetected = false;
+ window.threadPane.scrollToLatestRowIfNoSelection();
+ }
+ },
+ onDestroyingView(folderIsComingBack) {
+ if (!window.threadTree) {
+ return;
+ }
+
+ if (folderIsComingBack) {
+ // We'll get a new view of the same folder (e.g. with a quick filter) -
+ // try to preserve the selection.
+ window.threadPane.saveSelection();
+ } else {
+ if (gDBView) {
+ gDBView.setJSTree(null);
+ }
+ window.threadTree.view = gDBView = null;
+ }
+ },
+ onLoadingFolder(dbFolderInfo) {
+ window.quickFilterBar?.onFolderChanged();
+ },
+ onDisplayingFolder() {},
+ onLeavingFolder() {},
+ onMessagesLoaded(all) {
+ if (!window.threadPane) {
+ return;
+ }
+ // Try to restore what was selected. Keep the saved selection (if there is
+ // one) until we have all of the messages. This will also reveal selected
+ // messages in collapsed threads.
+ window.threadPane.restoreSelection({ discard: all });
+
+ if (all || gViewWrapper.search.hasSearchTerms) {
+ window.threadPane.ensureThreadStateForQuickSearchView();
+ let newMessageFound = false;
+ if (window.threadPane.scrollToNewMessage) {
+ try {
+ let index = gDBView.findIndexOfMsgHdr(gFolder.firstNewMessage, true);
+ if (index != nsMsgViewIndex_None) {
+ window.threadTree.scrollToIndex(index, true);
+ newMessageFound = true;
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ window.threadPane.scrollToNewMessage = false;
+ }
+ window.threadTree.reset();
+ if (!newMessageFound && !window.threadPane.scrollDetected) {
+ window.threadPane.scrollToLatestRowIfNoSelection();
+ }
+ }
+ window.quickFilterBar?.onMessagesChanged();
+ },
+ onMailViewChanged() {
+ window.dispatchEvent(new CustomEvent("MailViewChanged"));
+ },
+ onSortChanged() {
+ // If there is no selection, scroll to the most relevant end.
+ window.threadPane?.scrollToLatestRowIfNoSelection();
+ },
+ onMessagesRemoved() {
+ window.quickFilterBar?.onMessagesChanged();
+
+ if (!gDBView || (!gFolder && !gViewWrapper?.isSynthetic)) {
+ // This can't be a notification about the message currently displayed.
+ return;
+ }
+
+ let rowCount = gDBView.rowCount;
+
+ // There's no messages left.
+ if (rowCount == 0) {
+ if (location.href == "about:3pane") {
+ // In a 3-pane tab, clear the message pane and selection.
+ window.threadTree.selectedIndex = -1;
+ } else if (parent?.location != "about:3pane") {
+ // In a standalone message tab or window, close the tab or window.
+ let tabmail = top.document.getElementById("tabmail");
+ if (tabmail) {
+ tabmail.closeTab(window.tabOrWindow);
+ } else {
+ top.close();
+ }
+ }
+ this._nextViewIndexAfterDelete = null;
+ return;
+ }
+
+ if (
+ this._nextViewIndexAfterDelete != null &&
+ this._nextViewIndexAfterDelete != nsMsgViewIndex_None
+ ) {
+ // Select the next message in the view, based on what we were told in
+ // updateNextMessageAfterDelete.
+ if (this._nextViewIndexAfterDelete >= rowCount) {
+ this._nextViewIndexAfterDelete = rowCount - 1;
+ }
+ if (
+ this._nextViewIndexAfterDelete > -1 &&
+ !mailContextMenu.selectionIsOverridden
+ ) {
+ if (location.href == "about:3pane") {
+ // A "select" event should fire here, but setting the selected index
+ // might not fire it. OTOH, we want it to fire only once, so see if
+ // the event is fired, and if not, fire it.
+ let eventFired = false;
+ let onSelect = () => (eventFired = true);
+
+ window.threadTree.addEventListener("select", onSelect, {
+ once: true,
+ });
+ window.threadTree.selectedIndex = this._nextViewIndexAfterDelete;
+ window.threadTree.removeEventListener("select", onSelect);
+
+ if (!eventFired) {
+ window.threadTree.dispatchEvent(new CustomEvent("select"));
+ }
+ } else if (parent?.location != "about:3pane") {
+ if (
+ Services.prefs.getBoolPref("mail.close_message_window.on_delete")
+ ) {
+ // Bail out early if this is about a partial POP3 message that has
+ // just been completed and reloaded.
+ if (document.body.classList.contains("completed-message")) {
+ document.body.classList.remove("completed-message");
+ return;
+ }
+ // Close the tab or window if the displayed message is deleted.
+ let tabmail = top.document.getElementById("tabmail");
+ if (tabmail) {
+ tabmail.closeTab(window.tabOrWindow);
+ } else {
+ top.close();
+ }
+ return;
+ }
+ gDBView.selection.select(this._nextViewIndexAfterDelete);
+ window.displayMessage(
+ gDBView.getURIForViewIndex(this._nextViewIndexAfterDelete),
+ gViewWrapper
+ );
+ }
+ }
+ this._nextViewIndexAfterDelete = null;
+ }
+ },
+ onMessageRemovalFailed() {
+ this._nextViewIndexAfterDelete = null;
+ },
+ onMessageCountsChanged() {
+ window.quickFilterBar?.onMessagesChanged();
+ },
+};