diff --git a/mail/components/extensions/ExtensionAccounts.sys.mjs b/mail/components/extensions/ExtensionAccounts.sys.mjs new file mode 100644 index 0000000000..532fa3b4c0 --- /dev/null +++ b/mail/components/extensions/ExtensionAccounts.sys.mjs @@ -0,0 +1,221 @@ +/* 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 { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * Converts an nsIMsgAccount to a simple object + * + * @param {nsIMsgAccount} account + * @returns {object} + */ +export function convertAccount(account, includeFolders = true) { + if (!account) { + return null; + } + + account = account.QueryInterface(Ci.nsIMsgAccount); + let server = account.incomingServer; + if (server.type == "im") { + return null; + } + + let folders = null; + if (includeFolders) { + folders = traverseSubfolders( + account.incomingServer.rootFolder, + account.key + ).subFolders; + } + + return { + id: account.key, + name: account.incomingServer.prettyName, + type: account.incomingServer.type, + folders, + identities: account.identities.map(identity => + convertMailIdentity(account, identity) + ), + }; +} + +/** + * Converts an nsIMsgIdentity to a simple object for use in messages. + * + * @param {nsIMsgAccount} account + * @param {nsIMsgIdentity} identity + * @returns {object} + */ +export function convertMailIdentity(account, identity) { + if (!account || !identity) { + return null; + } + identity = identity.QueryInterface(Ci.nsIMsgIdentity); + return { + accountId: account.key, + id: identity.key, + label: identity.label || "", + name: identity.fullName || "", + email: identity.email || "", + replyTo: identity.replyTo || "", + organization: identity.organization || "", + composeHtml: identity.composeHtml, + signature: identity.htmlSigText || "", + signatureIsPlainText: !identity.htmlSigFormat, + }; +} + +/** + * The following functions turn nsIMsgFolder references into more human-friendly forms. + * A folder can be referenced with the account key, and the path to the folder in that account. + */ + +/** + * Convert a folder URI to a human-friendly path. + * + * @returns {string} + */ +export function folderURIToPath(accountId, uri) { + let server = MailServices.accounts.getAccount(accountId).incomingServer; + let rootURI = server.rootFolder.URI; + if (rootURI == uri) { + return "/"; + } + // The .URI property of an IMAP folder doesn't have %-encoded characters, but + // may include literal % chars. Services.io.newURI(uri) applies encodeURI to + // the returned filePath, but will not encode any literal % chars, which will + // cause decodeURIComponent to fail (bug 1707408). + if (server.type == "imap") { + return uri.substring(rootURI.length); + } + let path = Services.io.newURI(uri).filePath; + return path.split("/").map(decodeURIComponent).join("/"); +} + +/** + * Convert a human-friendly path to a folder URI. This function does not assume + * that the folder referenced exists. + * + * @returns {string} + */ +export function folderPathToURI(accountId, path) { + let server = MailServices.accounts.getAccount(accountId).incomingServer; + let rootURI = server.rootFolder.URI; + if (path == "/") { + return rootURI; + } + // The .URI property of an IMAP folder doesn't have %-encoded characters. + // If encoded here, the folder lookup service won't find the folder. + if (server.type == "imap") { + return rootURI + path; + } + return ( + rootURI + + path + .split("/") + .map(p => + encodeURIComponent(p) + .replace(/[!'()*]/g, c => "%" + c.charCodeAt(0).toString(16)) + // We do not encode "+" chars in folder URIs. Manually convert them + // back to literal + chars, otherwise folder lookup will fail. + .replaceAll("%2B", "+") + ) + .join("/") + ); +} + +const folderTypeMap = new Map([ + [Ci.nsMsgFolderFlags.Inbox, "inbox"], + [Ci.nsMsgFolderFlags.Drafts, "drafts"], + [Ci.nsMsgFolderFlags.SentMail, "sent"], + [Ci.nsMsgFolderFlags.Trash, "trash"], + [Ci.nsMsgFolderFlags.Templates, "templates"], + [Ci.nsMsgFolderFlags.Archive, "archives"], + [Ci.nsMsgFolderFlags.Junk, "junk"], + [Ci.nsMsgFolderFlags.Queue, "outbox"], +]); + +/** + * Converts an nsIMsgFolder to a simple object for use in API messages. + * + * @param {nsIMsgFolder} folder - The folder to convert. + * @param {string} [accountId] - An optimization to avoid looking up the + * account. The value from nsIMsgDBHdr.accountKey must not be used here. + * @returns {MailFolder} + * @see mail/components/extensions/schemas/folders.json + */ +export function convertFolder(folder, accountId) { + if (!folder) { + return null; + } + if (!accountId) { + let server = folder.server; + let account = MailServices.accounts.FindAccountForServer(server); + accountId = account.key; + } + + let folderObject = { + accountId, + name: folder.prettyName, + path: folderURIToPath(accountId, folder.URI), + }; + + let flags = folder.flags; + for (let [flag, typeName] of folderTypeMap.entries()) { + if (flags & flag) { + folderObject.type = typeName; + // Exit the loop as soon as an entry was found. + break; + } + } + + return folderObject; +} + +/** + * Converts an nsIMsgFolder and all its subfolders to a simple object for use in + * API messages. + * + * @param {nsIMsgFolder} folder - The folder to convert. + * @param {string} [accountId] - An optimization to avoid looking up the + * account. The value from nsIMsgDBHdr.accountKey must not be used here. + * @returns {MailFolder} + * @see mail/components/extensions/schemas/folders.json + */ +export function traverseSubfolders(folder, accountId) { + let f = convertFolder(folder, accountId); + f.subFolders = []; + if (folder.hasSubFolders) { + // Use the same order as used by Thunderbird. + let subFolders = [...folder.subFolders].sort((a, b) => + a.sortOrder == b.sortOrder + ? a.name.localeCompare(b.name) + : a.sortOrder - b.sortOrder + ); + for (let subFolder of subFolders) { + f.subFolders.push( + traverseSubfolders(subFolder, accountId || f.accountId) + ); + } + } + return f; +} + +export class FolderManager { + constructor(extension) { + this.extension = extension; + } + + convert(folder, accountId) { + return convertFolder(folder, accountId); + } + + get(accountId, path) { + return MailServices.folderLookup.getFolderForURL( + folderPathToURI(accountId, path) + ); + } +} diff --git a/mail/components/extensions/ExtensionMessages.sys.mjs b/mail/components/extensions/ExtensionMessages.sys.mjs new file mode 100644 index 0000000000..ccb8a9d565 --- /dev/null +++ b/mail/components/extensions/ExtensionMessages.sys.mjs @@ -0,0 +1,872 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; +import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; +import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +import { convertFolder } from "resource:///modules/ExtensionAccounts.sys.mjs"; + +var { ExtensionError } = ExtensionUtils; +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gJunkThreshold", + "mail.adaptivefilters.junk_threshold", + 90 +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gMessagesPerPage", + "extensions.webextensions.messagesPerPage", + 100 +); + +/** + * Class for cached message headers to reduce XPCOM requests and to cache msgHdr + * of file and attachment messages. + */ +export class CachedMsgHeader { + constructor(msgHdr) { + this.mProperties = {}; + + // Properties needed by convertMessage(). + this.author = null; + this.subject = ""; + this.recipients = null; + this.ccList = null; + this.bccList = null; + this.messageId = null; + this.date = 0; + this.flags = 0; + this.isRead = false; + this.isFlagged = false; + this.messageSize = 0; + this.folder = null; + + // Additional properties. + this.accountKey = ""; + + if (msgHdr) { + // Cache all elements which are needed by convertMessage(). + this.author = msgHdr.mime2DecodedAuthor; + this.subject = msgHdr.mime2DecodedSubject; + this.recipients = msgHdr.mime2DecodedRecipients; + this.ccList = msgHdr.ccList; + this.bccList = msgHdr.bccList; + this.messageId = msgHdr.messageId; + this.date = msgHdr.date; + this.flags = msgHdr.flags; + this.isRead = msgHdr.isRead; + this.isFlagged = msgHdr.isFlagged; + this.messageSize = msgHdr.messageSize; + this.folder = msgHdr.folder; + + this.mProperties.junkscore = msgHdr.getStringProperty("junkscore"); + this.mProperties.keywords = msgHdr.getStringProperty("keywords"); + + if (this.folder) { + this.messageKey = msgHdr.messageKey; + } else { + this.mProperties.dummyMsgUrl = msgHdr.getStringProperty("dummyMsgUrl"); + this.mProperties.dummyMsgLastModifiedTime = msgHdr.getUint32Property( + "dummyMsgLastModifiedTime" + ); + } + + // Also cache the additional elements. + this.accountKey = msgHdr.accountKey; + } + } + + getProperty(aProperty) { + return this.getStringProperty(aProperty); + } + setProperty(aProperty, aVal) { + return this.setStringProperty(aProperty, aVal); + } + getStringProperty(aProperty) { + if (this.mProperties.hasOwnProperty(aProperty)) { + return this.mProperties[aProperty]; + } + return ""; + } + setStringProperty(aProperty, aVal) { + this.mProperties[aProperty] = aVal; + } + getUint32Property(aProperty) { + if (this.mProperties.hasOwnProperty(aProperty)) { + return parseInt(this.mProperties[aProperty]); + } + return 0; + } + setUint32Property(aProperty, aVal) { + this.mProperties[aProperty] = aVal.toString(); + } + markHasAttachments(hasAttachments) {} + get mime2DecodedAuthor() { + return this.author; + } + get mime2DecodedSubject() { + return this.subject; + } + get mime2DecodedRecipients() { + return this.recipients; + } +} + +/** + * Checks if the provided dummyMsgUrl belongs to an attached message. + */ +function isAttachedMessageUrl(dummyMsgUrl) { + try { + return dummyMsgUrl && new URL(dummyMsgUrl).searchParams.has("part"); + } catch (ex) { + return false; + } +} + +/** + * A map of numeric identifiers to messages for easy reference. + * + * @implements {nsIFolderListener} + * @implements {nsIMsgFolderListener} + * @implements {nsIObserver} + */ +export class MessageTracker extends EventEmitter { + constructor(windowTracker) { + super(); + this._nextId = 1; + this._messages = new Map(); + this._messageIds = new Map(); + this._listenerCount = 0; + this._pendingKeyChanges = new Map(); + this._dummyMessageHeaders = new Map(); + this._windowTracker = windowTracker; + + // nsIObserver + Services.obs.addObserver(this, "quit-application-granted"); + Services.obs.addObserver(this, "attachment-delete-msgkey-changed"); + // nsIFolderListener + MailServices.mailSession.AddFolderListener( + this, + Ci.nsIFolderListener.propertyFlagChanged | + Ci.nsIFolderListener.intPropertyChanged + ); + // nsIMsgFolderListener + MailServices.mfn.addListener( + this, + MailServices.mfn.msgsJunkStatusChanged | + MailServices.mfn.msgsDeleted | + MailServices.mfn.msgsMoveCopyCompleted | + MailServices.mfn.msgKeyChanged + ); + + this._messageOpenListenerRegistered = false; + try { + this._windowTracker.addListener("MsgLoaded", this); + this._messageOpenListenerRegistered = true; + } catch (ex) { + // Fails during XPCSHELL tests, which mock the WindowWatcher but do not + // implement registerNotification. + } + } + + // Event handler for MsgLoaded event. + handleEvent(event) { + let msgHdr = event.detail; + // It is not possible to retrieve the dummyMsgHdr of messages opened + // from file at a later time, track them manually. + if ( + msgHdr && + !msgHdr.folder && + msgHdr.getStringProperty("dummyMsgUrl").startsWith("file://") + ) { + this.getId(msgHdr); + } + } + + cleanup() { + // nsIObserver + Services.obs.removeObserver(this, "quit-application-granted"); + Services.obs.removeObserver(this, "attachment-delete-msgkey-changed"); + // nsIFolderListener + MailServices.mailSession.RemoveFolderListener(this); + // nsIMsgFolderListener + MailServices.mfn.removeListener(this); + if (this._messageOpenListenerRegistered) { + this._windowTracker.removeListener("MsgLoaded", this); + this._messageOpenListenerRegistered = false; + } + } + + /** + * Generates a hash for the given msgIdentifier. + * + * @param {*} msgIdentifier + * @returns {string} + */ + getHash(msgIdentifier) { + if (msgIdentifier.folderURI) { + return `folderURI:${msgIdentifier.folderURI}, messageKey: ${msgIdentifier.messageKey}`; + } + return `dummyMsgUrl:${msgIdentifier.dummyMsgUrl}, dummyMsgLastModifiedTime: ${msgIdentifier.dummyMsgLastModifiedTime}`; + } + + /** + * Maps the provided message identifier to the given messageTracker id. + * + * @param {integer} id - messageTracker id of the message + * @param {*} msgIdentifier - msgIdentifier of the message + * @param {nsIMsgDBHdr} [msgHdr] - optional msgHdr of the message, will be + * added to the cache if it is a dummy msgHdr (a file or attachment message) + */ + _set(id, msgIdentifier, msgHdr) { + let hash = this.getHash(msgIdentifier); + this._messageIds.set(hash, id); + this._messages.set(id, msgIdentifier); + // Keep track of dummy message headers, which do not have a folder property + // and cannot be retrieved later. + if (msgHdr && !msgHdr.folder && msgIdentifier.dummyMsgUrl) { + this._dummyMessageHeaders.set( + msgIdentifier.dummyMsgUrl, + msgHdr instanceof Ci.nsIMsgDBHdr ? new CachedMsgHeader(msgHdr) : msgHdr + ); + } + } + + /** + * Lookup the messageTracker id for the given message identifier, return null + * if not known. + * + * @param {*} msgIdentifier - msgIdentifier of the message + * @returns {integer} The messageTracker id of the message. + */ + _get(msgIdentifier) { + let hash = this.getHash(msgIdentifier); + if (this._messageIds.has(hash)) { + return this._messageIds.get(hash); + } + return null; + } + + /** + * Removes the provided message identifier from the messageTracker. + * + * @param {*} msgIdentifier - msgIdentifier of the message + */ + _remove(msgIdentifier) { + let hash = this.getHash(msgIdentifier); + let id = this._get(msgIdentifier); + this._messages.delete(id); + this._messageIds.delete(hash); + this._dummyMessageHeaders.delete(msgIdentifier.dummyMsgUrl); + } + + /** + * Finds a message in the messageTracker or adds it. + * + * @param {nsIMsgDBHdr} - msgHdr of the requested message + * @returns {integer} The messageTracker id of the message. + */ + getId(msgHdr) { + let msgIdentifier; + if (msgHdr.folder) { + msgIdentifier = { + folderURI: msgHdr.folder.URI, + messageKey: msgHdr.messageKey, + }; + } else { + // Normalize the dummyMsgUrl by sorting its parameters and striping them + // to a minimum. + let url = new URL(msgHdr.getStringProperty("dummyMsgUrl")); + let parameters = Array.from(url.searchParams, p => p[0]).filter( + p => !["group", "number", "key", "part"].includes(p) + ); + for (let parameter of parameters) { + url.searchParams.delete(parameter); + } + url.searchParams.sort(); + + msgIdentifier = { + dummyMsgUrl: url.href, + dummyMsgLastModifiedTime: msgHdr.getUint32Property( + "dummyMsgLastModifiedTime" + ), + }; + } + + let id = this._get(msgIdentifier); + if (id) { + return id; + } + id = this._nextId++; + + this._set(id, msgIdentifier, msgHdr); + return id; + } + + /** + * Check if the provided msgIdentifier belongs to a modified file message. + * + * @param {*} msgIdentifier - msgIdentifier object of the message + * @returns {boolean} + */ + isModifiedFileMsg(msgIdentifier) { + if (!msgIdentifier.dummyMsgUrl?.startsWith("file://")) { + return false; + } + + try { + let file = Services.io + .newURI(msgIdentifier.dummyMsgUrl) + .QueryInterface(Ci.nsIFileURL).file; + if (!file?.exists()) { + throw new ExtensionError("File does not exist"); + } + if ( + msgIdentifier.dummyMsgLastModifiedTime && + Math.floor(file.lastModifiedTime / 1000000) != + msgIdentifier.dummyMsgLastModifiedTime + ) { + throw new ExtensionError("File has been modified"); + } + } catch (ex) { + console.error(ex); + return true; + } + return false; + } + + /** + * Retrieves a message from the messageTracker. If the message no longer, + * exists it is removed from the messageTracker. + * + * @param {integer} id - messageTracker id of the message + * @returns {nsIMsgDBHdr} The identifier of the message. + */ + getMessage(id) { + let msgIdentifier = this._messages.get(id); + if (!msgIdentifier) { + return null; + } + + if (msgIdentifier.folderURI) { + let folder = MailServices.folderLookup.getFolderForURL( + msgIdentifier.folderURI + ); + if (folder) { + let msgHdr = folder.msgDatabase.getMsgHdrForKey( + msgIdentifier.messageKey + ); + if (msgHdr) { + return msgHdr; + } + } + } else { + let msgHdr = this._dummyMessageHeaders.get(msgIdentifier.dummyMsgUrl); + if (msgHdr && !this.isModifiedFileMsg(msgIdentifier)) { + return msgHdr; + } + } + + this._remove(msgIdentifier); + return null; + } + + /** + * Converts an nsIMsgDBHdr to a simple object for use in messages. + * This function WILL change as the API develops. + * + * @param {nsIMsgDBHdr} msgHdr + * @param {ExtensionData} extension + * + * @returns {MessageHeader} MessageHeader object + * + * @see /mail/components/extensions/schemas/messages.json + */ + convertMessage(msgHdr, extension) { + if (!msgHdr) { + return null; + } + + let composeFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + // Cache msgHdr to reduce XPCOM requests. + let cachedHdr = new CachedMsgHeader(msgHdr); + + let junkScore = parseInt(cachedHdr.getStringProperty("junkscore"), 10) || 0; + let tags = (cachedHdr.getStringProperty("keywords") || "") + .split(" ") + .filter(MailServices.tags.isValidKey); + + // Getting the size of attached messages does not work consistently. For imap:// + // and mailbox:// messages the returned size in msgHdr.messageSize is 0, and for + // file:// messages the returned size is always the total file size + // Be consistent here and always return 0. The user can obtain the message size + // from the size of the associated attachment file. + let size = isAttachedMessageUrl(cachedHdr.getStringProperty("dummyMsgUrl")) + ? 0 + : cachedHdr.messageSize; + + let messageObject = { + id: this.getId(cachedHdr), + date: new Date(Math.round(cachedHdr.date / 1000)), + author: cachedHdr.mime2DecodedAuthor, + recipients: cachedHdr.mime2DecodedRecipients + ? composeFields.splitRecipients(cachedHdr.mime2DecodedRecipients, false) + : [], + ccList: cachedHdr.ccList + ? composeFields.splitRecipients(cachedHdr.ccList, false) + : [], + bccList: cachedHdr.bccList + ? composeFields.splitRecipients(cachedHdr.bccList, false) + : [], + subject: cachedHdr.mime2DecodedSubject, + read: cachedHdr.isRead, + new: !!(cachedHdr.flags & Ci.nsMsgMessageFlags.New), + headersOnly: !!(cachedHdr.flags & Ci.nsMsgMessageFlags.Partial), + flagged: !!cachedHdr.isFlagged, + junk: junkScore >= lazy.gJunkThreshold, + junkScore, + headerMessageId: cachedHdr.messageId, + size, + tags, + external: !cachedHdr.folder, + }; + // convertMessage can be called without providing an extension, if the info is + // needed for multiple extensions. The caller has to ensure that the folder info + // is not forwarded to extensions, which do not have the required permission. + if ( + cachedHdr.folder && + (!extension || extension.hasPermission("accountsRead")) + ) { + messageObject.folder = convertFolder(cachedHdr.folder); + } + return messageObject; + } + + // nsIFolderListener + + onFolderPropertyFlagChanged(item, property, oldFlag, newFlag) { + let changes = {}; + switch (property) { + case "Status": + if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.Read) { + changes.read = item.isRead; + } + if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.New) { + changes.new = !!(newFlag & Ci.nsMsgMessageFlags.New); + } + break; + case "Flagged": + changes.flagged = item.isFlagged; + break; + case "Keywords": + { + let tags = item.getStringProperty("keywords"); + tags = tags ? tags.split(" ") : []; + changes.tags = tags.filter(MailServices.tags.isValidKey); + } + break; + } + if (Object.keys(changes).length) { + this.emit("message-updated", item, changes); + } + } + + onFolderIntPropertyChanged(folder, property, oldValue, newValue) { + switch (property) { + case "BiffState": + if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) { + // The folder argument is a root folder. + this.findNewMessages(folder); + } + break; + case "NewMailReceived": + // The folder argument is a real folder. + this.findNewMessages(folder); + break; + } + } + + /** + * Finds all folders with new messages in the specified changedFolder and + * returns those. + * + * @see MailNotificationManager._getFirstRealFolderWithNewMail() + */ + findNewMessages(changedFolder) { + let folders = changedFolder.descendants; + folders.unshift(changedFolder); + for (let folder of folders) { + let flags = folder.flags; + if ( + !(flags & Ci.nsMsgFolderFlags.Inbox) && + flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual) + ) { + // Do not notify if the folder is not Inbox but one of + // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual. + continue; + } + let numNewMessages = folder.getNumNewMessages(false); + if (!numNewMessages) { + continue; + } + let msgDb = folder.msgDatabase; + let newMsgKeys = msgDb.getNewList().slice(-numNewMessages); + if (newMsgKeys.length == 0) { + continue; + } + this.emit( + "messages-received", + folder, + newMsgKeys.map(key => msgDb.getMsgHdrForKey(key)) + ); + } + } + + // nsIMsgFolderListener + + msgsJunkStatusChanged(messages) { + for (let msgHdr of messages) { + let junkScore = parseInt(msgHdr.getStringProperty("junkscore"), 10) || 0; + this.emit("message-updated", msgHdr, { + junk: junkScore >= lazy.gJunkThreshold, + }); + } + } + + msgsDeleted(deletedMsgs) { + if (deletedMsgs.length > 0) { + this.emit("messages-deleted", deletedMsgs); + } + } + + msgsMoveCopyCompleted(move, srcMsgs, dstFolder, dstMsgs) { + if (srcMsgs.length > 0 && dstMsgs.length > 0) { + let emitMsg = move ? "messages-moved" : "messages-copied"; + this.emit(emitMsg, srcMsgs, dstMsgs); + } + } + + msgKeyChanged(oldKey, newMsgHdr) { + // For IMAP messages there is a delayed update of database keys and if those + // keys change, the messageTracker needs to update its maps, otherwise wrong + // messages will be returned. Key changes are replayed in multi-step swaps. + let newKey = newMsgHdr.messageKey; + + // Replay pending swaps. + while (this._pendingKeyChanges.has(oldKey)) { + let next = this._pendingKeyChanges.get(oldKey); + this._pendingKeyChanges.delete(oldKey); + oldKey = next; + + // Check if we are left with a no-op swap and exit early. + if (oldKey == newKey) { + this._pendingKeyChanges.delete(oldKey); + return; + } + } + + if (oldKey != newKey) { + // New key swap, log the mirror swap as pending. + this._pendingKeyChanges.set(newKey, oldKey); + + // Swap tracker entries. + let oldId = this._get({ + folderURI: newMsgHdr.folder.URI, + messageKey: oldKey, + }); + let newId = this._get({ + folderURI: newMsgHdr.folder.URI, + messageKey: newKey, + }); + this._set(oldId, { folderURI: newMsgHdr.folder.URI, messageKey: newKey }); + this._set(newId, { folderURI: newMsgHdr.folder.URI, messageKey: oldKey }); + } + } + + // nsIObserver + + /** + * Observer to update message tracker if a message has received a new key due + * to attachments being removed, which we do not consider to be a new message. + */ + observe(subject, topic, data) { + if (topic == "attachment-delete-msgkey-changed") { + data = JSON.parse(data); + + if (data && data.folderURI && data.oldMessageKey && data.newMessageKey) { + let id = this._get({ + folderURI: data.folderURI, + messageKey: data.oldMessageKey, + }); + if (id) { + // Replace tracker entries. + this._set(id, { + folderURI: data.folderURI, + messageKey: data.newMessageKey, + }); + } + } + } else if (topic == "quit-application-granted") { + this.cleanup(); + } + } +} + +/** + * Convenience class to handle message pages. + */ +class MessagePage { + constructor() { + this.messages = []; + this.read = false; + this._deferredPromise = new Promise(resolve => { + this._resolveDeferredPromise = resolve; + }); + } + + get promise() { + return this._deferredPromise; + } + + resolvePage() { + this._resolveDeferredPromise(this.messages); + } +} + +/** + * Convenience class to keep track of the status of message lists. + */ +export class MessageList { + constructor(extension, messageTracker) { + this.messageListId = Services.uuid.generateUUID().number.substring(1, 37); + this.extension = extension; + this.isDone = false; + this.pages = []; + this._messageTracker = messageTracker; + this.autoPaginatorTimeout = null; + + this.addPage(); + } + + addPage() { + if (this.autoPaginatorTimeout) { + clearTimeout(this.autoPaginatorTimeout); + this.autoPaginatorTimeout = null; + } + + if (this.isDone) { + return; + } + + // Adding a page will make this.currentPage point to the new page. + let previousPage = this.currentPage; + + // If the current page has no messages, there is no need to add a page. + if (previousPage && previousPage.messages.length == 0) { + return; + } + + this.pages.push(new MessagePage()); + // The previous page is finished and can be resolved. + if (previousPage) { + previousPage.resolvePage(); + } + } + + get currentPage() { + return this.pages.length > 0 ? this.pages[this.pages.length - 1] : null; + } + + get id() { + return this.messageListId; + } + + addMessage(message) { + if (this.isDone || !this.currentPage) { + return; + } + if (this.currentPage.messages.length >= lazy.gMessagesPerPage) { + this.addPage(); + } + + this.currentPage.messages.push( + this._messageTracker.convertMessage(message, this.extension) + ); + + // Automatically push a new page and return the page with this message after + // a fixed amount of time, so that small sets of search results are not held + // back until a full page has been found or the entire search has finished. + if (!this.autoPaginatorTimeout) { + this.autoPaginatorTimeout = setTimeout(this.addPage.bind(this), 1000); + } + } + + done() { + if (this.isDone) { + return; + } + this.isDone = true; + + // Resolve the current page. + if (this.currentPage) { + this.currentPage.resolvePage(); + } + } + + async getNextUnreadPage() { + let page = this.pages.find(p => !p.read); + if (!page) { + return null; + } + + let messages = await page.promise; + page.read = true; + + return { + id: this.pages.find(p => !p.read) ? this.id : null, + messages, + }; + } +} + +/** + * Tracks lists of messages so that an extension can consume them in chunks. + * Any WebExtensions method that could return multiple messages should instead call + * messageListTracker.startList and return the results, which contain the first + * chunk. Further chunks can be fetched by the extension calling + * browser.messages.continueList. Chunk size is controlled by a pref. + */ +export class MessageListTracker { + constructor(messageTracker) { + this._contextLists = new WeakMap(); + this._messageTracker = messageTracker; + } + + /** + * Takes an array or enumerator of messages and returns a Promise for the first + * page, which will resolve as soon as it is available. + * + * @returns {object} + */ + startList(messages, extension) { + let messageList = this.createList(extension); + this._addMessages(messages, messageList); + return this.getNextPage(messageList); + } + + /** + * Add messages to a messageList. + */ + async _addMessages(messages, messageList) { + if (messageList.isDone) { + return; + } + if (Array.isArray(messages)) { + messages = this._createEnumerator(messages); + } + while (messages.hasMoreElements()) { + let next = messages.getNext(); + messageList.addMessage(next.QueryInterface(Ci.nsIMsgDBHdr)); + } + messageList.done(); + } + + _createEnumerator(array) { + let current = 0; + return { + hasMoreElements() { + return current < array.length; + }, + getNext() { + return array[current++]; + }, + }; + } + + /** + * Creates and returns a new messageList object. + * + * @returns {object} + */ + createList(extension) { + let messageList = new MessageList(extension, this._messageTracker); + let lists = this._contextLists.get(extension); + if (!lists) { + lists = new Map(); + this._contextLists.set(extension, lists); + } + lists.set(messageList.id, messageList); + return messageList; + } + + /** + * Returns the messageList object for a given id. + * + * @returns {object} + */ + getList(messageListId, extension) { + let lists = this._contextLists.get(extension); + let messageList = lists ? lists.get(messageListId, null) : null; + if (!messageList) { + throw new ExtensionError( + `No message list for id ${messageListId}. Have you reached the end of a list?` + ); + } + return messageList; + } + + /** + * Returns the first/next message page of the given messageList. + * + * @returns {object} + */ + async getNextPage(messageList) { + let page = await messageList.getNextUnreadPage(); + if (!page) { + return null; + } + + // If the page does not have an id, the list has been retrieved completely + // and can be removed. + if (!page.id) { + let lists = this._contextLists.get(messageList.extension); + if (lists && lists.has(messageList.id)) { + lists.delete(messageList.id); + } + } + return page; + } +} + +export class MessageManager { + constructor(extension, messageTracker, messageListTracker) { + this.extension = extension; + this._messageTracker = messageTracker; + this._messageListTracker = messageListTracker; + } + + convert(msgHdr) { + return this._messageTracker.convertMessage(msgHdr, this.extension); + } + + get(id) { + return this._messageTracker.getMessage(id); + } + + startMessageList(messageList) { + return this._messageListTracker.startList(messageList, this.extension); + } +} diff --git a/mail/components/extensions/moz.build b/mail/components/extensions/moz.build index f53bb3d587..efae7672ef 100644 --- a/mail/components/extensions/moz.build +++ b/mail/components/extensions/moz.build @@ -7,7 +7,9 @@ EXTRA_COMPONENTS += [ ] EXTRA_JS_MODULES += [ + "ExtensionAccounts.sys.mjs", "ExtensionBrowsingData.sys.mjs", + "ExtensionMessages.sys.mjs", "ExtensionPopups.sys.mjs", "ExtensionToolbarButtons.sys.mjs", "MailExtensionShortcuts.sys.mjs", diff --git a/mail/components/extensions/parent/.eslintrc.js b/mail/components/extensions/parent/.eslintrc.js index b5c5239b46..20de4b676d 100644 --- a/mail/components/extensions/parent/.eslintrc.js +++ b/mail/components/extensions/parent/.eslintrc.js @@ -42,20 +42,12 @@ module.exports = { MESSAGE_WINDOW_URI: true, MESSAGE_PROTOCOLS: true, NOTIFICATION_COLLAPSE_TIME: true, - CachedMsgHeader: true, ExtensionError: true, Tab: true, TabmailTab: true, Window: true, TabmailWindow: true, clickModifiersFromEvent: true, - convertFolder: true, - convertAccount: true, - traverseSubfolders: true, - convertMailIdentity: true, - convertMessage: true, - folderPathToURI: true, - folderURIToPath: true, getNormalWindowReady: true, getRealFileForFile: true, getTabBrowser: true, diff --git a/mail/components/extensions/parent/ext-accounts.js b/mail/components/extensions/parent/ext-accounts.js index 2388f896c7..d8a817365c 100644 --- a/mail/components/extensions/parent/ext-accounts.js +++ b/mail/components/extensions/parent/ext-accounts.js @@ -8,6 +8,10 @@ ChromeUtils.defineModuleGetter( "resource:///modules/MailServices.jsm" ); +var { convertAccount, convertMailIdentity } = ChromeUtils.importESModule( + "resource:///modules/ExtensionAccounts.sys.mjs" +); + /** * @implements {nsIObserver} * @implements {nsIMsgFolderListener} diff --git a/mail/components/extensions/parent/ext-cloudFile.js b/mail/components/extensions/parent/ext-cloudFile.js index 74193d8d14..f31dad1866 100644 --- a/mail/components/extensions/parent/ext-cloudFile.js +++ b/mail/components/extensions/parent/ext-cloudFile.js @@ -7,12 +7,14 @@ var { ExtensionParent } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionParent.sys.mjs" ); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); var { cloudFileAccounts } = ChromeUtils.import( "resource:///modules/cloudFileAccounts.jsm" ); -// eslint-disable-next-line mozilla/reject-importGlobalProperties -Cu.importGlobalProperties(["File", "FileReader"]); +XPCOMUtils.defineLazyGlobalGetters(this, ["File", "FileReader"]); async function promiseFileRead(nsifile) { let blob = await File.createFromNsIFile(nsifile); diff --git a/mail/components/extensions/parent/ext-compose.js b/mail/components/extensions/parent/ext-compose.js index 33a52c5e08..b1d865006b 100644 --- a/mail/components/extensions/parent/ext-compose.js +++ b/mail/components/extensions/parent/ext-compose.js @@ -6,7 +6,7 @@ var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); -XPCOMUtils.defineLazyGlobalGetters(this, ["IOUtils", "PathUtils"]); +XPCOMUtils.defineLazyGlobalGetters(this, ["File", "IOUtils", "PathUtils"]); ChromeUtils.defineModuleGetter( this, @@ -22,8 +22,12 @@ let parserUtils = Cc["@mozilla.org/parserutils;1"].getService( Ci.nsIParserUtils ); -// eslint-disable-next-line mozilla/reject-importGlobalProperties -Cu.importGlobalProperties(["File"]); +var { convertMessage } = ChromeUtils.importESModule( + "resource:///modules/ExtensionMessages.sys.mjs" +); +var { convertFolder, folderPathToURI } = ChromeUtils.importESModule( + "resource:///modules/ExtensionAccounts.sys.mjs" +); const deliveryFormats = [ { id: Ci.nsIMsgCompSendFormat.Auto, value: "auto" }, @@ -850,7 +854,7 @@ class MsgOperationObserver { folderURI: msgHdr.folder.URI, }); if (!this.classifiedMessages.has(key)) { - this.classifiedMessages.set(key, convertMessage(msgHdr)); + this.classifiedMessages.set(key, messageTracker.convertMessage(msgHdr)); } } } diff --git a/mail/components/extensions/parent/ext-folders.js b/mail/components/extensions/parent/ext-folders.js index 63704b9dd7..b84540dda5 100644 --- a/mail/components/extensions/parent/ext-folders.js +++ b/mail/components/extensions/parent/ext-folders.js @@ -10,7 +10,8 @@ ChromeUtils.defineModuleGetter( ChromeUtils.defineESModuleGetters(this, { DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", }); - +var { convertFolder, folderPathToURI, folderURIToPath, traverseSubfolders } = + ChromeUtils.importESModule("resource:///modules/ExtensionAccounts.sys.mjs"); /** * Tracks folder events. * diff --git a/mail/components/extensions/parent/ext-identities.js b/mail/components/extensions/parent/ext-identities.js index 1b9e719ebe..8bd1fd13b5 100644 --- a/mail/components/extensions/parent/ext-identities.js +++ b/mail/components/extensions/parent/ext-identities.js @@ -11,6 +11,10 @@ ChromeUtils.defineESModuleGetters(this, { DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", }); +var { convertMailIdentity } = ChromeUtils.importESModule( + "resource:///modules/ExtensionAccounts.sys.mjs" +); + function findIdentityAndAccount(identityId) { for (let account of MailServices.accounts.accounts) { for (let identity of account.identities) { diff --git a/mail/components/extensions/parent/ext-mail.js b/mail/components/extensions/parent/ext-mail.js index 50f6389b08..aa02d2fe77 100644 --- a/mail/components/extensions/parent/ext-mail.js +++ b/mail/components/extensions/parent/ext-mail.js @@ -9,7 +9,7 @@ var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); -var { ExtensionError, getInnerWindowID } = ExtensionUtils; +var { ExtensionError } = ExtensionUtils; var { defineLazyGetter, makeWidgetId } = ExtensionCommon; var { ExtensionSupport } = ChromeUtils.importESModule( @@ -18,26 +18,16 @@ var { ExtensionSupport } = ChromeUtils.importESModule( ChromeUtils.defineESModuleGetters(this, { ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs", - setTimeout: "resource://gre/modules/Timer.sys.mjs", - clearTimeout: "resource://gre/modules/Timer.sys.mjs", + FolderManager: "resource:///modules/ExtensionAccounts.sys.mjs", }); +var { MessageListTracker, MessageTracker, MessageManager } = + ChromeUtils.importESModule("resource:///modules/ExtensionMessages.sys.mjs"); + XPCOMUtils.defineLazyModuleGetters(this, { MailServices: "resource:///modules/MailServices.jsm", }); -XPCOMUtils.defineLazyPreferenceGetter( - this, - "gJunkThreshold", - "mail.adaptivefilters.junk_threshold", - 90 -); -XPCOMUtils.defineLazyPreferenceGetter( - this, - "gMessagesPerPage", - "extensions.webextensions.messagesPerPage", - 100 -); XPCOMUtils.defineLazyGlobalGetters(this, [ "IOUtils", "PathUtils", @@ -89,6 +79,8 @@ const NOTIFICATION_COLLAPSE_TIME = 200; }; })(); +let messageTracker; +let messageListTracker; let tabTracker; let spaceTracker; let windowTracker; @@ -307,99 +299,6 @@ XPCOMUtils.defineLazyGetter(global, "searchInitialized", () => { ); }); -/** - * Class for cached message headers to reduce XPCOM requests and to cache msgHdr - * of file and attachment messages. - */ -class CachedMsgHeader { - constructor(msgHdr) { - this.mProperties = {}; - - // Properties needed by convertMessage(). - this.author = null; - this.subject = ""; - this.recipients = null; - this.ccList = null; - this.bccList = null; - this.messageId = null; - this.date = 0; - this.flags = 0; - this.isRead = false; - this.isFlagged = false; - this.messageSize = 0; - this.folder = null; - - // Additional properties. - this.accountKey = ""; - - if (msgHdr) { - // Cache all elements which are needed by convertMessage(). - this.author = msgHdr.mime2DecodedAuthor; - this.subject = msgHdr.mime2DecodedSubject; - this.recipients = msgHdr.mime2DecodedRecipients; - this.ccList = msgHdr.ccList; - this.bccList = msgHdr.bccList; - this.messageId = msgHdr.messageId; - this.date = msgHdr.date; - this.flags = msgHdr.flags; - this.isRead = msgHdr.isRead; - this.isFlagged = msgHdr.isFlagged; - this.messageSize = msgHdr.messageSize; - this.folder = msgHdr.folder; - - this.mProperties.junkscore = msgHdr.getStringProperty("junkscore"); - this.mProperties.keywords = msgHdr.getStringProperty("keywords"); - - if (this.folder) { - this.messageKey = msgHdr.messageKey; - } else { - this.mProperties.dummyMsgUrl = msgHdr.getStringProperty("dummyMsgUrl"); - this.mProperties.dummyMsgLastModifiedTime = msgHdr.getUint32Property( - "dummyMsgLastModifiedTime" - ); - } - - // Also cache the additional elements. - this.accountKey = msgHdr.accountKey; - } - } - - getProperty(aProperty) { - return this.getStringProperty(aProperty); - } - setProperty(aProperty, aVal) { - return this.setStringProperty(aProperty, aVal); - } - getStringProperty(aProperty) { - if (this.mProperties.hasOwnProperty(aProperty)) { - return this.mProperties[aProperty]; - } - return ""; - } - setStringProperty(aProperty, aVal) { - this.mProperties[aProperty] = aVal; - } - getUint32Property(aProperty) { - if (this.mProperties.hasOwnProperty(aProperty)) { - return parseInt(this.mProperties[aProperty]); - } - return 0; - } - setUint32Property(aProperty, aVal) { - this.mProperties[aProperty] = aVal.toString(); - } - markHasAttachments(hasAttachments) {} - get mime2DecodedAuthor() { - return this.author; - } - get mime2DecodedSubject() { - return this.subject; - } - get mime2DecodedRecipients() { - return this.recipients; - } -} - /** * Returns the WebExtension window type for the given window, or null, if it is * not supported. @@ -1126,11 +1025,6 @@ class TabTracker extends TabTrackerBase { } } -tabTracker = new TabTracker(); -spaceTracker = new SpaceTracker(); -windowTracker = new WindowTracker(); -Object.assign(global, { tabTracker, spaceTracker, windowTracker }); - /** * Extension-specific wrapper around a Thunderbird tab. Note that for actual * tabs in the main window, some of these methods are overridden by the @@ -1809,8 +1703,6 @@ class TabmailWindow extends Window { } } -Object.assign(global, { Tab, Window }); - /** * Manages native tabs, their wrappers, and their dynamic permissions for a particular extension. */ @@ -1994,959 +1886,21 @@ async function getNormalWindowReady(context, windowId) { return window; } -/** - * Converts an nsIMsgAccount to a simple object - * - * @param {nsIMsgAccount} account - * @returns {object} - */ -function convertAccount(account, includeFolders = true) { - if (!account) { - return null; - } - - account = account.QueryInterface(Ci.nsIMsgAccount); - let server = account.incomingServer; - if (server.type == "im") { - return null; - } - - let folders = null; - if (includeFolders) { - folders = traverseSubfolders( - account.incomingServer.rootFolder, - account.key - ).subFolders; - } - - return { - id: account.key, - name: account.incomingServer.prettyName, - type: account.incomingServer.type, - folders, - identities: account.identities.map(identity => - convertMailIdentity(account, identity) - ), - }; -} - -/** - * Converts an nsIMsgIdentity to a simple object for use in messages. - * - * @param {nsIMsgAccount} account - * @param {nsIMsgIdentity} identity - * @returns {object} - */ -function convertMailIdentity(account, identity) { - if (!account || !identity) { - return null; - } - identity = identity.QueryInterface(Ci.nsIMsgIdentity); - return { - accountId: account.key, - id: identity.key, - label: identity.label || "", - name: identity.fullName || "", - email: identity.email || "", - replyTo: identity.replyTo || "", - organization: identity.organization || "", - composeHtml: identity.composeHtml, - signature: identity.htmlSigText || "", - signatureIsPlainText: !identity.htmlSigFormat, - }; -} - -/** - * The following functions turn nsIMsgFolder references into more human-friendly forms. - * A folder can be referenced with the account key, and the path to the folder in that account. - */ - -/** - * Convert a folder URI to a human-friendly path. - * - * @returns {string} - */ -function folderURIToPath(accountId, uri) { - let server = MailServices.accounts.getAccount(accountId).incomingServer; - let rootURI = server.rootFolder.URI; - if (rootURI == uri) { - return "/"; - } - // The .URI property of an IMAP folder doesn't have %-encoded characters, but - // may include literal % chars. Services.io.newURI(uri) applies encodeURI to - // the returned filePath, but will not encode any literal % chars, which will - // cause decodeURIComponent to fail (bug 1707408). - if (server.type == "imap") { - return uri.substring(rootURI.length); - } - let path = Services.io.newURI(uri).filePath; - return path.split("/").map(decodeURIComponent).join("/"); -} - -/** - * Convert a human-friendly path to a folder URI. This function does not assume - * that the folder referenced exists. - * - * @returns {string} - */ -function folderPathToURI(accountId, path) { - let server = MailServices.accounts.getAccount(accountId).incomingServer; - let rootURI = server.rootFolder.URI; - if (path == "/") { - return rootURI; - } - // The .URI property of an IMAP folder doesn't have %-encoded characters. - // If encoded here, the folder lookup service won't find the folder. - if (server.type == "imap") { - return rootURI + path; - } - return ( - rootURI + - path - .split("/") - .map(p => - encodeURIComponent(p) - .replace(/[!'()*]/g, c => "%" + c.charCodeAt(0).toString(16)) - // We do not encode "+" chars in folder URIs. Manually convert them - // back to literal + chars, otherwise folder lookup will fail. - .replaceAll("%2B", "+") - ) - .join("/") - ); -} - -const folderTypeMap = new Map([ - [Ci.nsMsgFolderFlags.Inbox, "inbox"], - [Ci.nsMsgFolderFlags.Drafts, "drafts"], - [Ci.nsMsgFolderFlags.SentMail, "sent"], - [Ci.nsMsgFolderFlags.Trash, "trash"], - [Ci.nsMsgFolderFlags.Templates, "templates"], - [Ci.nsMsgFolderFlags.Archive, "archives"], - [Ci.nsMsgFolderFlags.Junk, "junk"], - [Ci.nsMsgFolderFlags.Queue, "outbox"], -]); - -/** - * Converts an nsIMsgFolder to a simple object for use in API messages. - * - * @param {nsIMsgFolder} folder - The folder to convert. - * @param {string} [accountId] - An optimization to avoid looking up the - * account. The value from nsIMsgDBHdr.accountKey must not be used here. - * @returns {MailFolder} - * @see mail/components/extensions/schemas/folders.json - */ -function convertFolder(folder, accountId) { - if (!folder) { - return null; - } - if (!accountId) { - let server = folder.server; - let account = MailServices.accounts.FindAccountForServer(server); - accountId = account.key; - } - - let folderObject = { - accountId, - name: folder.prettyName, - path: folderURIToPath(accountId, folder.URI), - }; - - let flags = folder.flags; - for (let [flag, typeName] of folderTypeMap.entries()) { - if (flags & flag) { - folderObject.type = typeName; - // Exit the loop as soon as an entry was found. - break; - } - } - - return folderObject; -} - -/** - * Converts an nsIMsgFolder and all its subfolders to a simple object for use in - * API messages. - * - * @param {nsIMsgFolder} folder - The folder to convert. - * @param {string} [accountId] - An optimization to avoid looking up the - * account. The value from nsIMsgDBHdr.accountKey must not be used here. - * @returns {MailFolder} - * @see mail/components/extensions/schemas/folders.json - */ -function traverseSubfolders(folder, accountId) { - let f = convertFolder(folder, accountId); - f.subFolders = []; - if (folder.hasSubFolders) { - // Use the same order as used by Thunderbird. - let subFolders = [...folder.subFolders].sort((a, b) => - a.sortOrder == b.sortOrder - ? a.name.localeCompare(b.name) - : a.sortOrder - b.sortOrder - ); - for (let subFolder of subFolders) { - f.subFolders.push( - traverseSubfolders(subFolder, accountId || f.accountId) - ); - } - } - return f; -} - -class FolderManager { - constructor(extension) { - this.extension = extension; - } - - convert(folder, accountId) { - return convertFolder(folder, accountId); - } - - get(accountId, path) { - return MailServices.folderLookup.getFolderForURL( - folderPathToURI(accountId, path) - ); - } -} - -/** - * Checks if the provided dummyMsgUrl belongs to an attached message. - */ -function isAttachedMessageUrl(dummyMsgUrl) { - try { - return dummyMsgUrl && new URL(dummyMsgUrl).searchParams.has("part"); - } catch (ex) { - return false; - } -} - -/** - * Converts an nsIMsgDBHdr to a simple object for use in messages. - * This function WILL change as the API develops. - * - * @param {nsIMsgDBHdr} msgHdr - * @param {ExtensionData} extension - * - * @returns {MessageHeader} MessageHeader object - * - * @see /mail/components/extensions/schemas/messages.json - */ -function convertMessage(msgHdr, extension) { - if (!msgHdr) { - return null; - } - - const composeFields = Cc[ - "@mozilla.org/messengercompose/composefields;1" - ].createInstance(Ci.nsIMsgCompFields); - - // Cache msgHdr to reduce XPCOM requests. - let cachedHdr = new CachedMsgHeader(msgHdr); - - let junkScore = parseInt(cachedHdr.getStringProperty("junkscore"), 10) || 0; - let tags = (cachedHdr.getStringProperty("keywords") || "") - .split(" ") - .filter(MailServices.tags.isValidKey); - - // Getting the size of attached messages does not work consistently. For imap:// - // and mailbox:// messages the returned size in msgHdr.messageSize is 0, and for - // file:// messages the returned size is always the total file size - // Be consistent here and always return 0. The user can obtain the message size - // from the size of the associated attachment file. - let size = isAttachedMessageUrl(cachedHdr.getStringProperty("dummyMsgUrl")) - ? 0 - : cachedHdr.messageSize; - - let messageObject = { - id: messageTracker.getId(cachedHdr), - date: new Date(Math.round(cachedHdr.date / 1000)), - author: cachedHdr.mime2DecodedAuthor, - recipients: cachedHdr.mime2DecodedRecipients - ? composeFields.splitRecipients(cachedHdr.mime2DecodedRecipients, false) - : [], - ccList: cachedHdr.ccList - ? composeFields.splitRecipients(cachedHdr.ccList, false) - : [], - bccList: cachedHdr.bccList - ? composeFields.splitRecipients(cachedHdr.bccList, false) - : [], - subject: cachedHdr.mime2DecodedSubject, - read: cachedHdr.isRead, - new: !!(cachedHdr.flags & Ci.nsMsgMessageFlags.New), - headersOnly: !!(cachedHdr.flags & Ci.nsMsgMessageFlags.Partial), - flagged: !!cachedHdr.isFlagged, - junk: junkScore >= gJunkThreshold, - junkScore, - headerMessageId: cachedHdr.messageId, - size, - tags, - external: !cachedHdr.folder, - }; - // convertMessage can be called without providing an extension, if the info is - // needed for multiple extensions. The caller has to ensure that the folder info - // is not forwarded to extensions, which do not have the required permission. - if ( - cachedHdr.folder && - (!extension || extension.hasPermission("accountsRead")) - ) { - messageObject.folder = convertFolder(cachedHdr.folder); - } - return messageObject; -} - -/** - * A map of numeric identifiers to messages for easy reference. - * - * @implements {nsIFolderListener} - * @implements {nsIMsgFolderListener} - * @implements {nsIObserver} - */ -var messageTracker = new (class extends EventEmitter { - constructor() { - super(); - this._nextId = 1; - this._messages = new Map(); - this._messageIds = new Map(); - this._listenerCount = 0; - this._pendingKeyChanges = new Map(); - this._dummyMessageHeaders = new Map(); - - // nsIObserver - Services.obs.addObserver(this, "quit-application-granted"); - Services.obs.addObserver(this, "attachment-delete-msgkey-changed"); - // nsIFolderListener - MailServices.mailSession.AddFolderListener( - this, - Ci.nsIFolderListener.propertyFlagChanged | - Ci.nsIFolderListener.intPropertyChanged - ); - // nsIMsgFolderListener - MailServices.mfn.addListener( - this, - MailServices.mfn.msgsJunkStatusChanged | - MailServices.mfn.msgsDeleted | - MailServices.mfn.msgsMoveCopyCompleted | - MailServices.mfn.msgKeyChanged - ); - - this._messageOpenListener = { - registered: false, - async handleEvent(event) { - let msgHdr = event.detail; - // It is not possible to retrieve the dummyMsgHdr of messages opened - // from file at a later time, track them manually. - if ( - msgHdr && - !msgHdr.folder && - msgHdr.getStringProperty("dummyMsgUrl").startsWith("file://") - ) { - messageTracker.getId(msgHdr); - } - }, - }; - try { - windowTracker.addListener("MsgLoaded", this._messageOpenListener); - this._messageOpenListener.registered = true; - } catch (ex) { - // Fails during XPCSHELL tests, which mock the WindowWatcher but do not - // implement registerNotification. - } - } - - cleanup() { - // nsIObserver - Services.obs.removeObserver(this, "quit-application-granted"); - Services.obs.removeObserver(this, "attachment-delete-msgkey-changed"); - // nsIFolderListener - MailServices.mailSession.RemoveFolderListener(this); - // nsIMsgFolderListener - MailServices.mfn.removeListener(this); - if (this._messageOpenListener.registered) { - windowTracker.removeListener("MsgLoaded", this._messageOpenListener); - this._messageOpenListener.registered = false; - } - } - - /** - * Generates a hash for the given msgIdentifier. - * - * @param {*} msgIdentifier - * @returns {string} - */ - getHash(msgIdentifier) { - if (msgIdentifier.folderURI) { - return `folderURI:${msgIdentifier.folderURI}, messageKey: ${msgIdentifier.messageKey}`; - } - return `dummyMsgUrl:${msgIdentifier.dummyMsgUrl}, dummyMsgLastModifiedTime: ${msgIdentifier.dummyMsgLastModifiedTime}`; - } - - /** - * Maps the provided message identifier to the given messageTracker id. - * - * @param {integer} id - messageTracker id of the message - * @param {*} msgIdentifier - msgIdentifier of the message - * @param {nsIMsgDBHdr} [msgHdr] - optional msgHdr of the message, will be - * added to the cache if it is a dummy msgHdr (a file or attachment message) - */ - _set(id, msgIdentifier, msgHdr) { - let hash = this.getHash(msgIdentifier); - this._messageIds.set(hash, id); - this._messages.set(id, msgIdentifier); - // Keep track of dummy message headers, which do not have a folder property - // and cannot be retrieved later. - if (msgHdr && !msgHdr.folder && msgIdentifier.dummyMsgUrl) { - this._dummyMessageHeaders.set( - msgIdentifier.dummyMsgUrl, - msgHdr instanceof Ci.nsIMsgDBHdr ? new CachedMsgHeader(msgHdr) : msgHdr - ); - } - } - - /** - * Lookup the messageTracker id for the given message identifier, return null - * if not known. - * - * @param {*} msgIdentifier - msgIdentifier of the message - * @returns {integer} The messageTracker id of the message. - */ - _get(msgIdentifier) { - let hash = this.getHash(msgIdentifier); - if (this._messageIds.has(hash)) { - return this._messageIds.get(hash); - } - return null; - } - - /** - * Removes the provided message identifier from the messageTracker. - * - * @param {*} msgIdentifier - msgIdentifier of the message - */ - _remove(msgIdentifier) { - let hash = this.getHash(msgIdentifier); - let id = this._get(msgIdentifier); - this._messages.delete(id); - this._messageIds.delete(hash); - this._dummyMessageHeaders.delete(msgIdentifier.dummyMsgUrl); - } - - /** - * Finds a message in the messageTracker or adds it. - * - * @param {nsIMsgDBHdr} - msgHdr of the requested message - * @returns {integer} The messageTracker id of the message. - */ - getId(msgHdr) { - let msgIdentifier; - if (msgHdr.folder) { - msgIdentifier = { - folderURI: msgHdr.folder.URI, - messageKey: msgHdr.messageKey, - }; - } else { - // Normalize the dummyMsgUrl by sorting its parameters and striping them - // to a minimum. - let url = new URL(msgHdr.getStringProperty("dummyMsgUrl")); - let parameters = Array.from(url.searchParams, p => p[0]).filter( - p => !["group", "number", "key", "part"].includes(p) - ); - for (let parameter of parameters) { - url.searchParams.delete(parameter); - } - url.searchParams.sort(); - - msgIdentifier = { - dummyMsgUrl: url.href, - dummyMsgLastModifiedTime: msgHdr.getUint32Property( - "dummyMsgLastModifiedTime" - ), - }; - } - - let id = this._get(msgIdentifier); - if (id) { - return id; - } - id = this._nextId++; - - this._set(id, msgIdentifier, msgHdr); - return id; - } - - /** - * Check if the provided msgIdentifier belongs to a modified file message. - * - * @param {*} msgIdentifier - msgIdentifier object of the message - * @returns {boolean} - */ - isModifiedFileMsg(msgIdentifier) { - if (!msgIdentifier.dummyMsgUrl?.startsWith("file://")) { - return false; - } - - try { - let file = Services.io - .newURI(msgIdentifier.dummyMsgUrl) - .QueryInterface(Ci.nsIFileURL).file; - if (!file?.exists()) { - throw new ExtensionError("File does not exist"); - } - if ( - msgIdentifier.dummyMsgLastModifiedTime && - Math.floor(file.lastModifiedTime / 1000000) != - msgIdentifier.dummyMsgLastModifiedTime - ) { - throw new ExtensionError("File has been modified"); - } - } catch (ex) { - console.error(ex); - return true; - } - return false; - } - - /** - * Retrieves a message from the messageTracker. If the message no longer, - * exists it is removed from the messageTracker. - * - * @param {integer} id - messageTracker id of the message - * @returns {nsIMsgDBHdr} The identifier of the message. - */ - getMessage(id) { - let msgIdentifier = this._messages.get(id); - if (!msgIdentifier) { - return null; - } - - if (msgIdentifier.folderURI) { - let folder = MailServices.folderLookup.getFolderForURL( - msgIdentifier.folderURI - ); - if (folder) { - let msgHdr = folder.msgDatabase.getMsgHdrForKey( - msgIdentifier.messageKey - ); - if (msgHdr) { - return msgHdr; - } - } - } else { - let msgHdr = this._dummyMessageHeaders.get(msgIdentifier.dummyMsgUrl); - if (msgHdr && !this.isModifiedFileMsg(msgIdentifier)) { - return msgHdr; - } - } - - this._remove(msgIdentifier); - return null; - } - - // nsIFolderListener - - onFolderPropertyFlagChanged(item, property, oldFlag, newFlag) { - let changes = {}; - switch (property) { - case "Status": - if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.Read) { - changes.read = item.isRead; - } - if ((oldFlag ^ newFlag) & Ci.nsMsgMessageFlags.New) { - changes.new = !!(newFlag & Ci.nsMsgMessageFlags.New); - } - break; - case "Flagged": - changes.flagged = item.isFlagged; - break; - case "Keywords": - { - let tags = item.getStringProperty("keywords"); - tags = tags ? tags.split(" ") : []; - changes.tags = tags.filter(MailServices.tags.isValidKey); - } - break; - } - if (Object.keys(changes).length) { - this.emit("message-updated", item, changes); - } - } - - onFolderIntPropertyChanged(folder, property, oldValue, newValue) { - switch (property) { - case "BiffState": - if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) { - // The folder argument is a root folder. - this.findNewMessages(folder); - } - break; - case "NewMailReceived": - // The folder argument is a real folder. - this.findNewMessages(folder); - break; - } - } - - /** - * Finds all folders with new messages in the specified changedFolder and - * returns those. - * - * @see MailNotificationManager._getFirstRealFolderWithNewMail() - */ - findNewMessages(changedFolder) { - let folders = changedFolder.descendants; - folders.unshift(changedFolder); - for (let folder of folders) { - let flags = folder.flags; - if ( - !(flags & Ci.nsMsgFolderFlags.Inbox) && - flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual) - ) { - // Do not notify if the folder is not Inbox but one of - // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual. - continue; - } - let numNewMessages = folder.getNumNewMessages(false); - if (!numNewMessages) { - continue; - } - let msgDb = folder.msgDatabase; - let newMsgKeys = msgDb.getNewList().slice(-numNewMessages); - if (newMsgKeys.length == 0) { - continue; - } - this.emit( - "messages-received", - folder, - newMsgKeys.map(key => msgDb.getMsgHdrForKey(key)) - ); - } - } - - // nsIMsgFolderListener - - msgsJunkStatusChanged(messages) { - for (let msgHdr of messages) { - let junkScore = parseInt(msgHdr.getStringProperty("junkscore"), 10) || 0; - this.emit("message-updated", msgHdr, { - junk: junkScore >= gJunkThreshold, - }); - } - } - - msgsDeleted(deletedMsgs) { - if (deletedMsgs.length > 0) { - this.emit("messages-deleted", deletedMsgs); - } - } - - msgsMoveCopyCompleted(move, srcMsgs, dstFolder, dstMsgs) { - if (srcMsgs.length > 0 && dstMsgs.length > 0) { - let emitMsg = move ? "messages-moved" : "messages-copied"; - this.emit(emitMsg, srcMsgs, dstMsgs); - } - } - - msgKeyChanged(oldKey, newMsgHdr) { - // For IMAP messages there is a delayed update of database keys and if those - // keys change, the messageTracker needs to update its maps, otherwise wrong - // messages will be returned. Key changes are replayed in multi-step swaps. - let newKey = newMsgHdr.messageKey; - - // Replay pending swaps. - while (this._pendingKeyChanges.has(oldKey)) { - let next = this._pendingKeyChanges.get(oldKey); - this._pendingKeyChanges.delete(oldKey); - oldKey = next; - - // Check if we are left with a no-op swap and exit early. - if (oldKey == newKey) { - this._pendingKeyChanges.delete(oldKey); - return; - } - } - - if (oldKey != newKey) { - // New key swap, log the mirror swap as pending. - this._pendingKeyChanges.set(newKey, oldKey); - - // Swap tracker entries. - let oldId = this._get({ - folderURI: newMsgHdr.folder.URI, - messageKey: oldKey, - }); - let newId = this._get({ - folderURI: newMsgHdr.folder.URI, - messageKey: newKey, - }); - this._set(oldId, { folderURI: newMsgHdr.folder.URI, messageKey: newKey }); - this._set(newId, { folderURI: newMsgHdr.folder.URI, messageKey: oldKey }); - } - } - - // nsIObserver - - /** - * Observer to update message tracker if a message has received a new key due - * to attachments being removed, which we do not consider to be a new message. - */ - observe(subject, topic, data) { - if (topic == "attachment-delete-msgkey-changed") { - data = JSON.parse(data); - - if (data && data.folderURI && data.oldMessageKey && data.newMessageKey) { - let id = this._get({ - folderURI: data.folderURI, - messageKey: data.oldMessageKey, - }); - if (id) { - // Replace tracker entries. - this._set(id, { - folderURI: data.folderURI, - messageKey: data.newMessageKey, - }); - } - } - } else if (topic == "quit-application-granted") { - this.cleanup(); - } - } -})(); - -/** - * Convenience class to handle message pages. - */ -class MessagePage { - constructor() { - this.messages = []; - this.read = false; - this._deferredPromise = new Promise(resolve => { - this._resolveDeferredPromise = resolve; - }); - } - - get promise() { - return this._deferredPromise; - } - - resolvePage() { - this._resolveDeferredPromise(this.messages); - } -} - -/** - * Convenience class to keep track of the status of message lists. - */ -class MessageList { - constructor(extension) { - this.messageListId = Services.uuid.generateUUID().number.substring(1, 37); - this.extension = extension; - this.isDone = false; - this.pages = []; - this.autoPaginatorTimeout = null; - - this.addPage(); - } - - addPage() { - if (this.autoPaginatorTimeout) { - clearTimeout(this.autoPaginatorTimeout); - this.autoPaginatorTimeout = null; - } - - if (this.isDone) { - return; - } - - // Adding a page will make this.currentPage point to the new page. - let previousPage = this.currentPage; - - // If the current page has no messages, there is no need to add a page. - if (previousPage && previousPage.messages.length == 0) { - return; - } - - this.pages.push(new MessagePage()); - // The previous page is finished and can be resolved. - if (previousPage) { - previousPage.resolvePage(); - } - } - - get currentPage() { - return this.pages.length > 0 ? this.pages[this.pages.length - 1] : null; - } - - get id() { - return this.messageListId; - } - - addMessage(message) { - if (this.isDone || !this.currentPage) { - return; - } - if (this.currentPage.messages.length >= gMessagesPerPage) { - this.addPage(); - } - - this.currentPage.messages.push(convertMessage(message, this.extension)); - - // Automatically push a new page and return the page with this message after - // a fixed amount of time, so that small sets of search results are not held - // back until a full page has been found or the entire search has finished. - if (!this.autoPaginatorTimeout) { - this.autoPaginatorTimeout = setTimeout(this.addPage.bind(this), 1000); - } - } - - done() { - if (this.isDone) { - return; - } - this.isDone = true; - - // Resolve the current page. - if (this.currentPage) { - this.currentPage.resolvePage(); - } - } - - async getNextUnreadPage() { - let page = this.pages.find(p => !p.read); - if (!page) { - return null; - } - - let messages = await page.promise; - page.read = true; - - return { - id: this.pages.find(p => !p.read) ? this.id : null, - messages, - }; - } -} - -/** - * Tracks lists of messages so that an extension can consume them in chunks. - * Any WebExtensions method that could return multiple messages should instead call - * messageListTracker.startList and return the results, which contain the first - * chunk. Further chunks can be fetched by the extension calling - * browser.messages.continueList. Chunk size is controlled by a pref. - */ -var messageListTracker = { - _contextLists: new WeakMap(), - - /** - * Takes an array or enumerator of messages and returns a Promise for the first - * page, which will resolve as soon as it is available. - * - * @returns {object} - */ - startList(messages, extension) { - let messageList = this.createList(extension); - this._addMessages(messages, messageList); - return this.getNextPage(messageList); - }, - - /** - * Add messages to a messageList. - */ - async _addMessages(messages, messageList) { - if (messageList.isDone) { - return; - } - if (Array.isArray(messages)) { - messages = this._createEnumerator(messages); - } - while (messages.hasMoreElements()) { - let next = messages.getNext(); - messageList.addMessage(next.QueryInterface(Ci.nsIMsgDBHdr)); - } - messageList.done(); - }, - - _createEnumerator(array) { - let current = 0; - return { - hasMoreElements() { - return current < array.length; - }, - getNext() { - return array[current++]; - }, - }; - }, - - /** - * Creates and returns a new messageList object. - * - * @returns {object} - */ - createList(extension) { - let messageList = new MessageList(extension); - let lists = this._contextLists.get(extension); - if (!lists) { - lists = new Map(); - this._contextLists.set(extension, lists); - } - lists.set(messageList.id, messageList); - return messageList; - }, - - /** - * Returns the messageList object for a given id. - * - * @returns {object} - */ - getList(messageListId, extension) { - let lists = this._contextLists.get(extension); - let messageList = lists ? lists.get(messageListId, null) : null; - if (!messageList) { - throw new ExtensionError( - `No message list for id ${messageListId}. Have you reached the end of a list?` - ); - } - return messageList; - }, - - /** - * Returns the first/next message page of the given messageList. - * - * @returns {object} - */ - async getNextPage(messageList) { - let page = await messageList.getNextUnreadPage(); - if (!page) { - return null; - } - - // If the page does not have an id, the list has been retrieved completely - // and can be removed. - if (!page.id) { - let lists = this._contextLists.get(messageList.extension); - if (lists && lists.has(messageList.id)) { - lists.delete(messageList.id); - } - } - return page; - }, -}; - -class MessageManager { - constructor(extension) { - this.extension = extension; - } - - convert(msgHdr) { - return convertMessage(msgHdr, this.extension); - } - - get(id) { - return messageTracker.getMessage(id); - } - - startMessageList(messageList) { - return messageListTracker.startList(messageList, this.extension); - } -} +tabTracker = new TabTracker(); +spaceTracker = new SpaceTracker(); +windowTracker = new WindowTracker(); +Object.assign(global, { + tabTracker, + spaceTracker, + windowTracker, +}); + +messageTracker = new MessageTracker(windowTracker); +messageListTracker = new MessageListTracker(messageTracker); +Object.assign(global, { + messageTracker, + messageListTracker, +}); extensions.on("startup", (type, extension) => { // eslint-disable-line mozilla/balanced-listeners @@ -2980,7 +1934,7 @@ extensions.on("startup", (type, extension) => { defineLazyGetter( extension, "messageManager", - () => new MessageManager(extension) + () => new MessageManager(extension, messageTracker, messageListTracker) ); } defineLazyGetter(extension, "tabManager", () => new TabManager(extension)); diff --git a/mail/components/extensions/parent/ext-mailTabs.js b/mail/components/extensions/parent/ext-mailTabs.js index e67de3b031..74c7f716ef 100644 --- a/mail/components/extensions/parent/ext-mailTabs.js +++ b/mail/components/extensions/parent/ext-mailTabs.js @@ -11,6 +11,10 @@ XPCOMUtils.defineLazyModuleGetters(this, { MailServices: "resource:///modules/MailServices.jsm", }); +var { convertFolder, folderPathToURI } = ChromeUtils.importESModule( + "resource:///modules/ExtensionAccounts.sys.mjs" +); + XPCOMUtils.defineLazyPreferenceGetter( this, "gDynamicPaneConfig", diff --git a/mail/components/extensions/parent/ext-menus.js b/mail/components/extensions/parent/ext-menus.js index 0db7ddf809..770932e1fc 100644 --- a/mail/components/extensions/parent/ext-menus.js +++ b/mail/components/extensions/parent/ext-menus.js @@ -12,24 +12,26 @@ ChromeUtils.defineModuleGetter( "resource:///modules/MailServices.jsm" ); +var { convertFolder, convertAccount, folderPathToURI, traverseSubfolders } = + ChromeUtils.importESModule("resource:///modules/ExtensionAccounts.sys.mjs"); + var { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); + +var { ExtensionCommon } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionCommon.sys.mjs" +); +var { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); var { SelectionUtils } = ChromeUtils.importESModule( "resource://gre/modules/SelectionUtils.sys.mjs" ); -var { DefaultMap, ExtensionError } = ExtensionUtils; - -var { ExtensionParent } = ChromeUtils.importESModule( - "resource://gre/modules/ExtensionParent.sys.mjs" -); -var { IconDetails, StartupCache } = ExtensionParent; - -var { ExtensionCommon } = ChromeUtils.importESModule( - "resource://gre/modules/ExtensionCommon.sys.mjs" -); var { makeWidgetId } = ExtensionCommon; +var { DefaultMap, ExtensionError } = ExtensionUtils; +var { IconDetails, StartupCache } = ExtensionParent; const ACTION_MENU_TOP_LEVEL_LIMIT = 6; diff --git a/mail/components/extensions/parent/ext-messageDisplay.js b/mail/components/extensions/parent/ext-messageDisplay.js index 98ba2dc75c..0272a3b8a3 100644 --- a/mail/components/extensions/parent/ext-messageDisplay.js +++ b/mail/components/extensions/parent/ext-messageDisplay.js @@ -37,7 +37,7 @@ function getDisplayedMessages(tab) { function convertMessages(messages, extension) { let result = []; for (let msg of messages) { - let hdr = convertMessage(msg, extension); + let hdr = messageTracker.convertMessage(msg, extension); if (hdr) { result.push(hdr); } @@ -99,7 +99,7 @@ this.messageDisplay = class extends ExtensionAPIPersistent { // `event.target` is an about:message window. let nativeTab = event.target.tabOrWindow; let tab = tabManager.wrapTab(nativeTab); - let msg = convertMessage(event.detail, extension); + let msg = messageTracker.convertMessage(event.detail, extension); fire.async(tab.convert(), msg); }, }; @@ -252,7 +252,7 @@ this.messageDisplay = class extends ExtensionAPIPersistent { if (messages.length != 1) { return null; } - return convertMessage(messages[0], extension); + return messageTracker.convertMessage(messages[0], extension); }, async getDisplayedMessages(tabId) { let tab = await getMessageDisplayTab(tabId); diff --git a/mail/components/extensions/parent/ext-messages.js b/mail/components/extensions/parent/ext-messages.js index 45d654d049..560b562ef3 100644 --- a/mail/components/extensions/parent/ext-messages.js +++ b/mail/components/extensions/parent/ext-messages.js @@ -6,6 +6,16 @@ ChromeUtils.defineESModuleGetters(this, { AttachmentInfo: "resource:///modules/AttachmentInfo.sys.mjs", }); +var { CachedMsgHeader } = ChromeUtils.importESModule( + "resource:///modules/ExtensionMessages.sys.mjs" +); +var { convertFolder, folderPathToURI } = ChromeUtils.importESModule( + "resource:///modules/ExtensionAccounts.sys.mjs" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + ChromeUtils.defineModuleGetter( this, "MailServices", @@ -40,9 +50,7 @@ ChromeUtils.defineModuleGetter( var { MailStringUtils } = ChromeUtils.import( "resource:///modules/MailStringUtils.jsm" ); - -// eslint-disable-next-line mozilla/reject-importGlobalProperties -Cu.importGlobalProperties(["File", "IOUtils", "PathUtils"]); +XPCOMUtils.defineLazyGlobalGetters(this, ["File", "IOUtils", "PathUtils"]); var { DefaultMap } = ExtensionUtils; @@ -107,7 +115,7 @@ async function convertAttachment(attachment) { let hdrId = attachment.headers["message-id"]?.[0]; attachedMsgHdr.messageId = hdrId ? hdrId.replace(/^<|>$/g, "") : ""; - rv.message = convertMessage(attachedMsgHdr); + rv.message = messageTracker.convertMessage(attachedMsgHdr); } return rv; @@ -443,7 +451,10 @@ this.messages = class extends ExtensionAPIPersistent { let listener = async (event, message, properties) => { let { extension } = this; // The msgHdr could be gone after the wakeup, convert it early. - let convertedMessage = convertMessage(message, extension); + let convertedMessage = messageTracker.convertMessage( + message, + extension + ); if (fire.wakeup) { await fire.wakeup(); } @@ -756,7 +767,10 @@ this.messages = class extends ExtensionAPIPersistent { if (!msgHdr) { throw new ExtensionError(`Message not found: ${messageId}.`); } - let messageHeader = convertMessage(msgHdr, context.extension); + let messageHeader = messageTracker.convertMessage( + msgHdr, + context.extension + ); if (messageHeader.id != messageId) { throw new ExtensionError( "Unexpected Error: Returned message does not equal requested message." @@ -1503,7 +1517,7 @@ this.messages = class extends ExtensionAPIPersistent { if (!file.mozFullPath) { await IOUtils.remove(tempFile.path); } - return convertMessage(msgHeader, context.extension); + return messageTracker.convertMessage(msgHeader, context.extension); } catch (ex) { console.error(ex); throw new ExtensionError(`Error importing message: ${ex.message}`);