diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/compose/src/MessageSend.jsm | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/compose/src/MessageSend.jsm | 1434 |
1 files changed, 1434 insertions, 0 deletions
diff --git a/comm/mailnews/compose/src/MessageSend.jsm b/comm/mailnews/compose/src/MessageSend.jsm new file mode 100644 index 0000000000..914694fa79 --- /dev/null +++ b/comm/mailnews/compose/src/MessageSend.jsm @@ -0,0 +1,1434 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["MessageSend"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + MailUtils: "resource:///modules/MailUtils.jsm", + jsmime: "resource:///modules/jsmime.jsm", + MimeMessage: "resource:///modules/MimeMessage.jsm", + MsgUtils: "resource:///modules/MimeMessageUtils.jsm", +}); + +// nsMsgKey_None from MailNewsTypes.h. +const nsMsgKey_None = 0xffffffff; + +/** + * A class to manage sending processes. + * + * @implements {nsIMsgSend} + * @implements {nsIWebProgressListener} + */ +class MessageSend { + QueryInterface = ChromeUtils.generateQI([ + "nsIMsgSend", + "nsIWebProgressListener", + ]); + classID = Components.ID("{028b9c1e-8d0a-4518-80c2-842e07846eaa}"); + + async createAndSendMessage( + editor, + userIdentity, + accountKey, + compFields, + isDigest, + dontDeliver, + deliverMode, + msgToReplace, + bodyType, + body, + parentWindow, + progress, + listener, + smtpPassword, + originalMsgURI, + compType + ) { + this._userIdentity = userIdentity; + this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity); + this._compFields = compFields; + this._dontDeliver = dontDeliver; + this._deliverMode = deliverMode; + this._msgToReplace = msgToReplace; + this._sendProgress = progress; + this._smtpPassword = smtpPassword; + this._sendListener = listener; + this._parentWindow = parentWindow; + this._originalMsgURI = originalMsgURI; + this._shouldRemoveMessageFile = true; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + this.sendReport.deliveryMode = deliverMode; + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMailInformation") + ); + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessage") + ); + + this._fcc = lazy.MsgUtils.getFcc( + userIdentity, + compFields, + originalMsgURI, + compType + ); + let { embeddedAttachments, embeddedObjects } = + this._gatherEmbeddedAttachments(editor); + + let bodyText = this._getBodyFromEditor(editor) || body; + // Convert to a binary string. This is because MimeMessage requires it and: + // 1. An attachment content is BinaryString. + // 2. Body text and attachment contents are handled in the same way by + // MimeEncoder to pick encoding and encode. + bodyText = lazy.jsmime.mimeutils.typedArrayToString( + new TextEncoder().encode(bodyText) + ); + + this._restoreEditorContent(embeddedObjects); + this._message = new lazy.MimeMessage( + userIdentity, + compFields, + this._fcc, + bodyType, + bodyText, + deliverMode, + originalMsgURI, + compType, + embeddedAttachments, + this.sendReport + ); + + this._messageKey = nsMsgKey_None; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("creatingMailMessage") + ); + lazy.MsgUtils.sendLogger.debug("Creating message file"); + let messageFile; + try { + // Create a local file from MimeMessage, then pass it to _deliverMessage. + messageFile = await this._message.createMessageFile(); + } catch (e) { + lazy.MsgUtils.sendLogger.error(e); + let errorMsg = ""; + if (e.result == lazy.MsgUtils.NS_MSG_ERROR_ATTACHING_FILE) { + errorMsg = this._composeBundle.formatStringFromName( + "errorAttachingFile", + [e.data.name || e.data.url] + ); + } + this.fail(e.result || Cr.NS_ERROR_FAILURE, errorMsg); + this.notifyListenerOnStopSending(null, e.result, null, null); + return null; + } + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessageDone") + ); + lazy.MsgUtils.sendLogger.debug("Message file created"); + return this._deliverMessage(messageFile); + } + + sendMessageFile( + userIdentity, + accountKey, + compFields, + messageFile, + deleteSendFileOnCompletion, + digest, + deliverMode, + msgToReplace, + listener, + statusFeedback, + smtpPassword + ) { + this._userIdentity = userIdentity; + this._accountKey = accountKey || this._accountKeyForIdentity(userIdentity); + this._compFields = compFields; + this._deliverMode = deliverMode; + this._msgToReplace = msgToReplace; + this._smtpPassword = smtpPassword; + this._sendListener = listener; + this._statusFeedback = statusFeedback; + this._shouldRemoveMessageFile = deleteSendFileOnCompletion; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + this.sendReport.deliveryMode = deliverMode; + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMailInformation") + ); + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_BuildMessage; + + this._setStatusMessage( + this._composeBundle.GetStringFromName("assemblingMessage") + ); + + this._fcc = lazy.MsgUtils.getFcc( + userIdentity, + compFields, + null, + Ci.nsIMsgCompType.New + ); + + // nsMsgKey_None from MailNewsTypes.h. + this._messageKey = 0xffffffff; + + return this._deliverMessage(messageFile); + } + + // @see nsIMsgSend + createRFC822Message( + userIdentity, + compFields, + bodyType, + bodyText, + isDraft, + attachedFiles, + embeddedObjects, + listener + ) { + this._userIdentity = userIdentity; + this._compFields = compFields; + this._dontDeliver = true; + this._sendListener = listener; + + this._sendReport = Cc[ + "@mozilla.org/messengercompose/sendreport;1" + ].createInstance(Ci.nsIMsgSendReport); + this._composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + + // Initialize the error reporting mechanism. + this.sendReport.reset(); + let deliverMode = isDraft + ? Ci.nsIMsgSend.nsMsgSaveAsDraft + : Ci.nsIMsgSend.nsMsgDeliverNow; + this.sendReport.deliveryMode = deliverMode; + + // Convert nsIMsgAttachedFile[] to nsIMsgAttachment[] + for (let file of attachedFiles) { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = file.realName; + attachment.url = file.origUrl.spec; + attachment.contentType = file.type; + compFields.addAttachment(attachment); + } + + // Convert nsIMsgEmbeddedImageData[] to nsIMsgAttachment[] + let embeddedAttachments = embeddedObjects.map(obj => { + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = obj.name; + attachment.contentId = obj.cid; + attachment.url = obj.uri.spec; + return attachment; + }); + + this._message = new lazy.MimeMessage( + userIdentity, + compFields, + null, + bodyType, + bodyText, + deliverMode, + null, + Ci.nsIMsgCompType.New, + embeddedAttachments, + this.sendReport + ); + + this._messageKey = nsMsgKey_None; + + // Create a local file from MimeMessage, then pass it to _deliverMessage. + this._message + .createMessageFile() + .then(messageFile => this._deliverMessage(messageFile)); + } + + // nsIWebProgressListener. + onLocationChange(webProgress, request, location, flags) {} + onProgressChange( + webProgress, + request, + curSelfProgress, + maxSelfProgress, + curTotalProgress, + maxTotalProgress + ) {} + onStatusChange(webProgress, request, status, message) {} + onSecurityChange(webProgress, request, state) {} + onContentBlockingEvent(webProgress, request, event) {} + onStateChange(webProgress, request, stateFlags, status) { + if ( + stateFlags & Ci.nsIWebProgressListener.STATE_STOP && + !Components.isSuccessCode(status) + ) { + lazy.MsgUtils.sendLogger.debug("onStateChange with failure. Aborting."); + this._isRetry = false; + this.abort(); + } + } + + abort() { + if (this._aborting) { + return; + } + this._aborting = true; + if (this._smtpRequest?.value) { + this._smtpRequest.value.cancel(Cr.NS_ERROR_ABORT); + this._smtpRequest = null; + } + if (this._msgCopy) { + MailServices.copy.notifyCompletion( + this._copyFile, + this._msgCopy.dstFolder, + Cr.NS_ERROR_ABORT + ); + } else { + // If already in the fcc step, notifyListenerOnStopCopy will do the clean up. + this._cleanup(); + } + if (!this._failed) { + // Emit stopsending event if the sending is cancelled by user, so that + // listeners can do necessary clean up, e.g. reset the sending button. + this.notifyListenerOnStopSending(null, Cr.NS_ERROR_ABORT, null, null); + } + this._aborting = false; + } + + fail(exitCode, errorMsg) { + this._failed = true; + if (!Components.isSuccessCode(exitCode) && exitCode != Cr.NS_ERROR_ABORT) { + lazy.MsgUtils.sendLogger.error( + `Sending failed; ${errorMsg}, exitCode=${exitCode}, originalMsgURI=${this._originalMsgURI}` + ); + this._sendReport.setError( + Ci.nsIMsgSendReport.process_Current, + exitCode, + false + ); + if (errorMsg) { + this._sendReport.setMessage( + Ci.nsIMsgSendReport.process_Current, + errorMsg, + false + ); + } + exitCode = this._sendReport.displayReport(this._parentWindow, true, true); + } + this.abort(); + + return exitCode; + } + + getPartForDomIndex(domIndex) { + throw Components.Exception( + "getPartForDomIndex not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + getProgress() { + return this._sendProgress; + } + + /** + * NOTE: This is a copy of the C++ code, msgId and msgSize are only + * placeholders. Maybe refactor this after nsMsgSend is gone. + */ + notifyListenerOnStartSending(msgId, msgSize) { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartSending"); + if (this._sendListener) { + this._sendListener.onStartSending(msgId, msgSize); + } + } + + notifyListenerOnStartCopy() { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnStartCopy"); + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnStartCopy(); + } + } + + notifyListenerOnProgressCopy(progress, progressMax) { + lazy.MsgUtils.sendLogger.debug("notifyListenerOnProgressCopy"); + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnProgress(progress, progressMax); + } + } + + notifyListenerOnStopCopy(status) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnStopCopy; status=${status}` + ); + this._msgCopy = null; + + if (!this._isRetry) { + let statusMsgEntry = Components.isSuccessCode(status) + ? "copyMessageComplete" + : "copyMessageFailed"; + this._setStatusMessage( + this._composeBundle.GetStringFromName(statusMsgEntry) + ); + } else if (Components.isSuccessCode(status)) { + // We got here via retry and the save to sent, drafts or template + // succeeded so take down our progress dialog. We don't need it any more. + this._sendProgress.unregisterListener(this); + this._sendProgress.closeProgressDialog(false); + this._isRetry = false; + } + + if (!Components.isSuccessCode(status)) { + let localFoldersAccountName = + MailServices.accounts.localFoldersServer.prettyName; + let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + let accountName = folder?.server.prettyName; + if (!this._fcc || !localFoldersAccountName || !accountName) { + this.fail(Cr.NS_OK, null); + return; + } + + let params = [folder.name, accountName, localFoldersAccountName]; + let promptMsg; + switch (this._deliverMode) { + case Ci.nsIMsgSend.nsMsgDeliverNow: + case Ci.nsIMsgSend.nsMsgSendUnsent: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveSentLocally2", + params + ); + break; + case Ci.nsIMsgSend.nsMsgSaveAsDraft: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveDraftLocally2", + params + ); + break; + case Ci.nsIMsgSend.nsMsgSaveAsTemplate: + promptMsg = this._composeBundle.formatStringFromName( + "promptToSaveTemplateLocally2", + params + ); + break; + } + if (promptMsg) { + let showCheckBox = { value: false }; + let buttonFlags = + Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING + + Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE + + Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_SAVE; + let dialogTitle = + this._composeBundle.GetStringFromName("SaveDialogTitle"); + let buttonLabelRety = + this._composeBundle.GetStringFromName("buttonLabelRetry2"); + let buttonPressed = Services.prompt.confirmEx( + this._parentWindow, + dialogTitle, + promptMsg, + buttonFlags, + buttonLabelRety, + null, + null, + null, + showCheckBox + ); + if (buttonPressed == 0) { + // retry button clicked + // Check we have a progress dialog. + if ( + this._sendProgress.processCanceledByUser && + Services.prefs.getBoolPref("mailnews.show_send_progress") + ) { + let progress = Cc[ + "@mozilla.org/messenger/progress;1" + ].createInstance(Ci.nsIMsgProgress); + + let params = Cc[ + "@mozilla.org/messengercompose/composeprogressparameters;1" + ].createInstance(Ci.nsIMsgComposeProgressParams); + params.subject = this._parentWindow.gMsgCompose.compFields.subject; + params.deliveryMode = this._deliverMode; + + progress.openProgressDialog( + this._parentWindow, + this._sendProgress.msgWindow, + "chrome://messenger/content/messengercompose/sendProgress.xhtml", + false, + params + ); + + progress.onStateChange( + null, + null, + Ci.nsIWebProgressListener.STATE_START, + Cr.NS_OK + ); + + // We want to hear when this is cancelled. + progress.registerListener(this); + + this._sendProgress = progress; + this._isRetry = true; + } + // Ensure statusFeedback is set so progress percent bargraph occurs. + this._sendProgress.msgWindow.statusFeedback = this._sendProgress; + + this._mimeDoFcc(); + return; + } else if (buttonPressed == 2) { + try { + // Try to save to Local Folders/<account name>. Pass null to save + // to local folders and not the configured fcc. + this._mimeDoFcc(null, true, Ci.nsIMsgSend.nsMsgDeliverNow); + return; + } catch (e) { + Services.prompt.alert( + this._parentWindow, + null, + this._composeBundle.GetStringFromName("saveToLocalFoldersFailed") + ); + } + } + } + this.fail(Cr.NS_OK, null); + } + + if ( + !this._fcc2Handled && + this._messageKey != nsMsgKey_None && + [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes( + this._deliverMode + ) + ) { + try { + this._filterSentMessage(); + } catch (e) { + this.onStopOperation(e.result); + } + return; + } + + this._doFcc2(); + } + + notifyListenerOnStopSending(msgId, status, msg, returnFile) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnStopSending; status=${status}` + ); + try { + this._sendListener?.onStopSending(msgId, status, msg, returnFile); + } catch (e) {} + } + + notifyListenerOnTransportSecurityError(msgId, status, secInfo, location) { + lazy.MsgUtils.sendLogger.debug( + `notifyListenerOnTransportSecurityError; status=${status}, location=${location}` + ); + if (!this._sendListener) { + return; + } + try { + this._sendListener.onTransportSecurityError( + msgId, + status, + secInfo, + location + ); + } catch (e) {} + } + + /** + * Called by nsIMsgFilterService. + */ + onStopOperation(status) { + lazy.MsgUtils.sendLogger.debug(`onStopOperation; status=${status}`); + if (Components.isSuccessCode(status)) { + this._setStatusMessage( + this._composeBundle.GetStringFromName("filterMessageComplete") + ); + } else { + this._setStatusMessage( + this._composeBundle.GetStringFromName("filterMessageFailed") + ); + Services.prompt.alert( + this._parentWindow, + null, + this._composeBundle.GetStringFromName("errorFilteringMsg") + ); + } + + this._doFcc2(); + } + + /** + * Handle the exit code of message delivery. + * + * @param {nsIURI} url - The delivered message uri. + * @param {boolean} isNewsDelivery - The message was delivered to newsgroup. + * @param {nsreault} exitCode - The exit code of message delivery. + */ + _deliveryExitProcessing(url, isNewsDelivery, exitCode) { + lazy.MsgUtils.sendLogger.debug( + `Delivery exit processing; exitCode=${exitCode}` + ); + if (!Components.isSuccessCode(exitCode)) { + let isNSSError = false; + let errorName = lazy.MsgUtils.getErrorStringName(exitCode); + let errorMsg; + if ( + [ + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED, + lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT, + lazy.MsgUtils.NS_ERROR_SMTP_PASSWORD_UNDEFINED, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_FAILURE, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_GSSAPI, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_NO_SSL, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL, + lazy.MsgUtils.NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT, + lazy.MsgUtils.NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS, + ].includes(exitCode) + ) { + errorMsg = lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + errorName + ); + } else { + let nssErrorsService = Cc[ + "@mozilla.org/nss_errors_service;1" + ].getService(Ci.nsINSSErrorsService); + try { + // This is a server security issue as determined by the Mozilla + // platform. To the Mozilla security message string, appended a string + // having additional information with the server name encoded. + errorMsg = nssErrorsService.getErrorMessage(exitCode); + errorMsg += + "\n" + + lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + "smtpSecurityIssue" + ); + isNSSError = true; + } catch (e) { + if (url.errorMessage) { + // url.errorMessage is an already localized message, usually + // combined with the error message from SMTP server. + errorMsg = url.errorMessage; + } else if (errorName != "sendFailed") { + // Not the default string. A mailnews error occurred that does not + // require the server name to be encoded. Just print the descriptive + // string. + errorMsg = this._composeBundle.GetStringFromName(errorName); + } else { + errorMsg = this._composeBundle.GetStringFromName( + "sendFailedUnexpected" + ); + // nsIStringBundle.formatStringFromName doesn't work with %X. + errorMsg.replace("%X", `0x${exitCode.toString(16)}`); + errorMsg = + "\n" + + lazy.MsgUtils.formatStringWithSMTPHostName( + this._userIdentity, + this._composeBundle, + "smtpSendFailedUnknownReason" + ); + } + } + } + if (isNSSError) { + let u = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + this.notifyListenerOnTransportSecurityError( + null, + exitCode, + u.failedSecInfo, + u.asciiHostPort + ); + } + this.notifyListenerOnStopSending(null, exitCode, null, null); + this.fail(exitCode, errorMsg); + return; + } + + if ( + isNewsDelivery && + (this._compFields.to || this._compFields.cc || this._compFields.bcc) + ) { + this._deliverAsMail(); + return; + } + + this.notifyListenerOnStopSending( + this._compFields.messageId, + exitCode, + null, + null + ); + + this._doFcc(); + } + + sendDeliveryCallback(url, isNewsDelivery, exitCode) { + if (isNewsDelivery) { + if ( + !Components.isSuccessCode(exitCode) && + exitCode != Cr.NS_ERROR_ABORT && + !lazy.MsgUtils.isMsgError(exitCode) + ) { + exitCode = lazy.MsgUtils.NS_ERROR_POST_FAILED; + } + return this._deliveryExitProcessing(url, isNewsDelivery, exitCode); + } + if (!Components.isSuccessCode(exitCode)) { + switch (exitCode) { + case Cr.NS_ERROR_UNKNOWN_HOST: + case Cr.NS_ERROR_UNKNOWN_PROXY_HOST: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_UNKNOWN_SERVER; + break; + case Cr.NS_ERROR_CONNECTION_REFUSED: + case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_REFUSED; + break; + case Cr.NS_ERROR_NET_INTERRUPT: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_INTERRUPTED; + break; + case Cr.NS_ERROR_NET_TIMEOUT: + case Cr.NS_ERROR_NET_RESET: + exitCode = lazy.MsgUtils.NS_ERROR_SMTP_SEND_FAILED_TIMEOUT; + break; + default: + break; + } + } + return this._deliveryExitProcessing(url, isNewsDelivery, exitCode); + } + + get folderUri() { + return this._folderUri; + } + + /** + * @type {nsMsgKey} + */ + set messageKey(key) { + this._messageKey = key; + } + + /** + * @type {nsMsgKey} + */ + get messageKey() { + return this._messageKey; + } + + get sendReport() { + return this._sendReport; + } + + _setStatusMessage(msg) { + if (this._sendProgress) { + this._sendProgress.onStatusChange(null, null, Cr.NS_OK, msg); + } + } + + /** + * Deliver a message. + * + * @param {nsIFile} file - The message file to deliver. + */ + async _deliverMessage(file) { + if (this._dontDeliver) { + this.notifyListenerOnStopSending(null, Cr.NS_OK, null, file); + return; + } + + this._messageFile = file; + if ( + [ + Ci.nsIMsgSend.nsMsgQueueForLater, + Ci.nsIMsgSend.nsMsgDeliverBackground, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(this._deliverMode) + ) { + await this._mimeDoFcc(); + return; + } + + let warningSize = Services.prefs.getIntPref( + "mailnews.message_warning_size" + ); + if (warningSize > 0 && file.fileSize > warningSize) { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + let msg = this._composeBundle.formatStringFromName( + "largeMessageSendWarning", + [messenger.formatFileSize(file.fileSize)] + ); + if (!Services.prompt.confirm(this._parentWindow, null, msg)) { + this.fail(lazy.MsgUtils.NS_ERROR_BUT_DONT_SHOW_ALERT, msg); + throw Components.Exception( + "Cancelled sending large message", + Cr.NS_ERROR_FAILURE + ); + } + } + + this._deliveryFile = await this._createDeliveryFile(); + if (this._compFields.newsgroups) { + this._deliverAsNews(); + return; + } + await this._deliverAsMail(); + } + + /** + * Strip Bcc header, create the file to be actually delivered. + * + * @returns {nsIFile} + */ + async _createDeliveryFile() { + if (!this._compFields.bcc) { + return this._messageFile; + } + let deliveryFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + deliveryFile.append("nsemail.tmp"); + deliveryFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + let content = await IOUtils.read(this._messageFile.path); + let bodyIndex = content.findIndex( + (el, index) => + // header and body are separated by \r\n\r\n + el == 13 && + content[index + 1] == 10 && + content[index + 2] == 13 && + content[index + 3] == 10 + ); + let header = new TextDecoder("UTF-8").decode(content.slice(0, bodyIndex)); + let lastLinePruned = false; + let headerToWrite = ""; + for (let line of header.split("\r\n")) { + if (line.startsWith("Bcc") || (line.startsWith(" ") && lastLinePruned)) { + lastLinePruned = true; + continue; + } + lastLinePruned = false; + headerToWrite += `${line}\r\n`; + } + let encodedHeader = new TextEncoder().encode(headerToWrite); + // Prevent extra \r\n, which was already added to the last head line. + let body = content.slice(bodyIndex + 2); + let combinedContent = new Uint8Array(encodedHeader.length + body.length); + combinedContent.set(encodedHeader); + combinedContent.set(body, encodedHeader.length); + await IOUtils.write(deliveryFile.path, combinedContent); + return deliveryFile; + } + + /** + * Create the file to be copied to the Sent folder, add X-Mozilla-Status and + * X-Mozilla-Status2 if needed. + * + * @returns {nsIFile} + */ + async _createCopyFile() { + if (!this._folderUri.startsWith("mailbox:")) { + return this._messageFile; + } + + // Add a `From - Date` line, so that nsLocalMailFolder.cpp won't add a + // dummy envelope. The date string will be parsed by PR_ParseTimeString. + // TODO: this should not be added to Maildir, see bug 1686852. + let contentToWrite = `From - ${new Date().toUTCString()}\r\n`; + let xMozillaStatus = lazy.MsgUtils.getXMozillaStatus(this._deliverMode); + let xMozillaStatus2 = lazy.MsgUtils.getXMozillaStatus2(this._deliverMode); + if (xMozillaStatus) { + contentToWrite += `X-Mozilla-Status: ${xMozillaStatus}\r\n`; + } + if (xMozillaStatus2) { + contentToWrite += `X-Mozilla-Status2: ${xMozillaStatus2}\r\n`; + } + + // Create a separate copy file when there are extra headers. + let copyFile = Services.dirsvc.get("TmpD", Ci.nsIFile); + copyFile.append("nscopy.tmp"); + copyFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + await IOUtils.writeUTF8(copyFile.path, contentToWrite); + await IOUtils.write( + copyFile.path, + await IOUtils.read(this._messageFile.path), + { + mode: "append", + } + ); + return copyFile; + } + + /** + * Start copy operation according to this._fcc value. + */ + async _doFcc() { + if (!this._fcc || !lazy.MsgUtils.canSaveToFolder(this._fcc)) { + this.notifyListenerOnStopCopy(Cr.NS_OK); + return; + } + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Copy; + this._mimeDoFcc(this._fcc, false, Ci.nsIMsgSend.nsMsgDeliverNow); + } + + /** + * Copy a message to a folder, or fallback to a folder depending on pref and + * deliverMode, usually Drafts/Sent. + * + * @param {string} [fccHeader=this._fcc] - The target folder uri to copy the + * message to. + * @param {boolean} [throwOnError=false] - By default notifyListenerOnStopCopy + * is called on error. When throwOnError is true, the caller can handle the + * error by itself. + * @param {nsMsgDeliverMode} [deliverMode=this._deliverMode] - The deliver mode. + */ + async _mimeDoFcc( + fccHeader = this._fcc, + throwOnError = false, + deliverMode = this._deliverMode + ) { + let folder; + let folderUri; + if (fccHeader) { + folder = lazy.MailUtils.getExistingFolder(fccHeader); + } + if ( + [Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes( + deliverMode + ) && + folder + ) { + this._folderUri = fccHeader; + } else if (fccHeader == null) { + // Set fcc_header to a special folder in Local Folders "account" since can't + // save to Sent mbox, typically because imap connection is down. This + // folder is created if it doesn't yet exist. + let rootFolder = MailServices.accounts.localFoldersServer.rootMsgFolder; + folderUri = rootFolder.URI + "/"; + + // Now append the special folder name folder to the local folder uri. + if ( + [ + Ci.nsIMsgSend.nsMsgDeliverNow, + Ci.nsIMsgSend.nsMsgSendUnsent, + Ci.nsIMsgSend.nsMsgSaveAsDraft, + Ci.nsIMsgSend.nsMsgSaveAsTemplate, + ].includes(this._deliverMode) + ) { + // Typically, this appends "Sent-", "Drafts-" or "Templates-" to folder + // and then has the account name appended, e.g., .../Sent-MyImapAccount. + let folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + folderUri += folder.name + "-"; + } + if (this._fcc) { + // Get the account name where the "save to" failed. + let accountName = lazy.MailUtils.getOrCreateFolder(this._fcc).server + .prettyName; + + // Now append the imap account name (escaped) to the folder uri. + folderUri += accountName; + this._folderUri = folderUri; + } + } else { + this._folderUri = lazy.MsgUtils.getMsgFolderURIFromPrefs( + this._userIdentity, + this._deliverMode + ); + if ( + (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft && + this._compFields.draftId) || + (this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsTemplate && + this._compFields.templateId) + ) { + // Turn the draft/template ID into a folder URI string. + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + try { + // This can fail if the user renames/removed/moved the folder. + folderUri = messenger.msgHdrFromURI( + this._deliverMode == Ci.nsIMsgSend.nsMsgSaveAsDraft + ? this._compFields.draftId + : this._compFields.templateId + ).folder.URI; + } catch (ex) { + console.warn(ex); + } + // Only accept it if it's a subfolder of the identity's draft/template folder. + if (folderUri?.startsWith(this._folderUri)) { + this._folderUri = folderUri; + } + } + } + lazy.MsgUtils.sendLogger.debug( + `Processing fcc; folderUri=${this._folderUri}` + ); + + this._msgCopy = Cc[ + "@mozilla.org/messengercompose/msgcopy;1" + ].createInstance(Ci.nsIMsgCopy); + this._copyFile = await this._createCopyFile(); + lazy.MsgUtils.sendLogger.debug("fcc file created"); + + // Notify nsMsgCompose about the saved folder. + if (this._sendListener) { + this._sendListener.onGetDraftFolderURI( + this._compFields.messageId, + this._folderUri + ); + } + folder = lazy.MailUtils.getOrCreateFolder(this._folderUri); + let statusMsg = this._composeBundle.formatStringFromName( + "copyMessageStart", + [folder?.name || "?"] + ); + this._setStatusMessage(statusMsg); + lazy.MsgUtils.sendLogger.debug("startCopyOperation"); + try { + this._msgCopy.startCopyOperation( + this._userIdentity, + this._copyFile, + this._deliverMode, + this, + this._folderUri, + this._msgToReplace + ); + } catch (e) { + lazy.MsgUtils.sendLogger.warn( + `startCopyOperation failed with ${e.result}` + ); + if (throwOnError) { + throw Components.Exception("startCopyOperation failed", e.result); + } + this.notifyListenerOnStopCopy(e.result); + } + } + + /** + * Handle the fcc2 field. Then notify OnStopCopy and clean up. + */ + _doFcc2() { + // Handle fcc2 only once. + if (!this._fcc2Handled && this._compFields.fcc2) { + lazy.MsgUtils.sendLogger.debug("Processing fcc2"); + this._fcc2Handled = true; + this._mimeDoFcc( + this._compFields.fcc2, + false, + Ci.nsIMsgSend.nsMsgDeliverNow + ); + return; + } + + // NOTE: When nsMsgCompose receives OnStopCopy, it will release nsIMsgSend + // instance and close the compose window, which prevents the Promise from + // resolving in MsgComposeCommands.js. Use setTimeout to work around it. + lazy.setTimeout(() => { + try { + if (this._sendListener instanceof Ci.nsIMsgCopyServiceListener) { + this._sendListener.OnStopCopy(0); + } + } catch (e) { + // Ignore the return value of OnStopCopy. Non-zero nsresult will throw + // when going through XPConnect. In this case, we don't care about it. + console.warn( + `OnStopCopy failed with 0x${e.result.toString(16)}\n${e.stack}` + ); + } + this._cleanup(); + }); + } + + /** + * Run filters on the just sent message. + */ + _filterSentMessage() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Filter; + let folder = lazy.MailUtils.getExistingFolder(this._folderUri); + let msgHdr = folder.GetMessageHeader(this._messageKey); + let msgWindow = this._sendProgress?.msgWindow; + return MailServices.filters.applyFilters( + Ci.nsMsgFilterType.PostOutgoing, + [msgHdr], + folder, + msgWindow, + this + ); + } + + _cleanup() { + lazy.MsgUtils.sendLogger.debug("Clean up temporary files"); + if (this._copyFile && this._copyFile != this._messageFile) { + IOUtils.remove(this._copyFile.path).catch(console.error); + this._copyFile = null; + } + if (this._deliveryFile && this._deliveryFile != this._messageFile) { + IOUtils.remove(this._deliveryFile.path).catch(console.error); + this._deliveryFile = null; + } + if (this._messageFile && this._shouldRemoveMessageFile) { + IOUtils.remove(this._messageFile.path).catch(console.error); + this._messageFile = null; + } + } + + /** + * Send this._deliveryFile to smtp service. + */ + async _deliverAsMail() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_SMTP; + this._setStatusMessage( + this._composeBundle.GetStringFromName("sendingMessage") + ); + let recipients = [ + this._compFields.to, + this._compFields.cc, + this._compFields.bcc, + ].filter(Boolean); + this._collectAddressesToAddressBook(recipients); + let converter = Cc["@mozilla.org/messenger/mimeconverter;1"].getService( + Ci.nsIMimeConverter + ); + let encodedRecipients = encodeURIComponent( + converter.encodeMimePartIIStr_UTF8( + recipients.join(","), + true, + 0, + Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE + ) + ); + lazy.MsgUtils.sendLogger.debug( + `Delivering mail message <${this._compFields.messageId}>` + ); + let deliveryListener = new MsgDeliveryListener(this, false); + let msgStatus = + this._sendProgress instanceof Ci.nsIMsgStatusFeedback + ? this._sendProgress + : this._statusFeedback; + this._smtpRequest = {}; + // Do async call. This is necessary to ensure _smtpRequest is set so that + // cancel function can be obtained. + await MailServices.smtp.wrappedJSObject.sendMailMessage( + this._deliveryFile, + encodedRecipients, + this._userIdentity, + this._compFields.from, + this._smtpPassword, + deliveryListener, + msgStatus, + null, + this._compFields.DSN, + this._compFields.messageId, + {}, + this._smtpRequest + ); + } + + /** + * Send this._deliveryFile to nntp service. + */ + _deliverAsNews() { + this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_NNTP; + lazy.MsgUtils.sendLogger.debug("Delivering news message"); + let deliveryListener = new MsgDeliveryListener(this, true); + let msgWindow; + try { + msgWindow = + this._sendProgress?.msgWindow || + MailServices.mailSession.topmostMsgWindow; + } catch (e) {} + MailServices.nntp.postMessage( + this._deliveryFile, + this._compFields.newsgroups, + this._accountKey, + deliveryListener, + msgWindow, + null + ); + } + + /** + * Collect outgoing addresses to address book. + * + * @param {string[]} recipients - Outgoing addresses including to/cc/bcc. + */ + _collectAddressesToAddressBook(recipients) { + let createCard = Services.prefs.getBoolPref( + "mail.collect_email_address_outgoing", + false + ); + + let addressCollector = Cc[ + "@mozilla.org/addressbook/services/addressCollector;1" + ].getService(Ci.nsIAbAddressCollector); + for (let recipient of recipients) { + addressCollector.collectAddress(recipient, createCard); + } + } + + /** + * Check if link text is equivalent to the href. + * + * @param {string} text - The innerHTML of a <a> element. + * @param {string} href - The href of a <a> element. + * @returns {boolean} true if text is equivalent to href. + */ + _isLinkFreeText(text, href) { + href = href.trim(); + if (href.startsWith("mailto:")) { + return this._isLinkFreeText(text, href.slice("mailto:".length)); + } + text = text.trim(); + return ( + text == href || + (text.endsWith("/") && text.slice(0, -1) == href) || + (href.endsWith("/") && href.slice(0, -1) == text) + ); + } + + /** + * Collect embedded objects as attachments. + * + * @returns {object} collected + * @returns {nsIMsgAttachment[]} collected.embeddedAttachments + * @returns {object[]} collected.embeddedObjects objects {element, url} + */ + _gatherEmbeddedAttachments(editor) { + let embeddedAttachments = []; + let embeddedObjects = []; + + if (!editor || !editor.document) { + return { embeddedAttachments, embeddedObjects }; + } + let nodes = []; + nodes.push(...editor.document.querySelectorAll("img")); + nodes.push(...editor.document.querySelectorAll("a")); + let body = editor.document.querySelector("body[background]"); + if (body) { + nodes.push(body); + } + + let urlCidCache = {}; + for (let element of nodes) { + if (element.tagName == "A" && element.href) { + if (this._isLinkFreeText(element.innerHTML, element.href)) { + // Set this special classname, which is recognized by nsIParserUtils, + // so that links are not duplicated in text/plain. + element.classList.add("moz-txt-link-freetext"); + } + } + let isImage = false; + let url; + let name; + let mozDoNotSend = element.getAttribute("moz-do-not-send"); + if (mozDoNotSend && mozDoNotSend != "false") { + // Only empty or moz-do-not-send="false" may be accepted later. + continue; + } + if (element.tagName == "BODY" && element.background) { + isImage = true; + url = element.background; + } else if (element.tagName == "IMG" && element.src) { + isImage = true; + url = element.src; + name = element.name; + } else if (element.tagName == "A" && element.href) { + url = element.href; + name = element.name; + } else { + continue; + } + let acceptObject = false; + // Before going further, check what scheme we're dealing with. Files need to + // be converted to data URLs during composition. "Attaching" means + // sending as a cid: part instead of original URL. + if (/^https?:\/\//i.test(url)) { + acceptObject = + (isImage && + Services.prefs.getBoolPref( + "mail.compose.attach_http_images", + false + )) || + mozDoNotSend == "false"; + } + if (/^(data|news|snews|nntp):/i.test(url)) { + acceptObject = true; + } + if (!acceptObject) { + continue; + } + + let cid; + if (urlCidCache[url]) { + // If an url has already been inserted as MimePart, just reuse the cid. + cid = urlCidCache[url]; + } else { + cid = lazy.MsgUtils.makeContentId( + this._userIdentity, + embeddedAttachments.length + 1 + ); + urlCidCache[url] = cid; + + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + attachment.name = name || lazy.MsgUtils.pickFileNameFromUrl(url); + attachment.contentId = cid; + attachment.url = url; + embeddedAttachments.push(attachment); + } + embeddedObjects.push({ + element, + url, + }); + + let newUrl = `cid:${cid}`; + if (element.tagName == "BODY") { + element.background = newUrl; + } else if (element.tagName == "IMG") { + element.src = newUrl; + } else if (element.tagName == "A") { + element.href = newUrl; + } + } + return { embeddedAttachments, embeddedObjects }; + } + + /** + * Restore embedded objects in editor to their original urls. + * + * @param {object[]} embeddedObjects - An array of embedded objects. + * @param {Element} embeddedObjects.element + * @param {string} embeddedObjects.url + */ + _restoreEditorContent(embeddedObjects) { + for (let { element, url } of embeddedObjects) { + if (element.tagName == "BODY") { + element.background = url; + } else if (element.tagName == "IMG") { + element.src = url; + } else if (element.tagName == "A") { + element.href = url; + } + } + } + + /** + * Get the message body from an editor. + * + * @param {nsIEditor} editor - The editor instance. + * @returns {string} + */ + _getBodyFromEditor(editor) { + if (!editor) { + return ""; + } + + let flags = + Ci.nsIDocumentEncoder.OutputFormatted | + Ci.nsIDocumentEncoder.OutputNoFormattingInPre | + Ci.nsIDocumentEncoder.OutputDisallowLineBreaking; + // bodyText is UTF-16 string. + let bodyText = editor.outputToString("text/html", flags); + + // No need to do conversion if forcing plain text. + if (!this._compFields.forcePlainText) { + let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService( + Ci.mozITXTToHTMLConv + ); + let csFlags = Ci.mozITXTToHTMLConv.kURLs; + if (Services.prefs.getBoolPref("mail.send_struct", false)) { + csFlags |= Ci.mozITXTToHTMLConv.kStructPhrase; + } + bodyText = cs.scanHTML(bodyText, csFlags); + } + + return bodyText; + } + + /** + * Get the first account key of an identity. + * + * @param {nsIMsgIdentity} identity - The identity. + * @returns {string} + */ + _accountKeyForIdentity(identity) { + let servers = MailServices.accounts.getServersForIdentity(identity); + return servers.length + ? MailServices.accounts.FindAccountForServer(servers[0])?.key + : null; + } +} + +/** + * A listener to be passed to the SMTP service. + * + * @implements {nsIUrlListener} + */ +class MsgDeliveryListener { + QueryInterface = ChromeUtils.generateQI(["nsIUrlListener"]); + + /** + * @param {nsIMsgSend} msgSend - Send instance to use. + * @param {boolean} isNewsDelivery - Whether this is an nntp message delivery. + */ + constructor(msgSend, isNewsDelivery) { + this._msgSend = msgSend; + this._isNewsDelivery = isNewsDelivery; + } + + OnStartRunningUrl(url) { + this._msgSend.notifyListenerOnStartSending(null, 0); + } + + OnStopRunningUrl(url, exitCode) { + lazy.MsgUtils.sendLogger.debug(`OnStopRunningUrl; exitCode=${exitCode}`); + let mailUrl = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + mailUrl.UnRegisterListener(this); + + this._msgSend.sendDeliveryCallback(url, this._isNewsDelivery, exitCode); + } +} |