Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
// nsMsgKey_None from MailNewsTypes.h.
const nsMsgKey_None = 0xffffffff;
/**
* A class to manage sending processes.
*
* @implements {nsIMsgSend}
* @implements {nsIWebProgressListener}
*/
export class MessageSend {
QueryInterface = ChromeUtils.generateQI([
"nsIMsgSend",
"nsIWebProgressListener",
"nsISupportsWeakReference",
]);
classID = Components.ID("{028b9c1e-8d0a-4518-80c2-842e07846eaa}");
/**
* Create an rfc822 message and send it.
* @param {nsIEditor} editor - nsIEditor instance that contains message.
* May be a dummy, especially in the case of import.
* @param {nsIMsgIdentity} userIdentity - Identity to send from.
* @param {?string} accountKey - Account we're sending message from.
* @param {nsIMsgCompFields} compFields - Composition fields.
* @param {boolean} isDigest - Is this a digest message?
* @param {boolean} dontDeliver - Set to false by the import code -
* used when we're trying to create a message from parts.
* @param {nsMsgDeliverMode} deliverMode - Delivery mode.
* @param {?nsIMsgDBHdr} msgToReplace - E.g., when saving a draft over an old draft.
* @param {string} bodyType - Content type of message body.
* @param {string} body - Message body text (should have native line endings)
* @param {?mozIDOMWindowProxy} parentWindow - Compose window.
* @param {?nsIMsgProgress} progress - Where to send progress info.
* @param {?nsIMsgSendListener} listener - Optional listener for send progress.
* @param {?string} smtpPassword - Optional smtp server password
* @param {?string} originalMsgURI - URI of original message.
* @param {nsIMsgCompType} compType - Compose type.
* @returns {Promise} promise when the create and send process is done.
*/
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 || Services.wm.getMostRecentWindow("msgcompose");
this._originalMsgURI = originalMsgURI;
this._compType = compType;
this._shouldRemoveMessageFile = true;
this._sendReport = Cc[
"@mozilla.org/messengercompose/sendreport;1"
].createInstance(Ci.nsIMsgSendReport);
this._composeBundle = Services.strings.createBundle(
);
// 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
);
const { 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 == Cr.NS_ERROR_FILE_NOT_FOUND) {
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);
}
/**
* Sends a file to the specified composition fields, via the user identity
* provided.
*
* @param {nsIMsgIdentity} userIdentity - The user identity to use for sending
* this email.
* @param {string} accountKey - The key of the account that this message relates to.
* @param {nsIMsgCompFields} compFields - An object containing information
* on who to send the message to.
* @param {nsIFile} messageFile - A reference to the file to send.
* @param {boolean} deleteSendFileOnCompletion - Set to true if you want the
* send file deleted once the message has been sent.
* @param {boolean} digest - If this is a multipart message, this param
* specifies whether the message is in digest or mixed format.
* @param {nsMsgDeliverMode} deliverMode - The delivery mode for sending the
* message (see above for values).
* @param {?nsIMsgDBHdr} msgToReplace - A message header representing a
* message to be replaced by the one sent.
* @param {?nsIMsgSendListener} listener - An nsIMsgSendListener to receive
* feedback on the current send status. This parameter can also support
* the nsIMsgCopyServiceListener interface to receive notifications of copy
* finishing e.g. after saving a message to the sent mail folder.
* @param {string} smtpPassword - Pass this in to prevent a dialog if the
* password is needed for secure transmission.
*/
async sendMessageFile(
userIdentity,
accountKey,
compFields,
messageFile,
deleteSendFileOnCompletion,
digest,
deliverMode,
msgToReplace,
listener,
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._shouldRemoveMessageFile = deleteSendFileOnCompletion;
this._sendReport = Cc[
"@mozilla.org/messengercompose/sendreport;1"
].createInstance(Ci.nsIMsgSendReport);
this._composeBundle = Services.strings.createBundle(
);
// 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(
);
// Initialize the error reporting mechanism.
this.sendReport.reset();
const deliverMode = isDraft
? Ci.nsIMsgSend.nsMsgSaveAsDraft
: Ci.nsIMsgSend.nsMsgDeliverNow;
this.sendReport.deliveryMode = deliverMode;
// Convert nsIMsgAttachedFile[] to nsIMsgAttachment[]
for (const file of attachedFiles) {
const 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[]
const embeddedAttachments = embeddedObjects.map(obj => {
const 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() {}
onProgressChange() {}
onStatusChange() {}
onSecurityChange() {}
onContentBlockingEvent() {}
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._outgoingListener) {
this._outgoingListener.cancel(Cr.NS_ERROR_ABORT);
}
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}`
);
if (errorMsg) {
this._sendReport.errMessage = errorMsg;
}
this._sendReport.displayReport(this._parentWindow);
}
this.abort();
}
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);
}
}
async notifyListenerOnStopCopy(status) {
lazy.MsgUtils.sendLogger.debug(
`notifyListenerOnStopCopy; status=${status}`
);
this._msgCopy = null;
if (!this._isRetry) {
const 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)) {
const localFoldersAccountName =
MailServices.accounts.localFoldersServer.prettyName;
const folder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
const accountName = folder?.server.prettyName;
if (!this._fcc || !localFoldersAccountName || !accountName) {
this.fail(Cr.NS_OK, null);
return;
}
const params = [
folder.localizedName,
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) {
const showCheckBox = { value: false };
const 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;
const dialogTitle =
this._composeBundle.GetStringFromName("SaveDialogTitle");
const buttonLabelRety =
this._composeBundle.GetStringFromName("buttonLabelRetry2");
const buttonPressed = Services.prompt.confirmEx(
this._parentWindow,
dialogTitle,
promptMsg,
buttonFlags,
buttonLabelRety,
null,
null,
null,
showCheckBox
);
if (buttonPressed == 0) {
// retry button clicked
if (
this._sendProgress?.processCanceledByUser &&
Services.prefs.getBoolPref("mailnews.show_send_progress")
) {
// We had a progress dialog and the user cancelled it, create a
// new one.
const progress = Cc[
"@mozilla.org/messenger/progress;1"
].createInstance(Ci.nsIMsgProgress);
const composeParams = Cc[
"@mozilla.org/messengercompose/composeprogressparameters;1"
].createInstance(Ci.nsIMsgComposeProgressParams);
composeParams.subject =
this._parentWindow.gMsgCompose.compFields.subject;
composeParams.deliveryMode = this._deliverMode;
progress.openProgressDialog(
this._parentWindow,
composeParams
);
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;
}
await 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.
await this._mimeDoFcc(null, true, Ci.nsIMsgSend.nsMsgDeliverNow);
return;
} catch (e) {
Services.prompt.alert(
this._parentWindow,
null,
this._composeBundle.GetStringFromName("saveToLocalFoldersFailed")
);
}
}
}
// A failed or declined primary FCC copy must not skip the additional
// folder copy.
await this._doFcc2();
return;
}
if (
!this._fcc2Handled &&
this._messageKey != nsMsgKey_None &&
[Ci.nsIMsgSend.nsMsgDeliverNow, Ci.nsIMsgSend.nsMsgSendUnsent].includes(
this._deliverMode
)
) {
// Sent-message filters finish asynchronously in onStopOperation. Wait for
// that callback so FCC2 remains ordered after the filters.
const { promise, resolve, reject } = Promise.withResolvers();
this._filterCompletionResolvers = { resolve, reject };
try {
this._filterSentMessage();
} catch (e) {
await this.onStopOperation(e.result);
}
await promise;
return;
}
await 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.
*/
async 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")
);
}
try {
await this._doFcc2();
this._filterCompletionResolvers?.resolve();
} catch (e) {
// The awaiter in _doFcc surfaces this via the rejected promise; don't
// also throw, or the ignored onStopOperation promise becomes an
// unhandled rejection.
this._filterCompletionResolvers?.reject(e);
} finally {
this._filterCompletionResolvers = null;
}
}
/**
* Handle the exit code of message delivery.
*
* @param {nsIURI} serverURI - The URI of the server used for the delivery.
* @param {nsresult} exitCode - The exit code of message delivery.
* @param {?nsITransportSecurityInfo} secInfo - The info to use in case of a security error.
* @param {string} errMsg - A localized error message.
* @param {boolean} isNewsDelivery - The message was delivered to newsgroup.
*/
async _deliveryExitProcessing(
serverURI,
exitCode,
secInfo,
errMsg,
isNewsDelivery
) {
lazy.MsgUtils.sendLogger.debug(
`Delivery exit processing; exitCode=${exitCode}`
);
if (!Components.isSuccessCode(exitCode)) {
let isNSSError = false;
let isOverridable = false;
const nssErrorsService = Cc[
"@mozilla.org/nss_errors_service;1"
].getService(Ci.nsINSSErrorsService);
if (secInfo?.errorCode) {
if (nssErrorsService.isNSSErrorCode(secInfo.errorCode)) {
isNSSError = true;
exitCode = nssErrorsService.getXPCOMFromNSSError(secInfo.errorCode);
isOverridable = nssErrorsService.isErrorOverridable(exitCode);
}
}
let errorMsg;
if (!isNSSError) {
const errorName = lazy.MsgUtils.getErrorStringName(exitCode);
if (exitCode == Cr.NS_ERROR_FAILURE) {
errorMsg = errMsg;
} else if (
[
Cr.NS_ERROR_UNKNOWN_HOST,
Cr.NS_ERROR_UNKNOWN_PROXY_HOST,
Cr.NS_ERROR_CONNECTION_REFUSED,
Cr.NS_ERROR_PROXY_CONNECTION_REFUSED,
Cr.NS_ERROR_NET_INTERRUPT,
Cr.NS_ERROR_NET_TIMEOUT,
Cr.NS_ERROR_NET_RESET,
].includes(exitCode)
) {
errorMsg = lazy.MsgUtils.formatStringWithSMTPHostName(
this._userIdentity,
this._composeBundle,
errorName
);
} else if (errMsg) {
// errMsg is an already localized message, usually combined with the
// error message from SMTP server.
errorMsg = errMsg;
} else {
// May be the default string "sendFailed". Should be and error that
// does require the server name to be encoded.
errorMsg = this._composeBundle.GetStringFromName(errorName);
}
} else {
// 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"
);
}
this.notifyListenerOnStopSending(null, exitCode, null, null);
this.fail(exitCode, errorMsg);
if (isNSSError && isOverridable) {
this.notifyListenerOnTransportSecurityError(
null,
exitCode,
secInfo,
serverURI.asciiHostPort
);
}
return;
}
if (
isNewsDelivery &&
(this._compFields.to || this._compFields.cc || this._compFields.bcc)
) {
this._deliverAsMail();
return;
}
this.notifyListenerOnStopSending(
this._compFields.messageId,
exitCode,
null,
null
);
await this._doFcc();
}
async sendDeliveryCallback(
serverURI,
exitCode,
secInfo,
errMsg,
isNewsDelivery = false
) {
if (isNewsDelivery) {
if (
!Components.isSuccessCode(exitCode) &&
exitCode != Cr.NS_ERROR_ABORT
) {
exitCode = Cr.NS_ERROR_FAILURE;
errMsg = this._composeBundle.GetStringFromName("postFailed");
}
return await this._deliveryExitProcessing(
serverURI,
exitCode,
secInfo,
errMsg,
isNewsDelivery
);
}
return await this._deliveryExitProcessing(
serverURI,
exitCode,
secInfo,
errMsg,
isNewsDelivery
);
}
get folderUri() {
return this._folderUri;
}
get messageId() {
return this._compFields.messageId;
}
/**
* @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;
}
const warningSize = Services.prefs.getIntPref(
"mailnews.message_warning_size"
);
if (warningSize > 0 && file.fileSize > warningSize) {
const messenger = Cc["@mozilla.org/messenger;1"].createInstance(
Ci.nsIMessenger
);
const msg = this._composeBundle.formatStringFromName(
"largeMessageSendWarning",
[messenger.formatFileSize(file.fileSize)]
);
if (!Services.prompt.confirm(this._parentWindow, null, msg)) {
this.fail(Cr.NS_ERROR_ABORT);
throw Components.Exception(
"Cancelled sending large message",
Cr.NS_ERROR_FAILURE
);
}
}
this._deliveryFile = await this._createDeliveryFile();
if (this._compFields.newsgroups) {
await 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;
}
const deliveryFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
deliveryFile.append("nsemail.tmp");
deliveryFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
const content = await IOUtils.read(this._messageFile.path);
const 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
);
const header = new TextDecoder("UTF-8").decode(content.slice(0, bodyIndex));
let inBcc = false;
let headerToWrite = "";
for (const line of header.split("\r\n")) {
if (line.startsWith("Bcc:") || (line.startsWith(" ") && inBcc)) {
inBcc = true;
continue;
}
inBcc = false;
headerToWrite += `${line}\r\n`;
}
const encodedHeader = new TextEncoder().encode(headerToWrite);
// Prevent extra \r\n, which was already added to the last head line.
const body = content.slice(bodyIndex + 2);
const 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.
let contentToWrite = `From - ${new Date().toUTCString()}\r\n`;
const xMozillaStatus = lazy.MsgUtils.getXMozillaStatus(this._deliverMode);
const 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.
const 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)) {
await this.notifyListenerOnStopCopy(Cr.NS_OK);
return;
}
this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Copy;
await 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.
const 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.
const localFolder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
folderUri += localFolder.localizedName + "-";
}
if (this._fcc) {
// Get the account name where the "save to" failed.
const 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.
const 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.
this._sendListener?.onGetDraftFolderURI(
this._compFields.messageId,
this._folderUri
);
folder = lazy.MailUtils.getOrCreateFolder(this._folderUri);
const statusMsg = this._composeBundle.formatStringFromName(
"copyMessageStart",
[folder?.localizedName || "?"]
);
this._setStatusMessage(statusMsg);
lazy.MsgUtils.sendLogger.debug("startCopyOperation");
try {
this._msgCopy.startCopyOperation(
this._userIdentity,
this._copyFile,
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);
}
await this.notifyListenerOnStopCopy(e.result);
}
}
/**
* Handle the fcc2 field. Then notify OnStopCopy and clean up.
*/
async _doFcc2() {
// Handle fcc2 only once.
if (
!this._fcc2Handled &&
this._compFields.fcc2 &&
[
Ci.nsIMsgSend.nsMsgDeliverNow,
Ci.nsIMsgSend.nsMsgQueueForLater,
].includes(this._deliverMode)
) {
lazy.MsgUtils.sendLogger.debug("Processing fcc2");
this._fcc2Handled = true;
await 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", e);
}
this._cleanup();
});
}
/**
* Run filters on the just sent message.
*/
_filterSentMessage() {
this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_Filter;
const folder = lazy.MailUtils.getExistingFolder(this._folderUri);
const msgHdr = folder.GetMessageHeader(this._messageKey);
const 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 the outgoing service.
*/
async _deliverAsMail() {
this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_SMTP;
this._setStatusMessage(
this._composeBundle.GetStringFromName("sendingMessage")
);
// Turn the `to` and `cc` comp fields (which are both strings) into one
// continuous string, filtering out either of them if it's empty.
const visibleRecipients = [this._compFields.to, this._compFields.cc]
.filter(Boolean)
.join(",");
const parsedVisibleRecipients =
MailServices.headerParser.parseEncodedHeaderW(visibleRecipients);
// Parse the `bcc` comp field (a string) into a parsed array of
// `msgIAddressObject`.
let parsedBccRecipients = [];
if (this._compFields.bcc) {
parsedBccRecipients = MailServices.headerParser.parseEncodedHeaderW(
this._compFields.bcc
);
}
// Collect all recipients into the address book at once.
try {
this._collectAddressesToAddressBook(
[...parsedVisibleRecipients, ...parsedBccRecipients].filter(Boolean)
);
} catch (e) {
// Db access issues, etc. Not fatal for sending.
lazy.MsgUtils.sendLogger.warn(
`Collecting outgoing addresses FAILED: ${e.message}`,
e
);
}
lazy.MsgUtils.sendLogger.debug(
`Delivering mail message <${this._compFields.messageId}>`
);
const outgoingListener = new PromiseMsgOutgoingListener(this);
// Retrieve the relevant server to send this message from the outgoing
// server service (and make sure it gave us one).
const server = MailServices.outgoingServer.getServerByIdentity(
this._userIdentity
);
if (!server) {
throw new Error(`No outgoing server for ${this._userIdentity.email}`);
}
// Keep the listener available for abort(): the cancelable request may not
// exist until onSendStart runs.
this._outgoingListener = outgoingListener;
// Send the message using the server that was retrieved.
server.sendMailMessage(
this._deliveryFile,
parsedVisibleRecipients,
parsedBccRecipients,
this._userIdentity,
this._compFields.from,
this._smtpPassword,
this._sendProgress,
this._compFields.DSN,
this._compFields.messageId,
outgoingListener
);
await outgoingListener.requestPromise;
}
/**
* Send this._deliveryFile to nntp service.
*/
async _deliverAsNews() {
this.sendReport.currentProcess = Ci.nsIMsgSendReport.process_NNTP;
lazy.MsgUtils.sendLogger.debug("Delivering news message");
const deliveryListener = new NewsDeliveryListener(this);
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
);
await deliveryListener.requestPromise;
}
/**
* Collect outgoing addresses to address book.
*
* @param {msgIAddressObject[]} addresses - Outgoing addresses including to/cc/bcc.
*/
_collectAddressesToAddressBook(addresses) {
const createCard = Services.prefs.getBoolPref(
"mail.collect_email_address_outgoing",
false
);
for (const addr of addresses) {
let displayName = addr.name;
// If we know this is a list, or it seems likely, don't collect the
// displayName which may contain the sender's name instead of the (only)
// name of the list.
if (
this._compType == Ci.nsIMsgCompType.ReplyToList ||
addr.name.includes(" via ")
) {
displayName = "";
}
lazy.collectSingleAddress(addr.email, displayName, 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) {
const embeddedAttachments = [];
const embeddedObjects = [];
if (!editor || !editor.document) {
return { embeddedAttachments, embeddedObjects };
}
const nodes = [];
nodes.push(...editor.document.querySelectorAll("img"));
nodes.push(...editor.document.querySelectorAll("a"));
const body = editor.document.querySelector("body[background]");
if (body) {
nodes.push(body);
}
const urlCidCache = {};
for (const 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;
const 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 shouldEmbed = 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)) {
shouldEmbed =
(isImage &&
Services.prefs.getBoolPref(
"mail.compose.attach_http_images",
false
)) ||
mozDoNotSend == "false";
}
if (/^(data|nntp):/i.test(url)) {
shouldEmbed = true;
}
if (/^(news|snews):/i.test(url)) {
shouldEmbed = mozDoNotSend == "false";
}
if (!shouldEmbed) {
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;
const 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,
});
const 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 (const { 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 "";
}
const 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) {
const 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) {
const servers = MailServices.accounts.getServersForIdentity(identity);
return servers.length
? MailServices.accounts.findAccountForServer(servers[0])?.key
: null;
}
}
/**
* A listener to be passed to the NNTP service.
*
* @implements {nsIUrlListener}
*/
class NewsDeliveryListener {
QueryInterface = ChromeUtils.generateQI(["nsIUrlListener"]);
#resolve;
#reject;
#requestPromise;
/**
* @param {nsIMsgSend} msgSend - nsIMsgSend instance to use.
*/
constructor(msgSend) {
this._msgSend = msgSend;
const { promise, resolve, reject } = Promise.withResolvers();
this.#requestPromise = promise;
this.#resolve = resolve;
this.#reject = reject;
}
OnStartRunningUrl() {
this._msgSend.notifyListenerOnStartSending(null, 0);
}
async OnStopRunningUrl(url, exitCode) {
lazy.MsgUtils.sendLogger.debug(`OnStopRunningUrl; exitCode=${exitCode}`);
if (url instanceof Ci.nsIMsgMailNewsUrl) {
url.UnRegisterListener(this);
}
// Await the callback to ensure state cleanup completes.
await this._msgSend.sendDeliveryCallback(url, exitCode, null, null, true);
if (Components.isSuccessCode(exitCode)) {
this.#resolve();
} else {
this.#reject(
new Error(`NNTP delivery failed with exit code: ${exitCode}`)
);
}
}
/**
* A promise which resolves when the NNTP send attempt completes successfully,
* or rejects if it fails.
*/
get requestPromise() {
return this.#requestPromise;
}
}
/**
* A listener to be passed to an outgoing mail server.
*
* It provides a Promise which resolves to a request (of type `nsIRequest`) when
* the message send begins. This request can be used to cancel the send attempt
* if requested by the user.
*
* Upon start and stop of the send attempt, this listener also calls the
* relevant callbacks on its `nsIMsgSend`.
*
* @implements {nsIMsgOutgoingListener}
*/
class PromiseMsgOutgoingListener {
/**
* The nsIMsgSend instance to notify on message send start/stop.
*
* @type {nsIMsgSend}
*/
#msgSend;
/**
* A promise that resolves to a request that can be used to cancel the message
* send operation if requested.
*
* @type {Promise<nsIRequest>}
*/
#requestPromise;
/**
* The handle to resolve `#requestPromise`.
*
* @type {function(nsIRequest): void}
*/
#resolve;
/**
* The handle to reject `#requestPromise`.
*
* @type {function(Error): void}
*/
#reject;
/**
* @type {nsIRequest}
*/
#request;
/**
* A cancel status requested via cancel() before onSendStart had a cancelable
* request. onSendStart applies it once the request exists.
*
* @type {nsresult}
*/
#pendingCancelStatus;
QueryInterface = ChromeUtils.generateQI(["nsIMsgOutgoingListener"]);
/**
* @param {nsIMsgSend} msgSend - nsIMsgSend instance to notify on start/stop.
*/
constructor(msgSend) {
this.#msgSend = msgSend;
// We track the outcome of the send attempt, settled in onSendStop.
const { promise, resolve, reject } = Promise.withResolvers();
this.#requestPromise = promise;
this.#resolve = resolve;
this.#reject = reject;
}
/**
* Notifies that the send attempt has started, and resolves the inner promise.
*
* @param {nsIRequest} request - A request that can be used to cancel the send
* attempt.
*/
onSendStart(request) {
this.#request = request;
this.#msgSend.notifyListenerOnStartSending(null, 0);
if (this.#pendingCancelStatus !== undefined) {
// Apply a cancellation that was requested before onSendStart.
request.cancel(this.#pendingCancelStatus);
this.#pendingCancelStatus = undefined;
}
}
/**
* Notifies that the send attempt has finished.
*
* @param {nsIURI} serverURI - The URI of the server that was used to send.
* @param {nsresult} exitCode - The resulting status code for the send
* attempt.
* @param {?nsITransportSecurityInfo} secInfo - The security context for the
* send attempt.
* @param {?string} errMsg - An optional localized, human-readable error
* message.
*/
async onSendStop(serverURI, exitCode, secInfo, errMsg) {
if (this.#msgSend._outgoingListener == this) {
this.#msgSend._outgoingListener = null;
}
await this.#msgSend.sendDeliveryCallback(
serverURI,
exitCode,
secInfo,
errMsg
);
if (this.#msgSend._deliverMode == Ci.nsIMsgSend.nsMsgSendUnsent) {
return;
}
if (Components.isSuccessCode(exitCode)) {
this.#resolve(this.#request);
} else {
// Reject with an exception carrying the send status, so that
// nsMsgCompose::SendMsg can recognize NS_ERROR_ABORT and skip the
// failure alert when the user cancels their own send.
this.#reject(
new Components.Exception(`Sending FAILED! ${errMsg}`, exitCode)
);
}
}
/**
* A promise which resolves with an `nsIRequest`, which can be used to cancel
* a send attempt.
*/
get requestPromise() {
return this.#requestPromise;
}
/**
* Cancel the outgoing request, or remember the cancellation until the request
* exists.
*
* @param {nsresult} status - The cancellation status.
*/
cancel(status) {
if (this.#request) {
this.#request.cancel(status);
} else {
this.#pendingCancelStatus = status;
}
}
}
/**
* @implements {nsIMsgSendReport}
*/
export class MessageSendReport {
QueryInterface = ChromeUtils.generateQI(["nsIMsgSendReport"]);
/** @type {integer} - see nsMsgDeliverMode in nsIMsgSend.idl for valid value */
deliveryMode = 0;
/** @type {string} */
errMessage = "";
/** @type {integer} */
#currentProcess = 0;
/** @type {boolean} */
#nntpProcessed = false;
/** @type {boolean} */
#alreadyDisplayed = false;
get currentProcess() {
return this.#currentProcess;
}
set currentProcess(value) {
if (value < 0 || value > Ci.nsIMsgSendReport.process_FCC) {
throw new Error(`Illegal process value: ${value}`);
}
this.#currentProcess = value;
if (value == Ci.nsIMsgSendReport.process_NNTP) {
this.#nntpProcessed = true;
}
}
reset() {
this.deliveryMode = 0;
this.errMessage = "";
this.#currentProcess = 0;
this.#nntpProcessed = false;
this.#alreadyDisplayed = false;
}
/**
* Display Report will analyze data collected during the send and will show
* the most appropriate error.
*
* @param {?mozIDOMWindowProxy} win
*/
displayReport(win) {
if (this.#alreadyDisplayed) {
return;
}
this.#alreadyDisplayed = true;
const composeBundle = Services.strings.createBundle(
);
if (
this.deliveryMode == Ci.nsIMsgCompDeliverMode.Now ||
this.deliveryMode == Ci.nsIMsgCompDeliverMode.SendUnsent
) {
const title = composeBundle.GetStringFromName("sendMessageErrorTitle");
let str = "sendFailed";
switch (this.#currentProcess) {
case Ci.nsIMsgSendReport.process_SMTP:
str = this.#nntpProcessed ? "sendFailedButNntpOk" : "sendFailed";
break;
case Ci.nsIMsgSendReport.process_Copy:
case Ci.nsIMsgSendReport.process_FCC:
str = "failedCopyOperation";
break;
}
let message = composeBundle.GetStringFromName(str);
if (this.errMessage) {
message += "\n" + this.errMessage;
}
Services.prompt.alert(win, title, message);
} else {
let titleStr = "sendMessageErrorTitle";
let str = "sendFailed";
switch (this.deliveryMode) {
case Ci.nsIMsgCompDeliverMode.Later:
titleStr = "sendLaterErrorTitle";
str = "unableToSendLater";
break;
case Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft:
case Ci.nsIMsgCompDeliverMode.SaveAsDraft:
titleStr = "saveDraftErrorTitle";
str = "unableToSaveDraft";
break;
case Ci.nsIMsgCompDeliverMode.SaveAsTemplate:
titleStr = "saveTemplateErrorTitle";
str = "unableToSaveTemplate";
break;
}
const title = composeBundle.GetStringFromName(titleStr);
let message = composeBundle.GetStringFromName(str);
if (this.errMessage) {
message += "\n" + this.errMessage;
}
Services.prompt.alert(win, title, message);
}
}
}