diff options
Diffstat (limited to 'comm/mail/base/content/mailCommon.js')
-rw-r--r-- | comm/mail/base/content/mailCommon.js | 1126 |
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(); + }, +}; |