diff options
Diffstat (limited to 'comm/mail/base/content/mailCommands.js')
-rw-r--r-- | comm/mail/base/content/mailCommands.js | 667 |
1 files changed, 667 insertions, 0 deletions
diff --git a/comm/mail/base/content/mailCommands.js b/comm/mail/base/content/mailCommands.js new file mode 100644 index 0000000000..9c974202e5 --- /dev/null +++ b/comm/mail/base/content/mailCommands.js @@ -0,0 +1,667 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +/* import-globals-from utilityOverlay.js */ + +/* globals msgWindow, messenger */ // From mailWindow.js +/* globals openComposeWindowForRSSArticle */ // From newsblogOverlay.js + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "FeedUtils", + "resource:///modules/FeedUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MailUtils", + "resource:///modules/MailUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "MsgHdrToMimeMessage", + "resource:///modules/gloda/MimeMessage.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "EnigmailMime", + "chrome://openpgp/content/modules/mime.jsm" +); + +function GetNextNMessages(folder) { + if (folder) { + var newsFolder = folder.QueryInterface(Ci.nsIMsgNewsFolder); + if (newsFolder) { + newsFolder.getNextNMessages(msgWindow); + } + } +} + +/** + * Figure out the message key from the message uri. + * + * @param uri string defining internal storage + */ +function GetMsgKeyFromURI(uri) { + // Format of 'uri' : protocol://email/folder#key?params + // '?params' are optional + // ex : mailbox-message://john%2Edoe@pop.isp.invalid/Drafts#12345 + // We keep only the part after '#' and before an optional '?'. + // The regexp expects 'key' to be an integer (a series of digits) : '\d+'. + let match = /.+#(\d+)/.exec(uri); + return match ? match[1] : null; +} + +/* eslint-disable complexity */ +/** + * Compose a message. + * + * @param {nsIMsgCompType} type - Type of composition (new message, reply, draft, etc.) + * @param {nsIMsgCompFormat} format - Requested format (plain text, html, default) + * @param {nsIMsgFolder} folder - Folder where the original message is stored + * @param {string[]} messageArray - Array of message URIs to process, often only + * holding one element. + * @param {Selection} [selection=null] - A DOM selection to be quoted, or null + * to quote the whole message, if quoting is appropriate (e.g. in a reply). + * @param {boolean} [autodetectCharset=false] - If quoting the whole message, + * whether automatic character set detection should be used. + */ +async function ComposeMessage( + type, + format, + folder, + messageArray, + selection = null, + autodetectCharset = false +) { + let aboutMessage = + document.getElementById("tabmail")?.currentAboutMessage || + document.getElementById("messageBrowser")?.contentWindow; + let currentHeaderData = aboutMessage?.currentHeaderData; + + function isCurrentlyDisplayed(hdr) { + return ( + currentHeaderData && // ignoring enclosing brackets: + currentHeaderData["message-id"]?.headerValue.includes(hdr.messageId) + ); + } + + function findDeliveredToIdentityEmail(hdr) { + // This function reads from currentHeaderData, which is only useful if we're + // looking at the currently-displayed message. Otherwise, just return + // immediately so we don't waste time. + if (!isCurrentlyDisplayed(hdr)) { + return ""; + } + + // Get the delivered-to headers. + let key = "delivered-to"; + let deliveredTos = []; + let index = 0; + let header = ""; + while ((header = currentHeaderData[key])) { + deliveredTos.push(header.headerValue.toLowerCase().trim()); + key = "delivered-to" + index++; + } + + // Reverse the array so that the last delivered-to header will show at front. + deliveredTos.reverse(); + + for (let i = 0; i < deliveredTos.length; i++) { + for (let identity of MailServices.accounts.allIdentities) { + if (!identity.email) { + continue; + } + // If the deliver-to header contains the defined identity, that's it. + if ( + deliveredTos[i] == identity.email.toLowerCase() || + deliveredTos[i].includes("<" + identity.email.toLowerCase() + ">") + ) { + return identity.email; + } + } + } + return ""; + } + + let msgKey; + if (messageArray && messageArray.length == 1) { + msgKey = GetMsgKeyFromURI(messageArray[0]); + } + + // Check if the draft is already open in another window. If it is, just focus the window. + if (type == Ci.nsIMsgCompType.Draft && messageArray.length == 1) { + // We'll search this uri in the opened windows. + for (let win of Services.wm.getEnumerator("")) { + // Check if it is a compose window. + if ( + win.document.defaultView.gMsgCompose && + win.document.defaultView.gMsgCompose.compFields.draftId + ) { + let wKey = GetMsgKeyFromURI( + win.document.defaultView.gMsgCompose.compFields.draftId + ); + if (wKey == msgKey) { + // Found ! just focus it... + win.focus(); + // ...and nothing to do anymore. + return; + } + } + } + } + var identity = null; + var newsgroup = null; + var hdr; + + // dump("ComposeMessage folder=" + folder + "\n"); + try { + if (folder) { + // Get the incoming server associated with this uri. + var server = folder.server; + + // If they hit new or reply and they are reading a newsgroup, + // turn this into a new post or a reply to group. + if ( + !folder.isServer && + server.type == "nntp" && + type == Ci.nsIMsgCompType.New + ) { + type = Ci.nsIMsgCompType.NewsPost; + newsgroup = folder.folderURL; + } + + identity = folder.customIdentity; + if (!identity) { + [identity] = MailUtils.getIdentityForServer(server); + } + // dump("identity = " + identity + "\n"); + } + } catch (ex) { + dump("failed to get an identity to pre-select: " + ex + "\n"); + } + + // dump("\nComposeMessage from XUL: " + identity + "\n"); + + switch (type) { + case Ci.nsIMsgCompType.New: // new message + // dump("OpenComposeWindow with " + identity + "\n"); + + MailServices.compose.OpenComposeWindow( + null, + null, + null, + type, + format, + identity, + null, + msgWindow + ); + return; + case Ci.nsIMsgCompType.NewsPost: + // dump("OpenComposeWindow with " + identity + " and " + newsgroup + "\n"); + MailServices.compose.OpenComposeWindow( + null, + null, + newsgroup, + type, + format, + identity, + null, + msgWindow + ); + return; + case Ci.nsIMsgCompType.ForwardAsAttachment: + if (messageArray && messageArray.length) { + // If we have more than one ForwardAsAttachment then pass null instead + // of the header to tell the compose service to work out the attachment + // subjects from the URIs. + hdr = + messageArray.length > 1 + ? null + : messenger.msgHdrFromURI(messageArray[0]); + MailServices.compose.OpenComposeWindow( + null, + hdr, + messageArray.join(","), + type, + format, + identity, + null, + msgWindow + ); + } + return; + default: + if (!messageArray) { + return; + } + + // Limit the number of new compose windows to 8. Why 8 ? + // I like that number :-) + if (messageArray.length > 8) { + messageArray.length = 8; + } + + for (var i = 0; i < messageArray.length; ++i) { + var messageUri = messageArray[i]; + hdr = messenger.msgHdrFromURI(messageUri); + + if ( + [ + Ci.nsIMsgCompType.Reply, + Ci.nsIMsgCompType.ReplyAll, + Ci.nsIMsgCompType.ReplyToSender, + // Author's address doesn't matter for followup to a newsgroup. + // Ci.nsIMsgCompType.ReplyToGroup, + Ci.nsIMsgCompType.ReplyToSenderAndGroup, + Ci.nsIMsgCompType.ReplyWithTemplate, + Ci.nsIMsgCompType.ReplyToList, + ].includes(type) + ) { + let replyTo = hdr.getStringProperty("replyTo"); + let from = replyTo || hdr.author; + let fromAddrs = MailServices.headerParser.parseEncodedHeader( + from, + null + ); + let email = fromAddrs[0]?.email; + if ( + type == Ci.nsIMsgCompType.ReplyToList && + isCurrentlyDisplayed(hdr) + ) { + // ReplyToList is only enabled for current message (if at all), so + // using currentHeaderData is ok. + // List-Post value is of the format <mailto:list@example.com> + let listPost = currentHeaderData["list-post"]?.headerValue; + if (listPost) { + email = listPost.replace(/.*<mailto:(.+)>.*/, "$1"); + } + } + + if ( + /^(.*[._-])?(do[._-]?not|no)[._-]?reply([._-].*)?@/i.test(email) + ) { + let [title, message, replyAnywayButton] = + await document.l10n.formatValues([ + { id: "no-reply-title" }, + { id: "no-reply-message", args: { email } }, + { id: "no-reply-reply-anyway-button" }, + ]); + + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1 + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + + if ( + Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + replyAnywayButton, + null, // cancel + null, + null, + {} + ) + ) { + continue; + } + } + } + + if (FeedUtils.isFeedMessage(hdr)) { + // Do not use the header derived identity for feeds, pass on only a + // possible server identity from above. + openComposeWindowForRSSArticle( + null, + hdr, + messageUri, + type, + format, + identity, + msgWindow + ); + } else { + // Replies come here. + + let useCatchAll = false; + // Check if we are using catchAll on any identity. If current + // folder has some customIdentity set, ignore catchAll settings. + // CatchAll is not applicable to news (and doesn't work, bug 545365). + if ( + hdr.folder && + hdr.folder.server.type != "nntp" && + !hdr.folder.customIdentity + ) { + useCatchAll = MailServices.accounts.allIdentities.some( + identity => identity.catchAll + ); + } + + if (useCatchAll) { + // If we use catchAll, we need to get all headers. + // MsgHdr retrieval is asynchronous, do everything in the callback. + MsgHdrToMimeMessage( + hdr, + null, + function (hdr, mimeMsg) { + let catchAllHeaders = Services.prefs + .getStringPref("mail.compose.catchAllHeaders") + .split(",") + .map(header => header.toLowerCase().trim()); + // Collect catchAll hints from given headers. + let collectedHeaderAddresses = ""; + for (let header of catchAllHeaders) { + if (mimeMsg.has(header)) { + for (let mimeMsgHeader of mimeMsg.headers[header]) { + collectedHeaderAddresses += + MailServices.headerParser + .parseEncodedHeaderW(mimeMsgHeader) + .toString() + ","; + } + } + } + + let [identity, matchingHint] = MailUtils.getIdentityForHeader( + hdr, + type, + collectedHeaderAddresses + ); + + // The found identity might have no catchAll enabled. + if (identity.catchAll && matchingHint) { + // If name is not set in matchingHint, search trough other hints. + if (matchingHint.email && !matchingHint.name) { + let hints = + MailServices.headerParser.makeFromDisplayAddress( + hdr.recipients + + "," + + hdr.ccList + + "," + + collectedHeaderAddresses + ); + for (let hint of hints) { + if ( + hint.name && + hint.email.toLowerCase() == + matchingHint.email.toLowerCase() + ) { + matchingHint = + MailServices.headerParser.makeMailboxObject( + hint.name, + matchingHint.email + ); + break; + } + } + } + } else { + matchingHint = MailServices.headerParser.makeMailboxObject( + "", + "" + ); + } + + // Now open compose window and use matching hint as reply sender. + MailServices.compose.OpenComposeWindow( + null, + hdr, + messageUri, + type, + format, + identity, + matchingHint.toString(), + msgWindow, + selection, + autodetectCharset + ); + }, + true, + { saneBodySize: true } + ); + } else { + // Fall back to traditional behavior. + let [hdrIdentity] = MailUtils.getIdentityForHeader( + hdr, + type, + findDeliveredToIdentityEmail(hdr) + ); + MailServices.compose.OpenComposeWindow( + null, + hdr, + messageUri, + type, + format, + hdrIdentity, + null, + msgWindow, + selection, + autodetectCharset + ); + } + } + } + } +} +/* eslint-enable complexity */ + +function Subscribe(preselectedMsgFolder) { + window.openDialog( + "chrome://messenger/content/subscribe.xhtml", + "subscribe", + "chrome,modal,titlebar,resizable=yes", + { + folder: preselectedMsgFolder, + okCallback: SubscribeOKCallback, + } + ); +} + +function SubscribeOKCallback(changeTable) { + for (var serverURI in changeTable) { + var folder = MailUtils.getExistingFolder(serverURI); + var server = folder.server; + var subscribableServer = server.QueryInterface(Ci.nsISubscribableServer); + + for (var name in changeTable[serverURI]) { + if (changeTable[serverURI][name]) { + try { + subscribableServer.subscribe(name); + } catch (ex) { + dump("failed to subscribe to " + name + ": " + ex + "\n"); + } + } else if (!changeTable[serverURI][name]) { + try { + subscribableServer.unsubscribe(name); + } catch (ex) { + dump("failed to unsubscribe to " + name + ": " + ex + "\n"); + } + } + } + + try { + subscribableServer.commitSubscribeChanges(); + } catch (ex) { + dump("failed to commit the changes: " + ex + "\n"); + } + } +} + +function SaveAsFile(uris) { + let filenames = []; + + for (let uri of uris) { + let msgHdr = + MailServices.messageServiceFromURI(uri).messageURIToMsgHdr(uri); + let nameBase = GenerateFilenameFromMsgHdr(msgHdr); + let name = GenerateValidFilename(nameBase, ".eml"); + + let number = 2; + while (filenames.includes(name)) { + // should be unlikely + name = GenerateValidFilename(nameBase + "-" + number, ".eml"); + number++; + } + filenames.push(name); + } + + if (uris.length == 1) { + messenger.saveAs(uris[0], true, null, filenames[0]); + } else { + messenger.saveMessages(filenames, uris); + } +} + +function GenerateFilenameFromMsgHdr(msgHdr) { + function MakeIS8601ODateString(date) { + function pad(n) { + return n < 10 ? "0" + n : n; + } + return ( + date.getFullYear() + + "-" + + pad(date.getMonth() + 1) + + "-" + + pad(date.getDate()) + + " " + + pad(date.getHours()) + + "" + + pad(date.getMinutes()) + + "" + ); + } + + let filename; + if (msgHdr.flags & Ci.nsMsgMessageFlags.HasRe) { + filename = msgHdr.mime2DecodedSubject + ? "Re: " + msgHdr.mime2DecodedSubject + : "Re: "; + } else { + filename = msgHdr.mime2DecodedSubject; + } + + filename += " - "; + filename += msgHdr.mime2DecodedAuthor + " - "; + filename += MakeIS8601ODateString(new Date(msgHdr.date / 1000)); + + return filename; +} + +function saveAsUrlListener(aUri, aIdentity) { + this.uri = aUri; + this.identity = aIdentity; +} + +saveAsUrlListener.prototype = { + OnStartRunningUrl(aUrl) {}, + OnStopRunningUrl(aUrl, aExitCode) { + messenger.saveAs(this.uri, false, this.identity, null); + }, +}; + +function SaveAsTemplate(uri) { + if (uri) { + let hdr = messenger.msgHdrFromURI(uri); + let [identity] = MailUtils.getIdentityForHeader( + hdr, + Ci.nsIMsgCompType.Template + ); + let templates = MailUtils.getOrCreateFolder(identity.stationeryFolder); + if (!templates.parent) { + templates.setFlag(Ci.nsMsgFolderFlags.Templates); + let isAsync = templates.server.protocolInfo.foldersCreatedAsync; + templates.createStorageIfMissing(new saveAsUrlListener(uri, identity)); + if (isAsync) { + return; + } + } + messenger.saveAs(uri, false, identity, null); + } +} + +function viewEncryptedPart(message) { + let url; + try { + url = MailServices.mailSession.ConvertMsgURIToMsgURL(message, msgWindow); + } catch (e) { + console.debug(e); + // Couldn't get mail session + return false; + } + + // 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&/, ""); + + /** + * Save the given string to a file, then open it as an .eml file. + * + * @param {string} data - The message data. + */ + let msgOpenMessageFromString = function (data) { + let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + tempFile.append("subPart.eml"); + tempFile.createUnique(0, 0o600); + + let outputStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outputStream.init(tempFile, 2, 0x200, false); // open as "write only" + outputStream.write(data, data.length); + outputStream.close(); + + // Delete file on exit, because Windows locks the file + let extAppLauncher = Cc[ + "@mozilla.org/uriloader/external-helper-app-service;1" + ].getService(Ci.nsPIExternalAppLauncher); + extAppLauncher.deleteTemporaryFileOnExit(tempFile); + + let url = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .newFileURI(tempFile); + + MailUtils.openEMLFile(window, tempFile, url); + }; + + function recursiveEmitEncryptedParts(mimeTree) { + for (let part of mimeTree.subParts) { + const ct = part.headers.contentType.type; + if (ct == "multipart/encrypted") { + const boundary = part.headers.contentType.get("boundary"); + let full = `${part.headers.rawHeaderText}\n\n`; + for (let subPart of part.subParts) { + full += `${boundary}\n${subPart.headers.rawHeaderText}\n\n${subPart.body}\n`; + } + full += `${boundary}--\n`; + msgOpenMessageFromString(full); + continue; + } + recursiveEmitEncryptedParts(part); + } + } + + EnigmailMime.getMimeTreeFromUrl(url, true, recursiveEmitEncryptedParts); + return true; +} + +function viewEncryptedParts(messages) { + if (!messages?.length) { + dump("viewEncryptedParts(): No messages selected.\n"); + return false; + } + + if (messages.length > 1) { + dump("viewEncryptedParts(): Too many messages selected.\n"); + return false; + } + + return viewEncryptedPart(messages[0]); +} |