diff options
Diffstat (limited to 'comm/mail/base/content/aboutMessage.js')
-rw-r--r-- | comm/mail/base/content/aboutMessage.js | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/comm/mail/base/content/aboutMessage.js b/comm/mail/base/content/aboutMessage.js new file mode 100644 index 0000000000..29ce47ba4d --- /dev/null +++ b/comm/mail/base/content/aboutMessage.js @@ -0,0 +1,617 @@ +/* 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 Enigmail, MailE10SUtils */ + +// mailCommon.js +/* globals commandController, DBViewWrapper, dbViewWrapperListener, + nsMsgViewIndex_None, TreeSelection */ +/* globals gDBView: true, gFolder: true, gViewWrapper: true */ + +// mailContext.js +/* globals mailContextMenu */ + +// msgHdrView.js +/* globals AdjustHeaderView ClearCurrentHeaders ClearPendingReadTimer + HideMessageHeaderPane OnLoadMsgHeaderPane OnTagsChange + OnUnloadMsgHeaderPane HandleAllAttachments AttachmentMenuController */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + UIDensity: "resource:///modules/UIDensity.jsm", + UIFontSize: "resource:///modules/UIFontSize.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +const messengerBundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" +); + +var gMessage, gMessageURI; +var autodetectCharset; + +function getMessagePaneBrowser() { + return document.getElementById("messagepane"); +} + +function messagePaneOnResize() { + const doc = getMessagePaneBrowser().contentDocument; + // Bail out if it's http content or we don't have images. + if (doc?.URL.startsWith("http") || !doc?.images) { + return; + } + + for (let img of doc.images) { + img.toggleAttribute( + "overflowing", + img.clientWidth - doc.body.offsetWidth >= 0 && + (img.clientWidth <= img.naturalWidth || !img.naturalWidth) + ); + } +} + +function ReloadMessage() { + if (!gMessageURI) { + return; + } + displayMessage(gMessageURI, gViewWrapper); +} + +function MailSetCharacterSet() { + let messageService = MailServices.messageServiceFromURI(gMessageURI); + gMessage = messageService.messageURIToMsgHdr(gMessageURI); + messageService.loadMessage( + gMessageURI, + getMessagePaneBrowser().docShell, + top.msgWindow, + null, + true + ); + autodetectCharset = true; +} + +window.addEventListener("DOMContentLoaded", event => { + if (event.target != document) { + return; + } + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); + + OnLoadMsgHeaderPane(); + + Enigmail.msg.messengerStartup(); + Enigmail.hdrView.hdrViewLoad(); + + MailServices.mailSession.AddFolderListener( + folderListener, + Ci.nsIFolderListener.removed + ); + + preferenceObserver.init(); + Services.obs.addObserver(msgObserver, "message-content-updated"); + + const browser = getMessagePaneBrowser(); + + if (parent == top) { + // Standalone message display? Focus the message pane. + browser.focus(); + } + + if (window.parent == window.top) { + mailContextMenu.init(); + } + + // There might not be a msgWindow variable on the top window + // if we're e.g. showing a message in a dedicated window. + if (top.msgWindow) { + // Necessary plumbing to communicate status updates back to + // the user. + browser.docShell + ?.QueryInterface(Ci.nsIWebProgress) + .addProgressListener( + top.msgWindow.statusFeedback, + Ci.nsIWebProgress.NOTIFY_ALL + ); + } + + window.dispatchEvent( + new CustomEvent("aboutMessageLoaded", { bubbles: true }) + ); +}); + +window.addEventListener("unload", () => { + ClearPendingReadTimer(); + OnUnloadMsgHeaderPane(); + MailServices.mailSession.RemoveFolderListener(folderListener); + preferenceObserver.cleanUp(); + Services.obs.removeObserver(msgObserver, "message-content-updated"); + gViewWrapper?.close(); +}); + +function displayMessage(uri, viewWrapper) { + // Clear the state flags, if this window is re-used. + window.msgLoaded = false; + window.msgLoading = false; + + // Clean up existing objects before starting again. + ClearPendingReadTimer(); + gMessage = null; + if (gViewWrapper && viewWrapper != gViewWrapper) { + // Don't clean up gViewWrapper if we're going to reuse it. If we're inside + // about:3pane, close the view wrapper, but don't call `onLeavingFolder`, + // because about:3pane will do that if we're actually leaving the folder. + gViewWrapper?.close(parent != top); + gViewWrapper = null; + } + gDBView = null; + + gMessageURI = uri; + ClearCurrentHeaders(); + + if (!uri) { + HideMessageHeaderPane(); + MailE10SUtils.loadAboutBlank(getMessagePaneBrowser()); + window.msgLoaded = true; + window.dispatchEvent( + new CustomEvent("messageURIChanged", { bubbles: true, detail: uri }) + ); + return; + } + + let messageService = MailServices.messageServiceFromURI(uri); + gMessage = messageService.messageURIToMsgHdr(uri); + gFolder = gMessage.folder; + + messageHistory.push(uri); + + if (gFolder) { + if (viewWrapper) { + if (viewWrapper != gViewWrapper) { + gViewWrapper = viewWrapper.clone(dbViewWrapperListener); + } + } else { + gViewWrapper = new DBViewWrapper(dbViewWrapperListener); + gViewWrapper._viewFlags = Ci.nsMsgViewFlagsType.kThreadedDisplay; + gViewWrapper.open(gFolder); + } + } else { + gViewWrapper = new DBViewWrapper(dbViewWrapperListener); + gViewWrapper.openSearchView(); + } + gDBView = gViewWrapper.dbView; + let selection = (gDBView.selection = new TreeSelection()); + selection.view = gDBView; + let index = gDBView.findIndexOfMsgHdr(gMessage, true); + selection.select(index == nsMsgViewIndex_None ? -1 : index); + gDBView?.setJSTree({ + QueryInterface: ChromeUtils.generateQI(["nsIMsgJSTree"]), + _inBatch: false, + beginUpdateBatch() { + this._inBatch = true; + }, + endUpdateBatch() { + this._inBatch = false; + }, + ensureRowIsVisible(index) {}, + invalidate() {}, + invalidateRange(startIndex, endIndex) {}, + rowCountChanged(index, count) { + let wasSuppressed = gDBView.selection.selectEventsSuppressed; + gDBView.selection.selectEventsSuppressed = true; + gDBView.selection.adjustSelection(index, count); + gDBView.selection.selectEventsSuppressed = wasSuppressed; + }, + currentIndex: null, + }); + + if (gMessage.flags & Ci.nsMsgMessageFlags.HasRe) { + document.title = `Re: ${gMessage.mime2DecodedSubject || ""}`; + } else { + document.title = gMessage.mime2DecodedSubject; + } + + let browser = getMessagePaneBrowser(); + const browserChanged = MailE10SUtils.changeRemoteness(browser, null); + // The message pane browser should inherit `docShellIsActive` from the + // about:message browser, but changing remoteness causes that to not happen. + browser.docShellIsActive = !document.hidden; + browser.docShell.allowAuth = false; + browser.docShell.allowDNSPrefetch = false; + + if (browserChanged) { + browser.docShell + ?.QueryInterface(Ci.nsIWebProgress) + .addProgressListener( + top.msgWindow.statusFeedback, + Ci.nsIWebProgress.NOTIFY_ALL + ); + } + + if (gMessage.flags & Ci.nsMsgMessageFlags.Partial) { + document.body.classList.add("partial-message"); + } else if (document.body.classList.contains("partial-message")) { + document.body.classList.remove("partial-message"); + document.body.classList.add("completed-message"); + } + + // @implements {nsIUrlListener} + let urlListener = { + OnStartRunningUrl(url) {}, + OnStopRunningUrl(url, status) { + window.msgLoading = true; + window.dispatchEvent( + new CustomEvent("messageURIChanged", { bubbles: true, detail: uri }) + ); + if (url instanceof Ci.nsIMsgMailNewsUrl && url.seeOtherURI) { + // Show error page if needed. + HideMessageHeaderPane(); + MailE10SUtils.loadURI(getMessagePaneBrowser(), url.seeOtherURI); + } + }, + }; + try { + messageService.loadMessage( + uri, + browser.docShell, + top.msgWindow, + urlListener, + false + ); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_OFFLINE) { + throw ex; + } + + // TODO: This should be replaced with a real page, and made not ugly. + let title = messengerBundle.GetStringFromName("nocachedbodytitle"); + // This string includes some HTML! Get rid of it. + title = title.replace(/<\/?title>/gi, ""); + let body = messengerBundle.GetStringFromName("nocachedbodybody2"); + HideMessageHeaderPane(); + MailE10SUtils.loadURI( + getMessagePaneBrowser(), + "data:text/html;base64," + + btoa( + `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8" /> + <title>${title}</title> + </head> + <body> + <h1>${title}</h1> + <p>${body}</p> + </body> + </html>` + ) + ); + } + autodetectCharset = false; +} + +var folderListener = { + QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]), + + onFolderRemoved(parentFolder, childFolder) {}, + onMessageRemoved(parentFolder, msg) { + messageHistory.onMessageRemoved(parentFolder, msg); + }, +}; + +var msgObserver = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if ( + topic == "message-content-updated" && + gMessageURI == subject.QueryInterface(Ci.nsISupportsString).data + ) { + // This notification is triggered after a partial pop3 message was + // fully downloaded. The old message URI is now gone. To reload the + // message, we display it with its new URI. + displayMessage(data, gViewWrapper); + } + }, +}; + +var preferenceObserver = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + _topics: [ + "mail.inline_attachments", + "mail.show_headers", + "mail.showCondensedAddresses", + "mailnews.display.disallow_mime_handlers", + "mailnews.display.html_as", + "mailnews.display.prefer_plaintext", + "mailnews.headers.showReferences", + "rss.show.summary", + ], + + _reloadTimeout: null, + + init() { + for (let topic of this._topics) { + Services.prefs.addObserver(topic, this); + } + }, + + cleanUp() { + for (let topic of this._topics) { + Services.prefs.removeObserver(topic, this); + } + }, + + observe(subject, topic, data) { + if (data == "mail.show_headers") { + AdjustHeaderView(Services.prefs.getIntPref(data)); + } + if (!this._reloadTimeout) { + // Clear the event queue before reloading the message. Several prefs may + // be changed at once. + this._reloadTimeout = setTimeout(() => { + this._reloadTimeout = null; + ReloadMessage(); + }); + } + }, +}; + +var messageHistory = { + MAX_HISTORY_SIZE: 20, + /** + * @typedef {object} MessageHistoryEntry + * @property {string} messageURI - URI of the message for this entry. + * @property {string} folderURI - URI of the folder for this entry. + */ + /** + * @type {MessageHistoryEntry[]} + */ + _history: [], + _currentIndex: -1, + /** + * Remove the message from the history, cleaning up the state as needed in + * the process. + * + * @param {nsIMsgFolder} parentFolder + * @param {nsIMsgDBHdr} message + */ + onMessageRemoved(parentFolder, message) { + if (!this._history.length) { + return; + } + const messageURI = parentFolder.generateMessageURI(message.messageKey); + const folderURI = parentFolder.URI; + const oldLength = this._history.length; + let removedEntriesBeforeFuture = 0; + this._history = this._history.filter((entry, index) => { + const keepEntry = + entry.messageURI !== messageURI || entry.folderURI !== folderURI; + if (!keepEntry && index <= this._currentIndex) { + ++removedEntriesBeforeFuture; + } + return keepEntry; + }); + this._currentIndex -= removedEntriesBeforeFuture; + // Correct for first entry getting removed while it's the current entry. + if (this._history.length && this._currentIndex == -1) { + this._currentIndex = 0; + } + if (oldLength === this._history.length) { + return; + } + window.top.goUpdateCommand("cmd_goBack"); + window.top.goUpdateCommand("cmd_goForward"); + }, + /** + * Get the actual index in the history based on a delta from the current + * index. + * + * @param {number} delta - Relative delta from the current index. Forward is + * positive, backward is negative. + * @returns {number} Absolute index in the history, bounded to the history + * size. + */ + _getAbsoluteIndex(delta) { + return Math.min( + Math.max(this._currentIndex + delta, 0), + this._history.length - 1 + ); + }, + /** + * Add a message to the end of the history. Does nothing if the message is + * already the current item. Moves the history forward by one step if the next + * item already matches the given message. Else removes any "future" history + * if the current position isn't the newest entry in the history. + * + * If the history is growing larger than what we want to keep, it is trimmed. + * + * Assumes the view is currently in the folder that should be comitted to + * history. + * + * @param {string} messageURI - Message to add to the history. + */ + push(messageURI) { + if (!messageURI) { + return; + } + let currentItem = this._history[this._currentIndex]; + let currentFolder = gFolder?.URI; + if ( + currentItem && + messageURI === currentItem.messageURI && + currentFolder === currentItem.folderURI + ) { + return; + } + let nextMessageIndex = this._currentIndex + 1; + let erasedFuture = false; + if (nextMessageIndex < this._history.length) { + let nextMessage = this._history[nextMessageIndex]; + if ( + nextMessage && + messageURI === nextMessage.messageURI && + currentFolder === nextMessage.folderURI + ) { + this._currentIndex = nextMessageIndex; + if (this._currentIndex === 1) { + window.top.goUpdateCommand("cmd_goBack"); + } + if (this._currentIndex + 1 === this._history.length) { + window.top.goUpdateCommand("cmd_goForward"); + } + return; + } + this._history.splice(nextMessageIndex, Infinity); + erasedFuture = true; + } + this._history.push({ messageURI, folderURI: currentFolder }); + this._currentIndex = nextMessageIndex; + if (this._history.length > this.MAX_HISTORY_SIZE) { + let amountOfItemsToRemove = this._history.length - this.MAX_HISTORY_SIZE; + this._history.splice(0, amountOfItemsToRemove); + this._currentIndex -= amountOfItemsToRemove; + } + if (!currentItem || this._currentIndex === 0) { + window.top.goUpdateCommand("cmd_goBack"); + } + if (erasedFuture) { + window.top.goUpdateCommand("cmd_goForward"); + } + }, + /** + * Go forward or back in history relative to the current position. + * + * @param {number} delta + * @returns {?MessageHistoryEntry} The message and folder URI that are now at + * the active position in the history. If null is returned, no action was + * taken. + */ + pop(delta) { + let targetIndex = this._getAbsoluteIndex(delta); + if (this._currentIndex == targetIndex && gMessage) { + return null; + } + this._currentIndex = targetIndex; + window.top.goUpdateCommand("cmd_goBack"); + window.top.goUpdateCommand("cmd_goForward"); + return this._history[targetIndex]; + }, + /** + * Get the current state of the message history. + * + * @returns {{entries: MessageHistoryEntry[], currentIndex: number}} + * A list of message and folder URIs as strings and the current index in the + * entries. + */ + getHistory() { + return { entries: this._history.slice(), currentIndex: this._currentIndex }; + }, + /** + * Get a specific history entry relative to the current positon. + * + * @param {number} delta - Relative index to get the value of. + * @returns {?MessageHistoryEntry} If found, the message and + * folder URI at the given position. + */ + getMessageAt(delta) { + if (!this._history.length) { + return null; + } + return this._history[this._getAbsoluteIndex(delta)]; + }, + /** + * Check if going forward or back in the history by the given steps is + * possible. A special case is when no message is currently selected, going + * back to relative position 0 (so the current index) is possible. + * + * @param {number} delta - Relative position to go to from the current index. + * @returns {boolean} If there is a target available at that position in the + * current history. + */ + canPop(delta) { + let resultIndex = this._currentIndex + delta; + return ( + resultIndex >= 0 && + resultIndex < this._history.length && + (resultIndex !== this._currentIndex || !gMessage) + ); + }, + /** + * Clear the message history, resetting it to its initial empty state. + */ + clear() { + this._history.length = 0; + this._currentIndex = -1; + window.top.goUpdateCommand("cmd_goBack"); + window.top.goUpdateCommand("cmd_goForward"); + }, +}; + +commandController.registerCallback( + "cmd_delete", + () => commandController.doCommand("cmd_deleteMessage"), + () => commandController.isCommandEnabled("cmd_deleteMessage") +); +commandController.registerCallback( + "cmd_shiftDelete", + () => commandController.doCommand("cmd_shiftDeleteMessage"), + () => commandController.isCommandEnabled("cmd_shiftDeleteMessage") +); +commandController.registerCallback("cmd_find", () => + document.getElementById("FindToolbar").onFindCommand() +); +commandController.registerCallback("cmd_findAgain", () => + document.getElementById("FindToolbar").onFindAgainCommand(false) +); +commandController.registerCallback("cmd_findPrevious", () => + document.getElementById("FindToolbar").onFindAgainCommand(true) +); +commandController.registerCallback("cmd_print", () => { + top.PrintUtils.startPrintWindow(getMessagePaneBrowser().browsingContext, {}); +}); +commandController.registerCallback("cmd_fullZoomReduce", () => { + top.ZoomManager.reduce(); +}); +commandController.registerCallback("cmd_fullZoomEnlarge", () => { + top.ZoomManager.enlarge(); +}); +commandController.registerCallback("cmd_fullZoomReset", () => { + top.ZoomManager.reset(); +}); +commandController.registerCallback("cmd_fullZoomToggle", () => { + top.ZoomManager.toggleZoom(); +}); + +// Attachments commands. +commandController.registerCallback( + "cmd_openAllAttachments", + () => HandleAllAttachments("open"), + () => AttachmentMenuController.someFilesAvailable() +); + +commandController.registerCallback( + "cmd_saveAllAttachments", + () => HandleAllAttachments("save"), + () => AttachmentMenuController.someFilesAvailable() +); + +commandController.registerCallback( + "cmd_detachAllAttachments", + () => HandleAllAttachments("detach"), + () => AttachmentMenuController.canDetachFiles() +); + +commandController.registerCallback( + "cmd_deleteAllAttachments", + () => HandleAllAttachments("delete"), + () => AttachmentMenuController.canDetachFiles() +); |