async function setComposeDetails()

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();
}