/* 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/. */ /* globals commandController, dbViewWrapperListener */ // mailCommon.js // about:3pane and about:message must BOTH provide these: /* globals goDoCommand */ // globalOverlay.js /* globals gDBView, gFolder, gViewWrapper, messengerBundle */ var { MailServices } = ChromeUtils.importESModule( "resource:///modules/MailServices.sys.mjs" ); var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); var { openLinkExternally, openWebSearch } = ChromeUtils.importESModule( "resource:///modules/LinkHelper.sys.mjs" ); var { FeedUtils } = ChromeUtils.importESModule( "resource:///modules/FeedUtils.sys.mjs" ); ChromeUtils.defineESModuleGetters(this, { calendarDeactivator: "resource:///modules/calendar/calCalendarDeactivator.sys.mjs", EnigmailURIs: "chrome://openpgp/content/modules/uris.sys.mjs", MailUtils: "resource:///modules/MailUtils.sys.mjs", nsContextMenu: "chrome://messenger/content/nsContextMenu.sys.mjs", PhishingDetector: "resource:///modules/PhishingDetector.sys.mjs", TagUtils: "resource:///modules/TagUtils.sys.mjs", }); window.addEventListener( "DOMContentLoaded", () => { const contextMenuPopup = document.getElementById("mailContext"); mailContextMenu._menupopup = contextMenuPopup; contextMenuPopup.addEventListener("command", mailContextMenu); contextMenuPopup.addEventListener("popupshowing", mailContextMenu); contextMenuPopup.addEventListener("popuphidden", mailContextMenu); }, { once: true } ); /** * Called by ContextMenuParent if this window is about:3pane, or is * about:message but not contained by about:3pane. * * @returns {boolean} true if this function opened the context menu */ function openContextMenu({ data, target }, browser) { if (window.browsingContext.parent != window.browsingContext.top) { // Not sure how we'd get here, but let's not continue if we do. return false; } if (browser.id != "messagepane") { return false; } mailContextMenu.setAsMessagePaneContextMenu(data, target.browsingContext); const screenX = data.context.screenXDevPx / window.devicePixelRatio; const screenY = data.context.screenYDevPx / window.devicePixelRatio; const popup = document.getElementById("mailContext"); popup.openPopupAtScreen(screenX, screenY, true); return true; } var mailContextMenu = { /** * @type {XULPopupElement} */ _menupopup: null, // Commands handled by commandController. _commands: { "mailContext-editDraftMsg": "cmd_editDraftMsg", "mailContext-newMsgFromTemplate": "cmd_newMsgFromTemplate", "mailContext-editTemplateMsg": "cmd_editTemplateMsg", "mailContext-openConversation": "cmd_openConversation", "mailContext-replyNewsgroup": "cmd_replyGroup", "mailContext-replySender": "cmd_replySender", "navContext-reply": "cmd_reply", "mailContext-replyAll": "cmd_replyall", "mailContext-replyList": "cmd_replylist", "mailContext-forward": "cmd_forward", "mailContext-forwardAsInline": "cmd_forwardInline", "mailContext-forwardAsAttachment": "cmd_forwardAttachment", "mailContext-multiForwardAsAttachment": "cmd_forwardAttachment", "mailContext-redirect": "cmd_redirect", "mailContext-cancel": "cmd_cancel", "mailContext-editAsNew": "cmd_editAsNew", "mailContext-addNewTag": "cmd_addTag", "mailContext-manageTags": "cmd_manageTags", "mailContext-tagRemoveAll": "cmd_removeTags", "mailContext-markFlagged": "cmd_markAsFlagged", "navContext-archive": "cmd_archive", "mailContext-archive": "cmd_archive", "mailContext-moveToFolderAgain": "cmd_moveToFolderAgain", "mailContext-decryptToFolder": "cmd_copyDecryptedTo", "mailContext-ignoreThread": "cmd_killThread", "mailContext-ignoreSubthread": "cmd_killSubthread", "mailContext-watchThread": "cmd_watchThread", "mailContext-saveAs": "cmd_saveAsFile", "mailContext-print": "cmd_print", "mailContext-downloadSelected": "cmd_downloadSelected", }, // More commands handled by commandController, except these ones get // disabled instead of hidden. _alwaysVisibleCommands: { "navContext-markRead": "cmd_markAsRead", "mailContext-markRead": "cmd_markAsRead", "navContext-markUnread": "cmd_markAsUnread", "mailContext-markUnread": "cmd_markAsUnread", "mailContext-markThreadAsRead": "cmd_markThreadAsRead", "mailContext-markReadByDate": "cmd_markReadByDate", "mailContext-markAllRead": "cmd_markAllRead", "navContext-markAsJunk": "cmd_markAsJunk", "mailContext-markAsJunk": "cmd_markAsJunk", "navContext-markAsNotJunk": "cmd_markAsNotJunk", "mailContext-markAsNotJunk": "cmd_markAsNotJunk", "mailContext-recalculateJunkScore": "cmd_recalculateJunkScore", }, /** * If we have overridden the selection for the context menu. * * @see `setOverrideSelection` * @type {boolean} */ _selectionIsOverridden: false, handleEvent(event) { switch (event.type) { case "popupshowing": this.onPopupShowing(event); break; case "popuphidden": this.onPopupHidden(event); break; case "command": this.onCommand(event); break; } }, onPopupShowing(event) { if (event.target == this._menupopup) { this.fillMailContextMenu(event); } }, onPopupHidden(event) { if (event.target == this._menupopup) { this.clearOverrideSelection(); } }, onCommand(event) { this.onMailContextMenuCommand(event); }, /** * Override the selection that this context menu should operate on. The * effect lasts until `clearOverrideSelection` is called by `onPopupHidden`. * * @param {integer} index - The index of the row to use as selection. */ setOverrideSelection(index) { this._selectionIsOverridden = true; window.threadPane.saveSelection(); window.threadTree._selection.selectEventsSuppressed = true; window.threadTree._selection.select(index); }, /** * Has the real selection been overridden by a right-click on a message that * wasn't selected? * * @type {boolean} */ get selectionIsOverridden() { return this._selectionIsOverridden; }, /** * Clear the overriding selection, and go back to the previous selection. */ clearOverrideSelection() { if (!window.threadTree) { return; } if (this._selectionIsOverridden) { // Prevent selection of a new message if there's a delete in progress. dbViewWrapperListener._nextViewIndexAfterDelete = null; window.threadTree._selection.selectEventsSuppressed = true; window.threadPane.restoreSelection({ notify: false }); this._selectionIsOverridden = false; window.threadTree.invalidate(); } window.threadTree .querySelector(".context-menu-target") ?.classList.remove("context-menu-target"); window.threadTree._selection.selectEventsSuppressed = false; window.threadTree.table.body.focus(); }, setAsThreadPaneContextMenu() { delete this.browsingContext; delete this.context; delete this.selectionInfo; this.inThreadTree = true; for (const id of [ "mailContext-openInBrowser", "mailContext-openLinkInBrowser", "mailContext-copylink", "mailContext-savelink", "mailContext-reportPhishingURL", "mailContext-addemail", "mailContext-composeemailto", "mailContext-copyemail", "mailContext-copyimage", "mailContext-saveimage", "mailContext-copy", "mailContext-selectall", "mailContext-searchTheWeb", ]) { document.getElementById(id).hidden = true; } }, setAsMessagePaneContextMenu({ context, selectionInfo }, browsingContext) { function showItem(id, show) { const item = document.getElementById(id); if (item) { item.hidden = !show; } } delete this.inThreadTree; this.browsingContext = browsingContext; this.context = context; this.selectionInfo = selectionInfo; // showItem("mailContext-openInBrowser", false); showItem( "mailContext-openLinkInBrowser", context.onLink && !context.onMailtoLink ); showItem("mailContext-copylink", context.onLink && !context.onMailtoLink); showItem("mailContext-savelink", context.onLink && !context.onMailtoLink); showItem( "mailContext-reportPhishingURL", context.onLink && !context.onMailtoLink ); showItem("mailContext-addemail", context.onMailtoLink); showItem("mailContext-composeemailto", context.onMailtoLink); showItem("mailContext-copyemail", context.onMailtoLink); showItem("mailContext-copyimage", context.onImage); showItem("mailContext-saveimage", context.onLoadedImage); showItem( "mailContext-copy", selectionInfo && !selectionInfo.docSelectionIsCollapsed ); showItem("mailContext-selectall", true); showItem( "mailContext-searchTheWeb", selectionInfo && !selectionInfo.docSelectionIsCollapsed ); const searchTheWeb = document.getElementById("mailContext-searchTheWeb"); if (!searchTheWeb.hidden) { let key = "openSearch.label"; let abbrSelection; if (selectionInfo.text.length > 15) { key += ".truncated"; abbrSelection = selectionInfo.text.slice(0, 15); } else { abbrSelection = selectionInfo.text; } searchTheWeb.label = messengerBundle.formatStringFromName(key, [ Services.search.defaultEngine.name, abbrSelection, ]); } }, fillMailContextMenu(event) { function showItem(id, show) { const item = document.getElementById(id); if (item) { item.hidden = !show; } } function enableItem(id, enabled) { const item = document.getElementById(id); item.disabled = !enabled; } function checkItem(id, checked) { const item = document.getElementById(id); if (item) { // Convert truthy/falsy to boolean before string. item.setAttribute("checked", !!checked); } } function setSingleSelection(id, show = true) { showItem(id, numSelectedMessages == 1 && show); enableItem(id, numSelectedMessages == 1); } // Hide things that don't work yet. for (const id of [ "mailContext-openInBrowser", "mailContext-recalculateJunkScore", ]) { showItem(id, false); } const onSpecialItem = this.context?.isContentSelected || this.context?.onCanvas || this.context?.onLink || this.context?.onImage || this.context?.onAudio || this.context?.onVideo || this.context?.onTextInput; // Ask commandController about the commands it controls. for (const [id, command] of Object.entries(this._commands)) { showItem( id, !onSpecialItem && commandController.isCommandEnabled(command) ); } for (const [id, command] of Object.entries(this._alwaysVisibleCommands)) { showItem(id, !onSpecialItem); enableItem(id, commandController.isCommandEnabled(command)); } showItem( "navContext-delete", !onSpecialItem && commandController.isCommandEnabled("cmd_deleteMessage") ); showItem("mailContext-navigation", !onSpecialItem); showItem("mailContext-sep-navigation", !onSpecialItem); const inAbout3Pane = !!window.threadTree; const inThreadTree = !!this.inThreadTree; const message = gFolder || gViewWrapper.isSynthetic ? gDBView?.hdrForFirstSelectedMessage : top.messenger.msgHdrFromURI(window.gMessageURI); const folder = message?.folder; const isDummyMessage = !gViewWrapper.isSynthetic && !folder; const numSelectedMessages = isDummyMessage ? 1 : gDBView.numSelected; const isNewsgroup = folder?.isSpecialFolder( Ci.nsMsgFolderFlags.Newsgroup, true ); const canMove = numSelectedMessages >= 1 && !isNewsgroup && folder?.canDeleteMessages; const canCopy = numSelectedMessages >= 1; const isJunk = message.getStringProperty("junkscore") == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE; if (numSelectedMessages == 1) { // Hide junk button that isn't opposite of current state. showItem("navContext-markAsJunk", !isJunk); showItem("navContext-markAsNotJunk", isJunk); } else { showItem("navContext-markAsJunk", true); showItem("navContext-markAsNotJunk", false); } const areIMAPDeleted = gViewWrapper.dbView .getSelectedMsgHdrs() .every(msg => msg.flags & Ci.nsMsgMessageFlags.IMAPDeleted); setSingleSelection("mailContext-openNewTab", inThreadTree); setSingleSelection("mailContext-openNewWindow", inThreadTree); setSingleSelection( "mailContext-openContainingFolder", !onSpecialItem && ((!isDummyMessage && !inAbout3Pane) || gViewWrapper.isSynthetic) ); setSingleSelection("mailContext-forward", !onSpecialItem); setSingleSelection("mailContext-forwardAsInline", !onSpecialItem); document.l10n.setAttributes( document.getElementById("mailContext-forwardAsAttachment"), "mail-context-menu-forward-as-attachment", { count: numSelectedMessages, } ); showItem( "mailContext-forwardAsAttachment", !onSpecialItem && numSelectedMessages && commandController.isCommandEnabled("cmd_forwardAttachment") ); if (isDummyMessage || onSpecialItem) { showItem("mailContext-tags", false); } else { showItem("mailContext-tags", true); this._initMessageTags(); } checkItem("mailContext-markFlagged", message?.isFlagged); // Disable move if we can't delete message(s) from this folder. showItem("mailContext-moveMenu", canMove && !onSpecialItem); showItem("mailContext-copyMenu", canCopy && !onSpecialItem); top.initMoveToFolderAgainMenu( document.getElementById("mailContext-moveToFolderAgain") ); // Show only if a message is actively selected in the DOM. // extractFromEmail can't work on dummy messages. showItem( "mailContext-calendar-convert-menu", !onSpecialItem && numSelectedMessages == 1 && !isDummyMessage && calendarDeactivator.isCalendarActivated ); setSingleSelection( "mailContext-copyMessageLink", !onSpecialItem && numSelectedMessages == 1 && !isDummyMessage && !FeedUtils.isFeedMessage(message) ); setSingleSelection("mailContext-copyNewsLink", !!isNewsgroup); const contextDelete = document.getElementById("navContext-delete"); contextDelete.setAttribute("active", !!areIMAPDeleted); contextDelete.dataset.imapDeleted = !!areIMAPDeleted; document.l10n.setAttributes( contextDelete, areIMAPDeleted ? "mail-context-messages-undelete" : "mail-context-messages-delete", { count: numSelectedMessages, } ); checkItem( "mailContext-ignoreThread", folder?.msgDatabase.isIgnored(message?.messageKey) ); checkItem( "mailContext-ignoreSubthread", folder && message.flags & Ci.nsMsgMessageFlags.Ignored ); checkItem( "mailContext-watchThread", folder?.msgDatabase.isWatched(message?.messageKey) ); showItem( "mailContext-downloadSelected", window.threadTree && numSelectedMessages > 1 ); this._ensureSubmenuVisibility(); // Special case: mark menu shouldn't be shown on external messages. if (isDummyMessage) { showItem("mailContext-mark", false); } let lastItem; for (const child of document.getElementById("mailContext").children) { if (child.localName == "menuseparator") { child.hidden = !lastItem || lastItem.localName == "menuseparator"; } if (!child.hidden) { lastItem = child; } } if (lastItem.localName == "menuseparator") { lastItem.hidden = true; } // The rest of this block sends menu information to WebExtensions. const selectionInfo = this.selectionInfo; const isContentSelected = selectionInfo ? !selectionInfo.docSelectionIsCollapsed : false; const textSelected = selectionInfo ? selectionInfo.text : ""; const isTextSelected = !!textSelected.length; const tabmail = top.document.getElementById("tabmail"); const subject = { menu: event.target, tab: tabmail ? tabmail.currentTabInfo : top, isContentSelected, isTextSelected, onTextInput: this.context?.onTextInput, onLink: this.context?.onLink, onImage: this.context?.onImage, onEditable: this.context?.onEditable, srcUrl: this.context?.mediaURL, linkText: this.context?.linkTextStr, linkUrl: this.context?.linkURL, selectionText: isTextSelected ? selectionInfo.fullText : undefined, pageUrl: this.browsingContext?.currentURI?.spec, }; if (inThreadTree) { subject.displayedFolder = folder; subject.selectedMessages = gDBView.getSelectedMsgHdrs(); } subject.context = subject; subject.wrappedJSObject = subject; Services.obs.notifyObservers(subject, "on-prepare-contextmenu"); Services.obs.notifyObservers(subject, "on-build-contextmenu"); }, /** * Hide all top level submenus of the context menu that have no visible items. * Ignores separators since they are only a visual aid for the user and not * actual features making the menu worth showing. Excludes generated submenus. */ _ensureSubmenuVisibility() { for (const menu of document.querySelectorAll( "#mailContext > menu:not(#mailContext-tags,#mailContext-moveMenu,#mailContext-copyMenu)" )) { menu.hidden = Array.from(menu.menupopup.children).every( child => child.hidden || child.localName === "menuseparator" ); } }, onMailContextMenuCommand(event) { // If commandController handles this command, ask it to do so. if (event.target.id in this._commands) { commandController.doCommand(this._commands[event.target.id], event); return; } if (event.target.id in this._alwaysVisibleCommands) { commandController.doCommand( this._alwaysVisibleCommands[event.target.id], event ); return; } switch (event.target.id) { case "navContext-delete": commandController.doCommand( // No Shift-Undelete. event.shiftKey && event.target.dataset.imapDeleted == "false" ? "cmd_shiftDeleteMessage" : "cmd_deleteMessage" ); break; // Links // case "mailContext-openInBrowser": // this._openInBrowser(); // break; case "mailContext-openLinkInBrowser": // Only called in about:message. openLinkExternally(this.context.linkURL); break; case "mailContext-copylink": goDoCommand("cmd_copyLink"); break; case "mailContext-savelink": top.saveURL( this.context.linkURL, // URL null, // originalURL this.context.linkTextStr, // fileName null, // filePickerTitleKey true, // shouldBypassCache false, // skipPrompt null, // referrerInfo null, // cookieJarSettings this.browsingContext.window?.document, // sourceDocument null, // isContentWindowPrivate, Services.scriptSecurityManager.getSystemPrincipal() // principal ); break; case "mailContext-reportPhishingURL": PhishingDetector.reportPhishingURL(this.context.linkURL); break; case "mailContext-addemail": top.addEmail(this.context.linkURL); break; case "mailContext-composeemailto": top.composeEmailTo( this.context.linkURL, gFolder ? MailServices.accounts.getFirstIdentityForServer(gFolder.server) : null, event.shiftKey ? Ci.nsIMsgCompFormat.OppositeOfDefault : Ci.nsIMsgCompFormat.Default ); break; case "mailContext-copyemail": { const addresses = top.getEmail(this.context.linkURL); Cc["@mozilla.org/widget/clipboardhelper;1"] .getService(Ci.nsIClipboardHelper) .copyString(addresses); break; } // Images case "mailContext-copyimage": goDoCommand("cmd_copyImageContents"); break; case "mailContext-saveimage": top.saveURL( this.context.imageInfo.currentSrc, // URL null, // originalURL this.context.linkTextStr, // fileName "SaveImageTitle", // filePickerTitleKey true, // shouldBypassCache false, // skipPrompt null, // referrerInfo null, // cookieJarSettings this.browsingContext.window?.document, // sourceDocument null, // isContentWindowPrivate, Services.scriptSecurityManager.getSystemPrincipal() // principal ); break; // Edit case "mailContext-copy": goDoCommand("cmd_copy"); break; case "mailContext-selectall": goDoCommand("cmd_selectAll"); break; // Search case "mailContext-searchTheWeb": openWebSearch(this.selectionInfo.text); break; // Open messages in the background. case "mailContext-openNewTab": top.OpenMessageInNewTab(gDBView.hdrForFirstSelectedMessage, { event, viewWrapper: gViewWrapper, background: true, }); break; case "mailContext-openNewWindow": top.MsgOpenNewWindowForMessage( gDBView.hdrForFirstSelectedMessage, gViewWrapper ); break; case "mailContext-openContainingFolder": MailUtils.displayMessageInFolderTab(gDBView.hdrForFirstSelectedMessage); break; // Move/copy/archive/convert/delete // (Move and Copy sub-menus are handled in the default case.) // Calendar Convert sub-menu case "mailContext-calendar-convert-event-menuitem": top.calendarExtract.extractFromEmail( gDBView.hdrForFirstSelectedMessage, true ); break; case "mailContext-calendar-convert-task-menuitem": top.calendarExtract.extractFromEmail( gDBView.hdrForFirstSelectedMessage, false ); break; case "mailContext-copyMessageLink": navigator.clipboard.writeText( `mid:${gDBView.hdrForFirstSelectedMessage?.messageId}` ); break; case "mailContext-copyNewsLink": { const message = gDBView.hdrForFirstSelectedMessage; navigator.clipboard.writeText( MailUtils.constructNewsUriSpec( message?.messageId, message?.folder?.server ) ); } break; // Save/print/download default: { if ( document.getElementById("mailContext-moveMenu").contains(event.target) ) { commandController.doCommand("cmd_moveMessage", event.target._folder); } else if ( document.getElementById("mailContext-copyMenu").contains(event.target) ) { commandController.doCommand("cmd_copyMessage", event.target._folder); } else if ( document .getElementById("mailContext-decryptToFolder") .contains(event.target) ) { commandController.doCommand( "cmd_copyDecryptedTo", event.target._folder ); } break; } } }, // Tags sub-menu /** * Refresh the contents of the tag popup menu/panel. * Used for example for appmenu/Message/Tag panel. * * @see InitMessageTags() */ _initMessageTags() { const parent = document.getElementById("mailContext-tagpopup"); // Remove any existing non-static items (clear tags list before rebuilding it). // There is a separator element above the dynamically added tag elements, so // remove dynamically added elements below the separator. while (parent.lastElementChild.localName == "menuitem") { parent.lastElementChild.remove(); } // Create label and accesskey for the static "remove all tags" item. const removeItem = document.getElementById("mailContext-tagRemoveAll"); removeItem.label = messengerBundle.GetStringFromName( "mailnews.tags.remove" ); // Rebuild the list. const message = gDBView.hdrForFirstSelectedMessage; const currentTags = message ? message.getStringProperty("keywords").split(" ") : []; let index = 1; for (const tagInfo of MailServices.tags.getAllTags()) { const msgHasTag = currentTags.includes(tagInfo.key); if (tagInfo.ordinal.includes("~AUTOTAG") && !msgHasTag) { return; } const item = document.createXULElement("menuitem"); const accessKey = index < 10 ? index : ""; if (accessKey !== "") { item.accessKey = accessKey; } item.label = messengerBundle.formatStringFromName( "mailnews.tags.format", [accessKey, tagInfo.tag] ); item.setAttribute("type", "checkbox"); if (msgHasTag) { item.setAttribute("checked", "true"); } item.value = tagInfo.key; item.addEventListener("command", () => commandController._toggleMessageTag( tagInfo.key, item.getAttribute("checked") == "true" ) ); if (tagInfo.color) { item.style.color = tagInfo.color; } parent.appendChild(item); index++; } }, };