summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/parent/ext-compose.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/parent/ext-compose.js')
-rw-r--r--comm/mail/components/extensions/parent/ext-compose.js1703
1 files changed, 1703 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/parent/ext-compose.js b/comm/mail/components/extensions/parent/ext-compose.js
new file mode 100644
index 0000000000..33a52c5e08
--- /dev/null
+++ b/comm/mail/components/extensions/parent/ext-compose.js
@@ -0,0 +1,1703 @@
+/* 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/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["IOUtils", "PathUtils"]);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "MailServices",
+ "resource:///modules/MailServices.jsm"
+);
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+let { MsgUtils } = ChromeUtils.import(
+ "resource:///modules/MimeMessageUtils.jsm"
+);
+let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
+ Ci.nsIParserUtils
+);
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File"]);
+
+const deliveryFormats = [
+ { id: Ci.nsIMsgCompSendFormat.Auto, value: "auto" },
+ { id: Ci.nsIMsgCompSendFormat.PlainText, value: "plaintext" },
+ { id: Ci.nsIMsgCompSendFormat.HTML, value: "html" },
+ { id: Ci.nsIMsgCompSendFormat.Both, value: "both" },
+];
+
+async function parseComposeRecipientList(
+ list,
+ requireSingleValidEmail = false
+) {
+ if (!list) {
+ return list;
+ }
+
+ function isValidAddress(address) {
+ return address.includes("@", 1) && !address.endsWith("@");
+ }
+
+ // A ComposeRecipientList could be just a single ComposeRecipient.
+ if (!Array.isArray(list)) {
+ list = [list];
+ }
+
+ let recipients = [];
+ for (let recipient of list) {
+ if (typeof recipient == "string") {
+ let addressObjects =
+ MailServices.headerParser.makeFromDisplayAddress(recipient);
+
+ for (let ao of addressObjects) {
+ if (requireSingleValidEmail && !isValidAddress(ao.email)) {
+ throw new ExtensionError(`Invalid address: ${ao.email}`);
+ }
+ recipients.push(
+ MailServices.headerParser.makeMimeAddress(ao.name, ao.email)
+ );
+ }
+ continue;
+ }
+ if (!("addressBookCache" in this)) {
+ await extensions.asyncLoadModule("addressBook");
+ }
+ if (recipient.type == "contact") {
+ let contactNode = this.addressBookCache.findContactById(recipient.id);
+
+ if (
+ requireSingleValidEmail &&
+ !isValidAddress(contactNode.item.primaryEmail)
+ ) {
+ throw new ExtensionError(
+ `Contact does not have a valid email address: ${recipient.id}`
+ );
+ }
+ recipients.push(
+ MailServices.headerParser.makeMimeAddress(
+ contactNode.item.displayName,
+ contactNode.item.primaryEmail
+ )
+ );
+ } else {
+ if (requireSingleValidEmail) {
+ throw new ExtensionError("Mailing list not allowed.");
+ }
+
+ let mailingListNode = this.addressBookCache.findMailingListById(
+ recipient.id
+ );
+ recipients.push(
+ MailServices.headerParser.makeMimeAddress(
+ mailingListNode.item.dirName,
+ mailingListNode.item.description || mailingListNode.item.dirName
+ )
+ );
+ }
+ }
+ if (requireSingleValidEmail && recipients.length != 1) {
+ throw new ExtensionError(
+ `Exactly one address instead of ${recipients.length} is required.`
+ );
+ }
+ return recipients.join(",");
+}
+
+function composeWindowIsReady(composeWindow) {
+ return new Promise(resolve => {
+ if (composeWindow.composeEditorReady) {
+ resolve();
+ return;
+ }
+ composeWindow.addEventListener("compose-editor-ready", resolve, {
+ once: true,
+ });
+ });
+}
+
+async function openComposeWindow(relatedMessageId, type, details, extension) {
+ let format = Ci.nsIMsgCompFormat.Default;
+ let identity = null;
+
+ if (details) {
+ if (details.isPlainText != null) {
+ format = details.isPlainText
+ ? Ci.nsIMsgCompFormat.PlainText
+ : Ci.nsIMsgCompFormat.HTML;
+ } else {
+ // If none or both of details.body and details.plainTextBody are given, the
+ // default compose format will be used.
+ if (details.body != null && details.plainTextBody == null) {
+ format = Ci.nsIMsgCompFormat.HTML;
+ }
+ if (details.plainTextBody != null && details.body == null) {
+ format = Ci.nsIMsgCompFormat.PlainText;
+ }
+ }
+
+ if (details.identityId != null) {
+ if (!extension.hasPermission("accountsRead")) {
+ throw new ExtensionError(
+ 'Using identities requires the "accountsRead" permission'
+ );
+ }
+
+ identity = MailServices.accounts.allIdentities.find(
+ i => i.key == details.identityId
+ );
+ if (!identity) {
+ throw new ExtensionError(`Identity not found: ${details.identityId}`);
+ }
+ }
+ }
+
+ // ForwardInline is totally broken, see bug 1513824. Fake it 'til we make it.
+ if (
+ [
+ Ci.nsIMsgCompType.ForwardInline,
+ Ci.nsIMsgCompType.Redirect,
+ Ci.nsIMsgCompType.EditAsNew,
+ Ci.nsIMsgCompType.Template,
+ ].includes(type)
+ ) {
+ let msgHdr = null;
+ let msgURI = null;
+ if (relatedMessageId) {
+ msgHdr = messageTracker.getMessage(relatedMessageId);
+ msgURI = msgHdr.folder.getUriForMsg(msgHdr);
+ }
+
+ // For the types in this code path, OpenComposeWindow only uses
+ // nsIMsgCompFormat.Default or OppositeOfDefault. Check which is needed.
+ // See https://hg.mozilla.org/comm-central/file/592fb5c396ebbb75d4acd1f1287a26f56f4164b3/mailnews/compose/src/nsMsgComposeService.cpp#l395
+ if (format != Ci.nsIMsgCompFormat.Default) {
+ // The mimeConverter used in this code path is not setting any format but
+ // defaults to plaintext if no identity and also no default account is set.
+ // The "mail.identity.default.compose_html" preference is NOT used.
+ let usedIdentity =
+ identity || MailServices.accounts.defaultAccount?.defaultIdentity;
+ let defaultFormat = usedIdentity?.composeHtml
+ ? Ci.nsIMsgCompFormat.HTML
+ : Ci.nsIMsgCompFormat.PlainText;
+ format =
+ format == defaultFormat
+ ? Ci.nsIMsgCompFormat.Default
+ : Ci.nsIMsgCompFormat.OppositeOfDefault;
+ }
+
+ let composeWindowPromise = new Promise(resolve => {
+ function listener(event) {
+ let composeWindow = event.target.ownerGlobal;
+ // Skip if this window has been processed already. This already helps
+ // a lot to assign the opened windows in the correct order to the
+ // OpenCompomposeWindow calls.
+ if (composeWindowTracker.has(composeWindow)) {
+ return;
+ }
+ // Do a few more checks to make sure we are looking at the expected
+ // window. This is still a hack. We need to make OpenCompomposeWindow
+ // actually return the opened window.
+ let _msgURI = composeWindow.gMsgCompose.originalMsgURI;
+ let _type = composeWindow.gComposeType;
+ if (_msgURI == msgURI && _type == type) {
+ composeWindowTracker.add(composeWindow);
+ windowTracker.removeListener("compose-editor-ready", listener);
+ resolve(composeWindow);
+ }
+ }
+ windowTracker.addListener("compose-editor-ready", listener);
+ });
+ MailServices.compose.OpenComposeWindow(
+ null,
+ msgHdr,
+ msgURI,
+ type,
+ format,
+ identity,
+ null,
+ null
+ );
+ let composeWindow = await composeWindowPromise;
+
+ if (details) {
+ await setComposeDetails(composeWindow, details, extension);
+ if (details.attachments != null) {
+ let attachmentData = [];
+ for (let data of details.attachments) {
+ attachmentData.push(await createAttachment(data));
+ }
+ await AddAttachmentsToWindow(composeWindow, attachmentData);
+ }
+ }
+ composeWindow.gContentChanged = false;
+ return composeWindow;
+ }
+
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ let composeFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+
+ if (relatedMessageId) {
+ let msgHdr = messageTracker.getMessage(relatedMessageId);
+ params.originalMsgURI = msgHdr.folder.getUriForMsg(msgHdr);
+ }
+
+ params.type = type;
+ params.format = format;
+ if (identity) {
+ params.identity = identity;
+ }
+
+ params.composeFields = composeFields;
+ let composeWindow = Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml",
+ "_blank",
+ "all,chrome,dialog=no,status,toolbar",
+ params
+ );
+ await composeWindowIsReady(composeWindow);
+
+ // Not all details can be set with params for all types, so some need an extra
+ // call to setComposeDetails here. Since we have to use setComposeDetails for
+ // the EditAsNew code path, unify API behavior by always calling it here too.
+ if (details) {
+ await setComposeDetails(composeWindow, details, extension);
+ if (details.attachments != null) {
+ let attachmentData = [];
+ for (let data of details.attachments) {
+ attachmentData.push(await createAttachment(data));
+ }
+ await AddAttachmentsToWindow(composeWindow, attachmentData);
+ }
+ }
+ composeWindow.gContentChanged = false;
+ return composeWindow;
+}
+
+/**
+ * Converts "\r\n" line breaks to "\n" and removes trailing line breaks.
+ *
+ * @param {string} content - original content
+ * @returns {string} - trimmed content
+ */
+function trimContent(content) {
+ let data = content.replaceAll("\r\n", "\n").split("\n");
+ while (data[data.length - 1] == "") {
+ data.pop();
+ }
+ return data.join("\n");
+}
+
+/**
+ * Get the compose details of the requested compose window.
+ *
+ * @param {DOMWindow} composeWindow
+ * @param {ExtensionData} extension
+ * @returns {ComposeDetails}
+ *
+ * @see mail/components/extensions/schemas/compose.json
+ */
+async function getComposeDetails(composeWindow, extension) {
+ let composeFields = composeWindow.GetComposeDetails();
+ let editor = composeWindow.GetCurrentEditor();
+
+ let type;
+ // check all known nsIMsgComposeParams
+ switch (composeWindow.gComposeType) {
+ case Ci.nsIMsgCompType.Draft:
+ type = "draft";
+ break;
+ case Ci.nsIMsgCompType.New:
+ case Ci.nsIMsgCompType.Template:
+ case Ci.nsIMsgCompType.MailToUrl:
+ case Ci.nsIMsgCompType.EditAsNew:
+ case Ci.nsIMsgCompType.EditTemplate:
+ case Ci.nsIMsgCompType.NewsPost:
+ type = "new";
+ break;
+ case Ci.nsIMsgCompType.Reply:
+ case Ci.nsIMsgCompType.ReplyAll:
+ case Ci.nsIMsgCompType.ReplyToSender:
+ case Ci.nsIMsgCompType.ReplyToGroup:
+ case Ci.nsIMsgCompType.ReplyToSenderAndGroup:
+ case Ci.nsIMsgCompType.ReplyWithTemplate:
+ case Ci.nsIMsgCompType.ReplyToList:
+ type = "reply";
+ break;
+ case Ci.nsIMsgCompType.ForwardAsAttachment:
+ case Ci.nsIMsgCompType.ForwardInline:
+ type = "forward";
+ break;
+ case Ci.nsIMsgCompType.Redirect:
+ type = "redirect";
+ break;
+ }
+
+ let relatedMessageId = null;
+ if (composeWindow.gMsgCompose.originalMsgURI) {
+ try {
+ // This throws for messages opened from file and then being replied to.
+ let relatedMsgHdr = composeWindow.gMessenger.msgHdrFromURI(
+ composeWindow.gMsgCompose.originalMsgURI
+ );
+ relatedMessageId = messageTracker.getId(relatedMsgHdr);
+ } catch (ex) {
+ // We are currently unable to get the fake msgHdr from the uri of messages
+ // opened from file.
+ }
+ }
+
+ let customHeaders = [...composeFields.headerNames]
+ .map(h => h.toLowerCase())
+ .filter(h => h.startsWith("x-"))
+ .map(h => {
+ return {
+ // All-lower-case-names are ugly, so capitalize first letters.
+ name: h.replace(/(^|-)[a-z]/g, function (match) {
+ return match.toUpperCase();
+ }),
+ value: composeFields.getHeader(h),
+ };
+ });
+
+ // We have two file carbon copy settings: fcc and fcc2. fcc allows to override
+ // the default identity fcc and fcc2 is coupled to the UI selection.
+ let overrideDefaultFcc = false;
+ if (composeFields.fcc && composeFields.fcc != "") {
+ overrideDefaultFcc = true;
+ }
+ let overrideDefaultFccFolder = "";
+ if (overrideDefaultFcc && !composeFields.fcc.startsWith("nocopy://")) {
+ let folder = MailUtils.getExistingFolder(composeFields.fcc);
+ if (folder) {
+ overrideDefaultFccFolder = convertFolder(folder);
+ }
+ }
+ let additionalFccFolder = "";
+ if (composeFields.fcc2 && !composeFields.fcc2.startsWith("nocopy://")) {
+ let folder = MailUtils.getExistingFolder(composeFields.fcc2);
+ if (folder) {
+ additionalFccFolder = convertFolder(folder);
+ }
+ }
+
+ let deliveryFormat = composeWindow.IsHTMLEditor()
+ ? deliveryFormats.find(f => f.id == composeFields.deliveryFormat).value
+ : null;
+
+ let body = trimContent(
+ editor.outputToString("text/html", Ci.nsIDocumentEncoder.OutputRaw)
+ );
+ let plainTextBody;
+ if (composeWindow.IsHTMLEditor()) {
+ plainTextBody = trimContent(MsgUtils.convertToPlainText(body, true));
+ } else {
+ plainTextBody = parserUtils.convertToPlainText(
+ body,
+ Ci.nsIDocumentEncoder.OutputLFLineBreak,
+ 0
+ );
+ // Remove the extra new line at the end.
+ if (plainTextBody.endsWith("\n")) {
+ plainTextBody = plainTextBody.slice(0, -1);
+ }
+ }
+
+ let details = {
+ from: composeFields.splitRecipients(composeFields.from, false).shift(),
+ to: composeFields.splitRecipients(composeFields.to, false),
+ cc: composeFields.splitRecipients(composeFields.cc, false),
+ bcc: composeFields.splitRecipients(composeFields.bcc, false),
+ overrideDefaultFcc,
+ overrideDefaultFccFolder: overrideDefaultFcc
+ ? overrideDefaultFccFolder
+ : null,
+ additionalFccFolder,
+ type,
+ relatedMessageId,
+ replyTo: composeFields.splitRecipients(composeFields.replyTo, false),
+ followupTo: composeFields.splitRecipients(composeFields.followupTo, false),
+ newsgroups: composeFields.newsgroups
+ ? composeFields.newsgroups.split(",")
+ : [],
+ subject: composeFields.subject,
+ isPlainText: !composeWindow.IsHTMLEditor(),
+ deliveryFormat,
+ body,
+ plainTextBody,
+ customHeaders,
+ priority: composeFields.priority.toLowerCase() || "normal",
+ returnReceipt: composeFields.returnReceipt,
+ deliveryStatusNotification: composeFields.DSN,
+ attachVCard: composeFields.attachVCard,
+ };
+ if (extension.hasPermission("accountsRead")) {
+ details.identityId = composeWindow.getCurrentIdentityKey();
+ }
+ return details;
+}
+
+async function setFromField(composeWindow, details, extension) {
+ if (!details || details.from == null) {
+ return;
+ }
+
+ let from;
+ // Re-throw exceptions from parseComposeRecipientList with a prefix to
+ // minimize developers debugging time and make clear where restrictions are
+ // coming from.
+ try {
+ from = await parseComposeRecipientList(details.from, true);
+ } catch (ex) {
+ throw new ExtensionError(`ComposeDetails.from: ${ex.message}`);
+ }
+ if (!from) {
+ throw new ExtensionError(
+ "ComposeDetails.from: Address must not be set to an empty string."
+ );
+ }
+
+ let identityList = composeWindow.document.getElementById("msgIdentity");
+ // Make the from field editable only, if from differs from the currently shown identity.
+ if (from != identityList.value) {
+ let activeElement = composeWindow.document.activeElement;
+ // Manually update from, using the same approach used in
+ // https://hg.mozilla.org/comm-central/file/1283451c02926e2b7506a6450445b81f6d076f89/mail/components/compose/content/MsgComposeCommands.js#l3621
+ composeWindow.MakeFromFieldEditable(true);
+ identityList.value = from;
+ activeElement.focus();
+ }
+}
+
+/**
+ * Updates the compose details of the specified compose window, overwriting any
+ * property given in the details object.
+ *
+ * @param {DOMWindow} composeWindow
+ * @param {ComposeDetails} details - compose details to update the composer with
+ * @param {ExtensionData} extension
+ *
+ * @see mail/components/extensions/schemas/compose.json
+ */
+async function setComposeDetails(composeWindow, details, extension) {
+ let activeElement = composeWindow.document.activeElement;
+
+ // 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'
+ );
+ }
+
+ let identity = MailServices.accounts.allIdentities.find(
+ i => i.key == details.identityId
+ );
+ if (!identity) {
+ throw new ExtensionError(`Identity not found: ${details.identityId}`);
+ }
+ let identityElement = composeWindow.document.getElementById("msgIdentity");
+ identityElement.selectedItem = [
+ ...identityElement.childNodes[0].childNodes,
+ ].find(e => e.getAttribute("identitykey") == details.identityId);
+ composeWindow.LoadIdentity(false);
+ }
+ for (let 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 (details.overrideDefaultFcc === false) {
+ composeWindow.gMsgCompose.compFields.fcc = "";
+ } else if (details.overrideDefaultFccFolder != null) {
+ // Override identity fcc with enforced value.
+ if (details.overrideDefaultFccFolder) {
+ let uri = folderPathToURI(
+ details.overrideDefaultFccFolder.accountId,
+ details.overrideDefaultFccFolder.path
+ );
+ let folder = MailUtils.getExistingFolder(uri);
+ if (folder) {
+ composeWindow.gMsgCompose.compFields.fcc = uri;
+ } else {
+ throw new ExtensionError(
+ `Invalid MailFolder: {accountId:${details.overrideDefaultFccFolder.accountId}, path:${details.overrideDefaultFccFolder.path}}`
+ );
+ }
+ } else {
+ composeWindow.gMsgCompose.compFields.fcc = "nocopy://";
+ }
+ } else if (
+ details.overrideDefaultFcc === true &&
+ composeWindow.gMsgCompose.compFields.fcc == ""
+ ) {
+ throw new ExtensionError(
+ `Setting overrideDefaultFcc to true requires setting overrideDefaultFccFolder as well`
+ );
+ }
+
+ if (details.additionalFccFolder != null) {
+ if (details.additionalFccFolder) {
+ let uri = folderPathToURI(
+ details.additionalFccFolder.accountId,
+ details.additionalFccFolder.path
+ );
+ let folder = MailUtils.getExistingFolder(uri);
+ if (folder) {
+ composeWindow.gMsgCompose.compFields.fcc2 = uri;
+ } else {
+ throw new ExtensionError(
+ `Invalid MailFolder: {accountId:${details.additionalFccFolder.accountId}, path:${details.additionalFccFolder.path}}`
+ );
+ }
+ } else {
+ composeWindow.gMsgCompose.compFields.fcc2 = "";
+ }
+ }
+
+ // Update custom headers, if specified.
+ if (details.customHeaders) {
+ let newHeaderNames = details.customHeaders.map(h => h.name.toUpperCase());
+ let obsoleteHeaderNames = [
+ ...composeWindow.gMsgCompose.compFields.headerNames,
+ ]
+ .map(h => h.toUpperCase())
+ .filter(h => h.startsWith("X-") && !newHeaderNames.hasOwnProperty(h));
+
+ for (let headerName of obsoleteHeaderNames) {
+ composeWindow.gMsgCompose.compFields.deleteHeader(headerName);
+ }
+ for (let { name, value } of details.customHeaders) {
+ composeWindow.gMsgCompose.compFields.setHeader(name, value);
+ }
+ }
+
+ // Update priorities. The enum in the schema defines all allowed values, no
+ // need to validate here.
+ if (details.priority) {
+ if (details.priority == "normal") {
+ composeWindow.gMsgCompose.compFields.priority = "";
+ } else {
+ composeWindow.gMsgCompose.compFields.priority =
+ details.priority[0].toUpperCase() + details.priority.slice(1);
+ }
+ composeWindow.updatePriorityToolbarButton(
+ composeWindow.gMsgCompose.compFields.priority
+ );
+ }
+
+ // Update receipt notifications.
+ if (details.returnReceipt != null) {
+ composeWindow.ToggleReturnReceipt(details.returnReceipt);
+ }
+
+ if (
+ details.deliveryStatusNotification != null &&
+ details.deliveryStatusNotification !=
+ composeWindow.gMsgCompose.compFields.DSN
+ ) {
+ let 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.
+ composeWindow.gMsgCompose.compFields.deliveryFormat = deliveryFormats.find(
+ f => f.value == details.deliveryFormat
+ ).id;
+ composeWindow.initSendFormatMenu();
+ }
+
+ if (details.attachVCard != null) {
+ composeWindow.gMsgCompose.compFields.attachVCard = details.attachVCard;
+ composeWindow.gAttachVCardOptionChanged = true;
+ }
+
+ activeElement.focus();
+}
+
+async function fileURLForFile(file) {
+ let realFile = await getRealFileForFile(file);
+ return Services.io.newFileURI(realFile).spec;
+}
+
+async function createAttachment(data) {
+ let attachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+
+ if (data.id) {
+ if (!composeAttachmentTracker.hasAttachment(data.id)) {
+ throw new ExtensionError(`Invalid attachment ID: ${data.id}`);
+ }
+
+ let { attachment: originalAttachment, window: originalWindow } =
+ composeAttachmentTracker.getAttachment(data.id);
+
+ let originalAttachmentItem =
+ originalWindow.gAttachmentBucket.findItemForAttachment(
+ originalAttachment
+ );
+
+ attachment.name = data.name || originalAttachment.name;
+ attachment.size = originalAttachment.size;
+ attachment.url = originalAttachment.url;
+
+ return {
+ attachment,
+ originalAttachment,
+ originalCloudFileAccount: originalAttachmentItem.cloudFileAccount,
+ originalCloudFileUpload: originalAttachmentItem.cloudFileUpload,
+ };
+ }
+
+ if (data.file) {
+ attachment.name = data.name || data.file.name;
+ attachment.size = data.file.size;
+ attachment.url = await fileURLForFile(data.file);
+ attachment.contentType = data.file.type;
+ return { attachment };
+ }
+
+ throw new ExtensionError(`Failed to create attachment.`);
+}
+
+async function AddAttachmentsToWindow(window, attachmentData) {
+ await window.AddAttachments(attachmentData.map(a => a.attachment));
+ // Check if an attachment has been cloned and the cloudFileUpload needs to be
+ // re-applied.
+ for (let entry of attachmentData) {
+ let addedAttachmentItem = window.gAttachmentBucket.findItemForAttachment(
+ entry.attachment
+ );
+ if (!addedAttachmentItem) {
+ continue;
+ }
+
+ if (
+ !entry.originalAttachment ||
+ !entry.originalCloudFileAccount ||
+ !entry.originalCloudFileUpload
+ ) {
+ continue;
+ }
+
+ let updateSettings = {
+ cloudFileAccount: entry.originalCloudFileAccount,
+ relatedCloudFileUpload: entry.originalCloudFileUpload,
+ };
+ if (entry.originalAttachment.name != entry.attachment.name) {
+ updateSettings.name = entry.attachment.name;
+ }
+
+ try {
+ await window.UpdateAttachment(addedAttachmentItem, updateSettings);
+ } catch (ex) {
+ throw new ExtensionError(ex.message);
+ }
+ }
+}
+
+var composeStates = {
+ _states: {
+ canSendNow: "cmd_sendNow",
+ canSendLater: "cmd_sendLater",
+ },
+
+ getStates(tab) {
+ let states = {};
+ for (let [state, command] of Object.entries(this._states)) {
+ state[state] = tab.nativeTab.defaultController.isCommandEnabled(command);
+ }
+ return states;
+ },
+
+ // Translate core states (commands) to API states.
+ convert(states) {
+ let converted = {};
+ for (let [state, command] of Object.entries(this._states)) {
+ if (states.hasOwnProperty(command)) {
+ converted[state] = states[command];
+ }
+ }
+ return converted;
+ },
+};
+
+class MsgOperationObserver {
+ constructor(composeWindow) {
+ this.composeWindow = composeWindow;
+ this.savedMessages = [];
+ this.headerMessageId = null;
+ this.deliveryCallbacks = null;
+ this.preparedCallbacks = null;
+ this.classifiedMessages = new Map();
+
+ // The preparedPromise fulfills when the message has been prepared and handed
+ // over to the send process.
+ this.preparedPromise = new Promise((resolve, reject) => {
+ this.preparedCallbacks = { resolve, reject };
+ });
+
+ // The deliveryPromise fulfills when the message has been saved/send.
+ this.deliveryPromise = new Promise((resolve, reject) => {
+ this.deliveryCallbacks = { resolve, reject };
+ });
+
+ Services.obs.addObserver(this, "mail:composeSendProgressStop");
+ this.composeWindow.gMsgCompose.addMsgSendListener(this);
+ MailServices.mfn.addListener(this, MailServices.mfn.msgsClassified);
+ this.composeWindow.addEventListener(
+ "compose-prepare-message-success",
+ event => this.preparedCallbacks.resolve(),
+ { once: true }
+ );
+ this.composeWindow.addEventListener(
+ "compose-prepare-message-failure",
+ event => this.preparedCallbacks.reject(event.detail.exception),
+ { once: true }
+ );
+ }
+
+ // Observer for mail:composeSendProgressStop.
+ observe(subject, topic, data) {
+ let { composeWindow } = subject.wrappedJSObject;
+ if (composeWindow == this.composeWindow) {
+ this.deliveryCallbacks.resolve();
+ }
+ }
+
+ // nsIMsgSendListener
+ onStartSending(msgID, msgSize) {}
+ onProgress(msgID, progress, progressMax) {}
+ onStatus(msgID, msg) {}
+ onStopSending(msgID, status, msg, returnFile) {
+ if (!Components.isSuccessCode(status)) {
+ this.deliveryCallbacks.reject(
+ new ExtensionError("Message operation failed")
+ );
+ return;
+ }
+ // In case of success, this is only called for sendNow, stating the
+ // headerMessageId of the outgoing message.
+ // The msgID starts with < and ends with > which is not used by the API.
+ this.headerMessageId = msgID.replace(/^<|>$/g, "");
+ }
+ onGetDraftFolderURI(msgID, folderURI) {
+ // Only called for save operations and sendLater. Collect messageIds and
+ // folders of saved messages.
+ let headerMessageId = msgID.replace(/^<|>$/g, "");
+ this.savedMessages.push(JSON.stringify({ headerMessageId, folderURI }));
+ }
+ onSendNotPerformed(msgID, status) {}
+ onTransportSecurityError(msgID, status, secInfo, location) {}
+
+ // Implementation for nsIMsgFolderListener::msgsClassified
+ msgsClassified(msgs, junkProcessed, traitProcessed) {
+ // Collect all msgHdrs added to folders during the current message operation.
+ for (let msgHdr of msgs) {
+ let key = JSON.stringify({
+ headerMessageId: msgHdr.messageId,
+ folderURI: msgHdr.folder.URI,
+ });
+ if (!this.classifiedMessages.has(key)) {
+ this.classifiedMessages.set(key, convertMessage(msgHdr));
+ }
+ }
+ }
+
+ /**
+ * @typedef MsgOperationInfo
+ * @property {string} headerMessageId - the id used in the "Message-Id" header
+ * of the outgoing message, only available for the "sendNow" mode
+ * @property {MessageHeader[]} messages - array of WebExtension MessageHeader
+ * objects, with information about saved messages (depends on fcc config)
+ * @see mail/components/extensions/schemas/compose.json
+ */
+
+ /**
+ * Returns a Promise, which resolves once the message operation has finished.
+ *
+ * @returns {Promise<MsgOperationInfo>} - Promise for information about the
+ * performed message operation.
+ */
+ async waitForOperation() {
+ try {
+ await Promise.all([this.deliveryPromise, this.preparedPromise]);
+ return {
+ messages: this.savedMessages
+ .map(m => this.classifiedMessages.get(m))
+ .filter(Boolean),
+ headerMessageId: this.headerMessageId,
+ };
+ } catch (ex) {
+ // In case of error, reject the pending delivery Promise.
+ this.deliveryCallbacks.reject();
+ throw ex;
+ } finally {
+ MailServices.mfn.removeListener(this);
+ Services.obs.removeObserver(this, "mail:composeSendProgressStop");
+ this.composeWindow?.gMsgCompose?.removeMsgSendListener(this);
+ }
+ }
+}
+
+/**
+ * @typedef MsgOperationReturnValue
+ * @property {string} headerMessageId - the id used in the "Message-Id" header
+ * of the outgoing message, only available for the "sendNow" mode
+ * @property {MessageHeader[]} messages - array of WebExtension MessageHeader
+ * objects, with information about saved messages (depends on fcc config)
+ * @see mail/components/extensions/schemas/compose.json
+ * @property {string} mode - the mode of the message operation
+ * @see mail/components/extensions/schemas/compose.json
+ */
+
+/**
+ * Executes the given save/send command. The returned Promise resolves once the
+ * message operation has finished.
+ *
+ * @returns {Promise<MsgOperationReturnValue>} - Promise for information about
+ * the performed message operation, which is passed to the WebExtension.
+ */
+async function goDoCommand(composeWindow, extension, mode) {
+ let commands = new Map([
+ ["draft", "cmd_saveAsDraft"],
+ ["template", "cmd_saveAsTemplate"],
+ ["sendNow", "cmd_sendNow"],
+ ["sendLater", "cmd_sendLater"],
+ ]);
+
+ if (!commands.has(mode)) {
+ throw new ExtensionError(`Unsupported mode: ${mode}`);
+ }
+
+ if (!composeWindow.defaultController.isCommandEnabled(commands.get(mode))) {
+ throw new ExtensionError(
+ `Message compose window not ready for the requested command`
+ );
+ }
+
+ let sendPromise = new Promise((resolve, reject) => {
+ let listener = {
+ onSuccess(window, mode, messages, headerMessageId) {
+ if (window == composeWindow) {
+ afterSaveSendEventTracker.removeListener(listener);
+ let info = { mode, messages };
+ if (mode == "sendNow") {
+ info.headerMessageId = headerMessageId;
+ }
+ resolve(info);
+ }
+ },
+ onFailure(window, mode, exception) {
+ if (window == composeWindow) {
+ afterSaveSendEventTracker.removeListener(listener);
+ reject(exception);
+ }
+ },
+ modes: [mode],
+ extension,
+ };
+ afterSaveSendEventTracker.addListener(listener);
+ });
+
+ // Initiate send.
+ switch (mode) {
+ case "draft":
+ composeWindow.SaveAsDraft();
+ break;
+ case "template":
+ composeWindow.SaveAsTemplate();
+ break;
+ case "sendNow":
+ composeWindow.SendMessage();
+ break;
+ case "sendLater":
+ composeWindow.SendMessageLater();
+ break;
+ }
+ return sendPromise;
+}
+
+var afterSaveSendEventTracker = {
+ listeners: new Set(),
+
+ addListener(listener) {
+ this.listeners.add(listener);
+ },
+ removeListener(listener) {
+ this.listeners.delete(listener);
+ },
+ async handleSuccess(window, mode, messages, headerMessageId) {
+ for (let listener of this.listeners) {
+ if (!listener.modes.includes(mode)) {
+ continue;
+ }
+ await listener.onSuccess(
+ window,
+ mode,
+ messages.map(message => {
+ // Strip data from MessageHeader if this extension doesn't have
+ // the required permission.
+ let clone = Object.assign({}, message);
+ if (!listener.extension.hasPermission("accountsRead")) {
+ delete clone.folders;
+ }
+ return clone;
+ }),
+ headerMessageId
+ );
+ }
+ },
+ async handleFailure(window, mode, exception) {
+ for (let listener of this.listeners) {
+ if (!listener.modes.includes(mode)) {
+ continue;
+ }
+ await listener.onFailure(window, mode, exception);
+ }
+ },
+
+ // Event handler for the "compose-prepare-message-start", which initiates a
+ // new message operation (send or save).
+ handleEvent(event) {
+ let composeWindow = event.target;
+ let msgType = event.detail.msgType;
+
+ let modes = new Map([
+ [Ci.nsIMsgCompDeliverMode.SaveAsDraft, "draft"],
+ [Ci.nsIMsgCompDeliverMode.SaveAsTemplate, "template"],
+ [Ci.nsIMsgCompDeliverMode.Now, "sendNow"],
+ [Ci.nsIMsgCompDeliverMode.Later, "sendLater"],
+ ]);
+ let mode = modes.get(msgType);
+
+ if (mode && this.listeners.size > 0) {
+ let msgOperationObserver = new MsgOperationObserver(composeWindow);
+ msgOperationObserver
+ .waitForOperation()
+ .then(msgOperationInfo =>
+ this.handleSuccess(
+ composeWindow,
+ mode,
+ msgOperationInfo.messages,
+ msgOperationInfo.headerMessageId
+ )
+ )
+ .catch(msgOperationException =>
+ this.handleFailure(composeWindow, mode, msgOperationException)
+ );
+ }
+ },
+};
+windowTracker.addListener(
+ "compose-prepare-message-start",
+ afterSaveSendEventTracker
+);
+
+var beforeSendEventTracker = {
+ listeners: new Set(),
+
+ addListener(listener) {
+ this.listeners.add(listener);
+ if (this.listeners.size == 1) {
+ windowTracker.addListener("beforesend", this);
+ }
+ },
+ removeListener(listener) {
+ this.listeners.delete(listener);
+ if (this.listeners.size == 0) {
+ windowTracker.removeListener("beforesend", this);
+ }
+ },
+ async handleEvent(event) {
+ event.preventDefault();
+
+ let sendPromise = event.detail;
+ let composeWindow = event.target;
+ await composeWindowIsReady(composeWindow);
+ composeWindow.ToggleWindowLock(true);
+
+ // Send process waits till sendPromise.resolve() or sendPromise.reject() is
+ // called.
+
+ for (let { handler, extension } of this.listeners) {
+ let result = await handler(
+ composeWindow,
+ await getComposeDetails(composeWindow, extension)
+ );
+ if (!result) {
+ continue;
+ }
+ if (result.cancel) {
+ composeWindow.ToggleWindowLock(false);
+ sendPromise.reject();
+ return;
+ }
+ if (result.details) {
+ await setComposeDetails(composeWindow, result.details, extension);
+ }
+ }
+
+ // Load the new details into gMsgCompose.compFields for sending.
+ composeWindow.GetComposeDetails();
+
+ composeWindow.ToggleWindowLock(false);
+ sendPromise.resolve();
+ },
+};
+
+var composeAttachmentTracker = {
+ _nextId: 1,
+ _attachments: new Map(),
+ _attachmentIds: new Map(),
+
+ getId(attachment, window) {
+ if (this._attachmentIds.has(attachment)) {
+ return this._attachmentIds.get(attachment).id;
+ }
+ let id = this._nextId++;
+ this._attachments.set(id, { attachment, window });
+ this._attachmentIds.set(attachment, { id, window });
+ return id;
+ },
+
+ getAttachment(id) {
+ return this._attachments.get(id);
+ },
+
+ hasAttachment(id) {
+ return this._attachments.has(id);
+ },
+
+ forgetAttachment(attachment) {
+ // This is called on all attachments when the window closes, whether the
+ // attachments have been assigned IDs or not.
+ let id = this._attachmentIds.get(attachment)?.id;
+ if (id) {
+ this._attachmentIds.delete(attachment);
+ this._attachments.delete(id);
+ }
+ },
+
+ forgetAttachments(window) {
+ if (window.location.href == COMPOSE_WINDOW_URI) {
+ let bucket = window.document.getElementById("attachmentBucket");
+ for (let item of bucket.itemChildren) {
+ this.forgetAttachment(item.attachment);
+ }
+ }
+ },
+
+ convert(attachment, window) {
+ return {
+ id: this.getId(attachment, window),
+ name: attachment.name,
+ size: attachment.size,
+ };
+ },
+
+ getFile(attachment) {
+ if (!attachment) {
+ return null;
+ }
+ let uri = Services.io.newURI(attachment.url).QueryInterface(Ci.nsIFileURL);
+ // Enforce the actual filename used in the composer, do not leak internal or
+ // temporary filenames.
+ return File.createFromNsIFile(uri.file, { name: attachment.name });
+ },
+};
+
+windowTracker.addCloseListener(
+ composeAttachmentTracker.forgetAttachments.bind(composeAttachmentTracker)
+);
+
+var composeWindowTracker = new Set();
+windowTracker.addCloseListener(window => composeWindowTracker.delete(window));
+
+this.compose = class extends ExtensionAPIPersistent {
+ PERSISTENT_EVENTS = {
+ // For primed persistent events (deactivated background), the context is only
+ // available after fire.wakeup() has fulfilled (ensuring the convert() function
+ // has been called).
+
+ onBeforeSend({ context, fire }) {
+ const { extension } = this;
+ const { tabManager, windowManager } = extension;
+ let listener = {
+ async handler(window, details) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ let win = windowManager.wrapWindow(window);
+ return fire.async(
+ tabManager.convert(win.activeTab.nativeTab),
+ details
+ );
+ },
+ extension,
+ };
+
+ beforeSendEventTracker.addListener(listener);
+ return {
+ unregister: () => {
+ beforeSendEventTracker.removeListener(listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onAfterSend({ context, fire }) {
+ const { extension } = this;
+ const { tabManager, windowManager } = extension;
+ let listener = {
+ async onSuccess(window, mode, messages, headerMessageId) {
+ let win = windowManager.wrapWindow(window);
+ let tab = tabManager.convert(win.activeTab.nativeTab);
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ let sendInfo = { mode, messages };
+ if (mode == "sendNow") {
+ sendInfo.headerMessageId = headerMessageId;
+ }
+ return fire.async(tab, sendInfo);
+ },
+ async onFailure(window, mode, exception) {
+ let win = windowManager.wrapWindow(window);
+ let tab = tabManager.convert(win.activeTab.nativeTab);
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ return fire.async(tab, {
+ mode,
+ messages: [],
+ error: exception.message,
+ });
+ },
+ modes: ["sendNow", "sendLater"],
+ extension,
+ };
+ afterSaveSendEventTracker.addListener(listener);
+ return {
+ unregister: () => {
+ afterSaveSendEventTracker.removeListener(listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onAfterSave({ context, fire }) {
+ const { extension } = this;
+ const { tabManager, windowManager } = extension;
+ let listener = {
+ async onSuccess(window, mode, messages, headerMessageId) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ let win = windowManager.wrapWindow(window);
+ let saveInfo = { mode, messages };
+ return fire.async(
+ tabManager.convert(win.activeTab.nativeTab),
+ saveInfo
+ );
+ },
+ async onFailure(window, mode, exception) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ let win = windowManager.wrapWindow(window);
+ return fire.async(tabManager.convert(win.activeTab.nativeTab), {
+ mode,
+ messages: [],
+ error: exception.message,
+ });
+ },
+ modes: ["draft", "template"],
+ extension,
+ };
+ afterSaveSendEventTracker.addListener(listener);
+ return {
+ unregister: () => {
+ afterSaveSendEventTracker.removeListener(listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onAttachmentAdded({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ async function listener(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ for (let attachment of event.detail) {
+ attachment = composeAttachmentTracker.convert(
+ attachment,
+ event.target.ownerGlobal
+ );
+ fire.async(tabManager.convert(event.target.ownerGlobal), attachment);
+ }
+ }
+ windowTracker.addListener("attachments-added", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("attachments-added", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onAttachmentRemoved({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ async function listener(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ for (let attachment of event.detail) {
+ let attachmentId = composeAttachmentTracker.getId(
+ attachment,
+ event.target.ownerGlobal
+ );
+ fire.async(
+ tabManager.convert(event.target.ownerGlobal),
+ attachmentId
+ );
+ composeAttachmentTracker.forgetAttachment(attachment);
+ }
+ }
+ windowTracker.addListener("attachments-removed", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("attachments-removed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onIdentityChanged({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ async function listener(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(
+ tabManager.convert(event.target.ownerGlobal),
+ event.target.getCurrentIdentityKey()
+ );
+ }
+ windowTracker.addListener("compose-from-changed", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("compose-from-changed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onComposeStateChanged({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ async function listener(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(
+ tabManager.convert(event.target.ownerGlobal),
+ composeStates.convert(event.detail)
+ );
+ }
+ windowTracker.addListener("compose-state-changed", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("compose-state-changed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onActiveDictionariesChanged({ context, fire }) {
+ const { extension } = this;
+ const { tabManager } = extension;
+ async function listener(event) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ let activeDictionaries = event.detail.split(",");
+ fire.async(
+ tabManager.convert(event.target.ownerGlobal),
+ Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine)
+ .getDictionaryList()
+ .reduce((list, dict) => {
+ list[dict] = activeDictionaries.includes(dict);
+ return list;
+ }, {})
+ );
+ }
+ windowTracker.addListener("active-dictionaries-changed", listener);
+ return {
+ unregister: () => {
+ windowTracker.removeListener("active-dictionaries-changed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ };
+
+ getAPI(context) {
+ /**
+ * Guard to make sure the API waits until the compose tab has been fully loaded,
+ * to cope with tabs.onCreated returning tabs very early.
+ *
+ * @param {integer} tabId
+ * @returns {Tab} a fully loaded messageCompose tab
+ */
+ async function getComposeTab(tabId) {
+ let tab = tabManager.get(tabId);
+ if (tab.type != "messageCompose") {
+ throw new ExtensionError(`Invalid compose tab: ${tabId}`);
+ }
+ await composeWindowIsReady(tab.nativeTab);
+ return tab;
+ }
+
+ let { extension } = context;
+ let { tabManager } = extension;
+
+ return {
+ compose: {
+ onBeforeSend: new EventManager({
+ context,
+ module: "compose",
+ event: "onBeforeSend",
+ inputHandling: true,
+ extensionApi: this,
+ }).api(),
+ onAfterSend: new EventManager({
+ context,
+ module: "compose",
+ event: "onAfterSend",
+ inputHandling: true,
+ extensionApi: this,
+ }).api(),
+ onAfterSave: new EventManager({
+ context,
+ module: "compose",
+ event: "onAfterSave",
+ inputHandling: true,
+ extensionApi: this,
+ }).api(),
+ onAttachmentAdded: new ExtensionCommon.EventManager({
+ context,
+ module: "compose",
+ event: "onAttachmentAdded",
+ extensionApi: this,
+ }).api(),
+ onAttachmentRemoved: new ExtensionCommon.EventManager({
+ context,
+ module: "compose",
+ event: "onAttachmentRemoved",
+ extensionApi: this,
+ }).api(),
+ onIdentityChanged: new ExtensionCommon.EventManager({
+ context,
+ module: "compose",
+ event: "onIdentityChanged",
+ extensionApi: this,
+ }).api(),
+ onComposeStateChanged: new ExtensionCommon.EventManager({
+ context,
+ module: "compose",
+ event: "onComposeStateChanged",
+ extensionApi: this,
+ }).api(),
+ onActiveDictionariesChanged: new ExtensionCommon.EventManager({
+ context,
+ module: "compose",
+ event: "onActiveDictionariesChanged",
+ extensionApi: this,
+ }).api(),
+ async beginNew(messageId, details) {
+ let type = Ci.nsIMsgCompType.New;
+ if (messageId) {
+ let msgHdr = messageTracker.getMessage(messageId);
+ type =
+ msgHdr.flags & Ci.nsMsgMessageFlags.Template
+ ? Ci.nsIMsgCompType.Template
+ : Ci.nsIMsgCompType.EditAsNew;
+ }
+ let composeWindow = await openComposeWindow(
+ messageId,
+ type,
+ details,
+ extension
+ );
+ return tabManager.convert(composeWindow);
+ },
+ async beginReply(messageId, replyType, details) {
+ let type = Ci.nsIMsgCompType.Reply;
+ if (replyType == "replyToList") {
+ type = Ci.nsIMsgCompType.ReplyToList;
+ } else if (replyType == "replyToAll") {
+ type = Ci.nsIMsgCompType.ReplyAll;
+ }
+ let composeWindow = await openComposeWindow(
+ messageId,
+ type,
+ details,
+ extension
+ );
+ return tabManager.convert(composeWindow);
+ },
+ async beginForward(messageId, forwardType, details) {
+ let type = Ci.nsIMsgCompType.ForwardInline;
+ if (forwardType == "forwardAsAttachment") {
+ type = Ci.nsIMsgCompType.ForwardAsAttachment;
+ } else if (
+ forwardType === null &&
+ Services.prefs.getIntPref("mail.forward_message_mode") == 0
+ ) {
+ type = Ci.nsIMsgCompType.ForwardAsAttachment;
+ }
+ let composeWindow = await openComposeWindow(
+ messageId,
+ type,
+ details,
+ extension
+ );
+ return tabManager.convert(composeWindow);
+ },
+ async saveMessage(tabId, options) {
+ let tab = await getComposeTab(tabId);
+ let saveMode = options?.mode || "draft";
+
+ try {
+ return await goDoCommand(
+ tab.nativeTab,
+ context.extension,
+ saveMode
+ );
+ } catch (ex) {
+ throw new ExtensionError(
+ `compose.saveMessage failed: ${ex.message}`
+ );
+ }
+ },
+ async sendMessage(tabId, options) {
+ let tab = await getComposeTab(tabId);
+ let sendMode = options?.mode;
+ if (!["sendLater", "sendNow"].includes(sendMode)) {
+ sendMode = Services.io.offline ? "sendLater" : "sendNow";
+ }
+
+ try {
+ return await goDoCommand(
+ tab.nativeTab,
+ context.extension,
+ sendMode
+ );
+ } catch (ex) {
+ throw new ExtensionError(
+ `compose.sendMessage failed: ${ex.message}`
+ );
+ }
+ },
+ async getComposeState(tabId) {
+ let tab = await getComposeTab(tabId);
+ return composeStates.getStates(tab);
+ },
+ async getComposeDetails(tabId) {
+ let tab = await getComposeTab(tabId);
+ return getComposeDetails(tab.nativeTab, extension);
+ },
+ async setComposeDetails(tabId, details) {
+ let tab = await getComposeTab(tabId);
+ return setComposeDetails(tab.nativeTab, details, extension);
+ },
+ async getActiveDictionaries(tabId) {
+ let tab = await getComposeTab(tabId);
+ let dictionaries = tab.nativeTab.gActiveDictionaries;
+
+ // Return the list of installed dictionaries, setting those who are
+ // enabled to true.
+ return Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine)
+ .getDictionaryList()
+ .reduce((list, dict) => {
+ list[dict] = dictionaries.has(dict);
+ return list;
+ }, {});
+ },
+ async setActiveDictionaries(tabId, activeDictionaries) {
+ let tab = await getComposeTab(tabId);
+ let installedDictionaries = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine)
+ .getDictionaryList();
+
+ for (let dict of activeDictionaries) {
+ if (!installedDictionaries.includes(dict)) {
+ throw new ExtensionError(`Dictionary not found: ${dict}`);
+ }
+ }
+
+ await tab.nativeTab.ComposeChangeLanguage(activeDictionaries);
+ },
+ async listAttachments(tabId) {
+ let tab = await getComposeTab(tabId);
+
+ let bucket =
+ tab.nativeTab.document.getElementById("attachmentBucket");
+ let attachments = [];
+ for (let item of bucket.itemChildren) {
+ attachments.push(
+ composeAttachmentTracker.convert(item.attachment, tab.nativeTab)
+ );
+ }
+ return attachments;
+ },
+ async getAttachmentFile(attachmentId) {
+ if (!composeAttachmentTracker.hasAttachment(attachmentId)) {
+ throw new ExtensionError(`Invalid attachment: ${attachmentId}`);
+ }
+ let { attachment } =
+ composeAttachmentTracker.getAttachment(attachmentId);
+ return composeAttachmentTracker.getFile(attachment);
+ },
+ async addAttachment(tabId, data) {
+ let tab = await getComposeTab(tabId);
+ let attachmentData = await createAttachment(data);
+ await AddAttachmentsToWindow(tab.nativeTab, [attachmentData]);
+ return composeAttachmentTracker.convert(
+ attachmentData.attachment,
+ tab.nativeTab
+ );
+ },
+ async updateAttachment(tabId, attachmentId, data) {
+ let tab = await getComposeTab(tabId);
+ if (!composeAttachmentTracker.hasAttachment(attachmentId)) {
+ throw new ExtensionError(`Invalid attachment: ${attachmentId}`);
+ }
+ let { attachment, window } =
+ composeAttachmentTracker.getAttachment(attachmentId);
+ if (window != tab.nativeTab) {
+ throw new ExtensionError(
+ `Attachment ${attachmentId} is not associated with tab ${tabId}`
+ );
+ }
+
+ let attachmentItem =
+ window.gAttachmentBucket.findItemForAttachment(attachment);
+ if (!attachmentItem) {
+ throw new ExtensionError(`Unexpected invalid attachment item`);
+ }
+
+ if (!data.file && !data.name) {
+ throw new ExtensionError(
+ `Either data.file or data.name property must be specified`
+ );
+ }
+
+ let realFile = data.file ? await getRealFileForFile(data.file) : null;
+ try {
+ await window.UpdateAttachment(attachmentItem, {
+ file: realFile,
+ name: data.name,
+ relatedCloudFileUpload: attachmentItem.cloudFileUpload,
+ });
+ } catch (ex) {
+ throw new ExtensionError(ex.message);
+ }
+
+ return composeAttachmentTracker.convert(attachmentItem.attachment);
+ },
+ async removeAttachment(tabId, attachmentId) {
+ let tab = await getComposeTab(tabId);
+ if (!composeAttachmentTracker.hasAttachment(attachmentId)) {
+ throw new ExtensionError(`Invalid attachment: ${attachmentId}`);
+ }
+ let { attachment, window } =
+ composeAttachmentTracker.getAttachment(attachmentId);
+ if (window != tab.nativeTab) {
+ throw new ExtensionError(
+ `Attachment ${attachmentId} is not associated with tab ${tabId}`
+ );
+ }
+
+ let item = window.gAttachmentBucket.findItemForAttachment(attachment);
+ await window.RemoveAttachments([item]);
+ },
+ },
+ };
+ }
+};