in mail/components/extensions/parent/ext-compose.js [603:926]
async function setComposeDetails(composeWindow, details, extension) {
const activeElement = composeWindow.document.activeElement;
const composeFields = composeWindow.gMsgCompose.compFields;
// Check if conflicting formats have been specified.
if (
details.isPlainText === true &&
details.body != null &&
details.plainTextBody == null
) {
throw new ExtensionError(
"Conflicting format setting: isPlainText = true and providing a body but no plainTextBody."
);
}
if (
details.isPlainText === false &&
details.body == null &&
details.plainTextBody != null
) {
throw new ExtensionError(
"Conflicting format setting: isPlainText = false and providing a plainTextBody but no body."
);
}
// Remove any unsupported body type. Otherwise, this will throw an
// NS_UNEXPECTED_ERROR later. Note: setComposeDetails cannot change the compose
// format, details.isPlainText is ignored.
if (composeWindow.IsHTMLEditor()) {
delete details.plainTextBody;
} else {
delete details.body;
}
if (details.identityId) {
if (!extension.hasPermission("accountsRead")) {
throw new ExtensionError(
'Using identities requires the "accountsRead" permission'
);
}
const identity = MailServices.accounts.allIdentities.find(
i => i.key == details.identityId
);
if (!identity) {
throw new ExtensionError(`Identity not found: ${details.identityId}`);
}
const identityElement =
composeWindow.document.getElementById("msgIdentity");
identityElement.selectedItem = [
...identityElement.childNodes[0].childNodes,
].find(e => e.getAttribute("identitykey") === details.identityId);
composeWindow.LoadIdentity(false);
}
for (const field of ["to", "cc", "bcc", "replyTo", "followupTo"]) {
if (field in details) {
details[field] = await parseComposeRecipientList(details[field]);
}
}
if (Array.isArray(details.newsgroups)) {
details.newsgroups = details.newsgroups.join(",");
}
composeWindow.SetComposeDetails(details);
await setFromField(composeWindow, details, extension);
// Set file carbon copy values.
if (extension.hasPermission("accountsRead")) {
if (extension.manifest.manifest_version < 3) {
if (details.overrideDefaultFcc === false) {
composeFields.fcc = "";
} else if (details.overrideDefaultFccFolder != null) {
// Override identity fcc with enforced value.
if (details.overrideDefaultFccFolder) {
const { folder } = getFolder(details.overrideDefaultFccFolder);
composeFields.fcc = folder.URI;
} else {
composeFields.fcc = "nocopy://";
}
} else if (
details.overrideDefaultFcc === true &&
composeFields.fcc == ""
) {
throw new ExtensionError(
`Setting overrideDefaultFcc to true requires setting overrideDefaultFccFolder as well`
);
}
if (details.additionalFccFolder != null) {
if (details.additionalFccFolder) {
const { folder } = getFolder(details.additionalFccFolder);
composeFields.fcc2 = folder.URI;
} else {
composeFields.fcc2 = "";
}
}
} else {
// We need === here to differentiate between null and undefined.
if (details.overrideDefaultFccFolderId === null) {
composeFields.fcc = "";
} else if (details.overrideDefaultFccFolderId == "") {
composeFields.fcc = "nocopy://";
} else if (details.overrideDefaultFccFolderId) {
// Override identity fcc with enforced value.
const { folder } = getFolder(details.overrideDefaultFccFolderId);
composeFields.fcc = folder.URI;
}
if (
details.additionalFccFolderId === null ||
details.additionalFccFolderId == ""
) {
composeFields.fcc2 = "";
} else if (details.additionalFccFolderId) {
const { folder } = getFolder(details.additionalFccFolderId);
composeFields.fcc2 = folder.URI;
}
}
}
// Update custom headers, if specified.
if (details.customHeaders) {
const customHeaders = new Map(
details.customHeaders.map(h => {
const sanitizedName = sanitizeCustomHeaderName(h.name);
if (!sanitizedName) {
throw new ExtensionError(
`Invalid custom header: ${
h.name
}. Name must be prefixed by "X-" (but not by "X-Mozilla-") or be one of the explicitly allowed headers (${ALLOWED_CUSTOM_HEADER_NAMES.join(
", "
)})`
);
}
return [sanitizedName, sanitizeCustomHeaderValue(h.value)];
})
);
const obsoleteHeaderNames = new Set(
[...composeFields.headerNames].flatMap(h => {
const sanitizedName = sanitizeCustomHeaderName(h);
return !sanitizedName || customHeaders.has(sanitizedName)
? []
: [sanitizedName];
})
);
for (const headerName of obsoleteHeaderNames) {
composeFields.deleteHeader(headerName);
}
for (const [headerName, headerValue] of customHeaders) {
composeFields.setHeader(headerName, headerValue);
}
// If we added or removed custom headers, which are also displayed in the UI,
// update these fields as well. Such headers are defined in in the pref
// "mail.compose.other.header".
for (const row of composeWindow.document.querySelectorAll(
".address-row-raw"
)) {
const recipientType = row.dataset.recipienttype.trim().toLowerCase();
if (customHeaders.has(recipientType)) {
row.classList.remove("hidden");
row.querySelector(".address-row-input").value =
customHeaders.get(recipientType);
}
if (obsoleteHeaderNames.has(recipientType)) {
row.querySelector(".address-row-input").value = "";
}
}
}
// Update priorities. The enum in the schema defines all allowed values, no
// need to validate here.
if (details.priority) {
if (details.priority == "normal") {
composeFields.priority = "";
} else {
composeFields.priority =
details.priority[0].toUpperCase() + details.priority.slice(1);
}
composeWindow.updatePriorityToolbarButton(composeFields.priority);
}
// Update receipt notifications.
if (details.returnReceipt != null) {
composeWindow.ToggleReturnReceipt(details.returnReceipt);
}
if (
details.deliveryStatusNotification != null &&
details.deliveryStatusNotification != composeFields.DSN
) {
const target = composeWindow.document.getElementById("dsnMenu");
composeWindow.ToggleDSN(target);
}
if (details.deliveryFormat && composeWindow.IsHTMLEditor()) {
// Do not throw when a deliveryFormat is set on a plaint text composer, because
// it is allowed to set ComposeDetails of an html composer onto a plain text
// composer (and automatically pick the plainText body). The deliveryFormat
// will be ignored.
composeFields.deliveryFormat = deliveryFormats.find(
f => f.value == details.deliveryFormat
).id;
composeWindow.initSendFormatMenu();
}
if (
details.attachVCard != null &&
composeFields.attachVCard != details.attachVCard
) {
composeFields.attachVCard = details.attachVCard;
composeWindow.gAttachVCardOptionChanged = true;
}
if (
details.attachPublicPGPKey != null &&
composeWindow.gAttachMyPublicPGPKey != details.attachPublicPGPKey &&
composeWindow.isPgpConfigured()
) {
// Cannot use toggleAttachMyPublicKey() function, as that acts on the clicked
// menu item, but also does not toggle all menuitens (the others are updated
// on show). Just set the flags.
composeWindow.gAttachMyPublicPGPKey = details.attachPublicPGPKey;
composeWindow.gUserTouchedAttachMyPubKey = true;
}
if (details.isModified != null) {
const modified =
composeWindow.gContentChanged ||
composeWindow.gMsgCompose.bodyModified ||
composeWindow.gReceiptOptionChanged ||
composeWindow.gDSNOptionChanged;
if (details.isModified === true && !modified) {
// To trigger the close confirmation dialog, it is enough to set
// gContentChanged to true.
composeWindow.gContentChanged = true;
} else if (details.isModified === false && modified) {
// In order to prevent the close confirmation dialog, we need to make sure
// all potential triggers are set to false.
composeWindow.gContentChanged = false;
composeWindow.gMsgCompose.bodyModified = false;
composeWindow.gReceiptOptionChanged = false;
composeWindow.gDSNOptionChanged = false;
}
}
// Handle encryption.
if (details.selectedEncryptionTechnology?.name) {
const isPgpConfigured = composeWindow.isPgpConfigured();
const isSmimeSigningConfigured = composeWindow.isSmimeSigningConfigured();
const isSmimeEncryptionConfigured =
composeWindow.isSmimeEncryptionConfigured();
const technology = details.selectedEncryptionTechnology;
const wantsPGP = technology.name == "OpenPGP";
const wantsSMIME = technology.name == "S/MIME";
// We cannot switch to an unsupported technology.
if (
(!isPgpConfigured && wantsPGP) ||
(!isSmimeEncryptionConfigured && !isSmimeSigningConfigured && wantsSMIME)
) {
throw new ExtensionError(
`The current identity does not support ${technology.name}`
);
}
// Cannot enable subject encryption but not encryption in general.
if (wantsPGP && technology.encryptSubject && !technology.encryptBody) {
throw new ExtensionError(
`Cannot encrypt the subject without also encrypting the body using ${technology.name}`
);
}
// Abort if S/MIME encryption is requested, but not possible.
if (wantsSMIME && technology.encryptBody && !isSmimeEncryptionConfigured) {
throw new ExtensionError(
`The current identity does not support encryption using ${technology.name}`
);
}
// Abort if S/MIME signing is requested, but not possible.
if (wantsSMIME && technology.signMessage && !isSmimeSigningConfigured) {
throw new ExtensionError(
`The current identity does not support signing using ${technology.name}`
);
}
// If an add-on sets encryption settings, we need to make sure they are not
// altered (asyncrounously) by defaults. This is solved by setting them as
// touched, before using onEncryptionChoice() to toggle the values (if needed).
composeWindow.gUserTouchedSendEncrypted = true;
composeWindow.gUserTouchedSendSigned = true;
if (wantsPGP) {
composeWindow.gUserTouchedEncryptSubject = true;
}
// Toggle technology, if needed.
if (composeWindow.gSelectedTechnologyIsPGP != wantsPGP) {
composeWindow.onEncryptionChoice(wantsPGP ? "OpenPGP" : "SMIME");
}
// Toggle encryption, if needed.
if (composeWindow.gSendEncrypted != technology.encryptBody) {
composeWindow.onEncryptionChoice("enc");
}
// Toggle subject encryption, if needed. We already prevented encryptSubject
// being enabled while encryptBody is disabled.
if (
wantsPGP &&
composeWindow.gEncryptSubject != technology.encryptSubject
) {
composeWindow.onEncryptionChoice("encsub");
}
// Toggle signing, if needed.
if (composeWindow.gSendSigned != technology.signMessage) {
composeWindow.onEncryptionChoice("sig");
}
}
activeElement.focus();
}