summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/parent/ext-folders.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/parent/ext-folders.js')
-rw-r--r--comm/mail/components/extensions/parent/ext-folders.js675
1 files changed, 675 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/parent/ext-folders.js b/comm/mail/components/extensions/parent/ext-folders.js
new file mode 100644
index 0000000000..63704b9dd7
--- /dev/null
+++ b/comm/mail/components/extensions/parent/ext-folders.js
@@ -0,0 +1,675 @@
+/* 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/. */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "MailServices",
+ "resource:///modules/MailServices.jsm"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+});
+
+/**
+ * Tracks folder events.
+ *
+ * @implements {nsIMsgFolderListener}
+ */
+var folderTracker = new (class extends EventEmitter {
+ constructor() {
+ super();
+ this.listenerCount = 0;
+ this.pendingInfoNotifications = new ExtensionUtils.DefaultMap(
+ () => new Map()
+ );
+ this.deferredInfoNotifications = new ExtensionUtils.DefaultMap(
+ folder =>
+ new DeferredTask(
+ () => this.emitPendingInfoNotification(folder),
+ NOTIFICATION_COLLAPSE_TIME
+ )
+ );
+ }
+
+ on(...args) {
+ super.on(...args);
+ this.incrementListeners();
+ }
+
+ off(...args) {
+ super.off(...args);
+ this.decrementListeners();
+ }
+
+ incrementListeners() {
+ this.listenerCount++;
+ if (this.listenerCount == 1) {
+ // nsIMsgFolderListener
+ const flags =
+ MailServices.mfn.folderAdded |
+ MailServices.mfn.folderDeleted |
+ MailServices.mfn.folderMoveCopyCompleted |
+ MailServices.mfn.folderRenamed;
+ MailServices.mfn.addListener(this, flags);
+ // nsIFolderListener
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+ }
+ }
+ decrementListeners() {
+ this.listenerCount--;
+ if (this.listenerCount == 0) {
+ MailServices.mfn.removeListener(this);
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ }
+
+ // nsIFolderListener
+
+ onFolderIntPropertyChanged(item, property, oldValue, newValue) {
+ if (!(item instanceof Ci.nsIMsgFolder)) {
+ return;
+ }
+
+ switch (property) {
+ case "FolderFlag":
+ if (
+ (oldValue & Ci.nsMsgFolderFlags.Favorite) !=
+ (newValue & Ci.nsMsgFolderFlags.Favorite)
+ ) {
+ this.addPendingInfoNotification(
+ item,
+ "favorite",
+ !!(newValue & Ci.nsMsgFolderFlags.Favorite)
+ );
+ }
+ break;
+ case "TotalMessages":
+ this.addPendingInfoNotification(item, "totalMessageCount", newValue);
+ break;
+ case "TotalUnreadMessages":
+ this.addPendingInfoNotification(item, "unreadMessageCount", newValue);
+ break;
+ }
+ }
+
+ addPendingInfoNotification(folder, key, value) {
+ // If there is already a notification entry, decide if it must be emitted,
+ // or if it can be collapsed: Message count changes can be collapsed.
+ // This also collapses multiple different notifications types into a
+ // single event.
+ if (
+ ["favorite"].includes(key) &&
+ this.deferredInfoNotifications.has(folder) &&
+ this.pendingInfoNotifications.get(folder).has(key)
+ ) {
+ this.deferredInfoNotifications.get(folder).disarm();
+ this.emitPendingInfoNotification(folder);
+ }
+
+ this.pendingInfoNotifications.get(folder).set(key, value);
+ this.deferredInfoNotifications.get(folder).disarm();
+ this.deferredInfoNotifications.get(folder).arm();
+ }
+
+ emitPendingInfoNotification(folder) {
+ let folderInfo = this.pendingInfoNotifications.get(folder);
+ if (folderInfo.size > 0) {
+ this.emit(
+ "folder-info-changed",
+ convertFolder(folder),
+ Object.fromEntries(folderInfo)
+ );
+ this.pendingInfoNotifications.delete(folder);
+ }
+ }
+
+ // nsIMsgFolderListener
+
+ folderAdded(childFolder) {
+ this.emit("folder-created", convertFolder(childFolder));
+ }
+ folderDeleted(oldFolder) {
+ // Deleting an account, will trigger delete notifications for its folders,
+ // but the account lookup fails, so skip them.
+ let server = oldFolder.server;
+ let account = MailServices.accounts.FindAccountForServer(server);
+ if (account) {
+ this.emit("folder-deleted", convertFolder(oldFolder, account.key));
+ }
+ }
+ folderMoveCopyCompleted(move, srcFolder, targetFolder) {
+ // targetFolder is not the copied/moved folder, but its parent. Find the
+ // actual folder by its name (which is unique).
+ let dstFolder = null;
+ if (targetFolder && targetFolder.hasSubFolders) {
+ dstFolder = targetFolder.subFolders.find(
+ f => f.prettyName == srcFolder.prettyName
+ );
+ }
+
+ if (move) {
+ this.emit(
+ "folder-moved",
+ convertFolder(srcFolder),
+ convertFolder(dstFolder)
+ );
+ } else {
+ this.emit(
+ "folder-copied",
+ convertFolder(srcFolder),
+ convertFolder(dstFolder)
+ );
+ }
+ }
+ folderRenamed(oldFolder, newFolder) {
+ this.emit(
+ "folder-renamed",
+ convertFolder(oldFolder),
+ convertFolder(newFolder)
+ );
+ }
+})();
+
+/**
+ * Accepts a MailFolder or a MailAccount and returns the actual folder and its
+ * accountId. Throws if the requested folder does not exist.
+ */
+function getFolder({ accountId, path, id }) {
+ if (id && !path && !accountId) {
+ accountId = id;
+ path = "/";
+ }
+
+ let uri = folderPathToURI(accountId, path);
+ let folder = MailServices.folderLookup.getFolderForURL(uri);
+ if (!folder) {
+ throw new ExtensionError(`Folder not found: ${path}`);
+ }
+ return { folder, accountId };
+}
+
+/**
+ * Copy or Move a folder.
+ */
+async function doMoveCopyOperation(source, destination, isMove) {
+ // The schema file allows destination to be either a MailFolder or a
+ // MailAccount.
+ let srcFolder = getFolder(source);
+ let dstFolder = getFolder(destination);
+
+ if (
+ srcFolder.folder.server.type == "nntp" ||
+ dstFolder.folder.server.type == "nntp"
+ ) {
+ throw new ExtensionError(
+ `folders.${isMove ? "move" : "copy"}() is not supported in news accounts`
+ );
+ }
+
+ if (
+ dstFolder.folder.hasSubFolders &&
+ dstFolder.folder.subFolders.find(
+ f => f.prettyName == srcFolder.folder.prettyName
+ )
+ ) {
+ throw new ExtensionError(
+ `folders.${isMove ? "move" : "copy"}() failed, because ${
+ srcFolder.folder.prettyName
+ } already exists in ${folderURIToPath(
+ dstFolder.accountId,
+ dstFolder.folder.URI
+ )}`
+ );
+ }
+
+ let rv = await new Promise(resolve => {
+ let _destination = null;
+ const listener = {
+ folderMoveCopyCompleted(_isMove, _srcFolder, _dstFolder) {
+ if (
+ _destination != null ||
+ _isMove != isMove ||
+ _srcFolder.URI != srcFolder.folder.URI ||
+ _dstFolder.URI != dstFolder.folder.URI
+ ) {
+ return;
+ }
+
+ // The targetFolder is not the copied/moved folder, but its parent.
+ // Find the actual folder by its name (which is unique).
+ if (_dstFolder && _dstFolder.hasSubFolders) {
+ _destination = _dstFolder.subFolders.find(
+ f => f.prettyName == _srcFolder.prettyName
+ );
+ }
+ },
+ };
+ MailServices.mfn.addListener(
+ listener,
+ MailServices.mfn.folderMoveCopyCompleted
+ );
+ MailServices.copy.copyFolder(
+ srcFolder.folder,
+ dstFolder.folder,
+ isMove,
+ {
+ OnStartCopy() {},
+ OnProgress() {},
+ SetMessageKey() {},
+ GetMessageId() {},
+ OnStopCopy(status) {
+ MailServices.mfn.removeListener(listener);
+ resolve({
+ status,
+ folder: _destination,
+ });
+ },
+ },
+ null
+ );
+ });
+
+ if (!Components.isSuccessCode(rv.status)) {
+ throw new ExtensionError(
+ `folders.${isMove ? "move" : "copy"}() failed for unknown reasons`
+ );
+ }
+
+ return convertFolder(rv.folder, dstFolder.accountId);
+}
+
+/**
+ * Wait for a folder operation.
+ */
+function waitForOperation(flags, uri) {
+ return new Promise(resolve => {
+ MailServices.mfn.addListener(
+ {
+ folderAdded(childFolder) {
+ if (childFolder.parent.URI != uri) {
+ return;
+ }
+
+ MailServices.mfn.removeListener(this);
+ resolve(childFolder);
+ },
+ folderDeleted(oldFolder) {
+ if (oldFolder.URI != uri) {
+ return;
+ }
+
+ MailServices.mfn.removeListener(this);
+ resolve();
+ },
+ folderMoveCopyCompleted(move, srcFolder, destFolder) {
+ if (srcFolder.URI != uri) {
+ return;
+ }
+
+ MailServices.mfn.removeListener(this);
+ resolve(destFolder);
+ },
+ folderRenamed(oldFolder, newFolder) {
+ if (oldFolder.URI != uri) {
+ return;
+ }
+
+ MailServices.mfn.removeListener(this);
+ resolve(newFolder);
+ },
+ },
+ flags
+ );
+ });
+}
+
+this.folders = 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).
+
+ onCreated({ context, fire }) {
+ async function listener(event, createdMailFolder) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(createdMailFolder);
+ }
+ folderTracker.on("folder-created", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-created", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onRenamed({ context, fire }) {
+ async function listener(event, originalMailFolder, renamedMailFolder) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(originalMailFolder, renamedMailFolder);
+ }
+ folderTracker.on("folder-renamed", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-renamed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onMoved({ context, fire }) {
+ async function listener(event, srcMailFolder, dstMailFolder) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(srcMailFolder, dstMailFolder);
+ }
+ folderTracker.on("folder-moved", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-moved", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onCopied({ context, fire }) {
+ async function listener(event, srcMailFolder, dstMailFolder) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(srcMailFolder, dstMailFolder);
+ }
+ folderTracker.on("folder-copied", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-copied", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onDeleted({ context, fire }) {
+ async function listener(event, deletedMailFolder) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(deletedMailFolder);
+ }
+ folderTracker.on("folder-deleted", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-deleted", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ onFolderInfoChanged({ context, fire }) {
+ async function listener(event, changedMailFolder, mailFolderInfo) {
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ fire.async(changedMailFolder, mailFolderInfo);
+ }
+ folderTracker.on("folder-info-changed", listener);
+ return {
+ unregister: () => {
+ folderTracker.off("folder-info-changed", listener);
+ },
+ convert(newFire, extContext) {
+ fire = newFire;
+ context = extContext;
+ },
+ };
+ },
+ };
+
+ getAPI(context) {
+ return {
+ folders: {
+ onCreated: new EventManager({
+ context,
+ module: "folders",
+ event: "onCreated",
+ extensionApi: this,
+ }).api(),
+ onRenamed: new EventManager({
+ context,
+ module: "folders",
+ event: "onRenamed",
+ extensionApi: this,
+ }).api(),
+ onMoved: new EventManager({
+ context,
+ module: "folders",
+ event: "onMoved",
+ extensionApi: this,
+ }).api(),
+ onCopied: new EventManager({
+ context,
+ module: "folders",
+ event: "onCopied",
+ extensionApi: this,
+ }).api(),
+ onDeleted: new EventManager({
+ context,
+ module: "folders",
+ event: "onDeleted",
+ extensionApi: this,
+ }).api(),
+ onFolderInfoChanged: new EventManager({
+ context,
+ module: "folders",
+ event: "onFolderInfoChanged",
+ extensionApi: this,
+ }).api(),
+ async create(parent, childName) {
+ // The schema file allows parent to be either a MailFolder or a
+ // MailAccount.
+ let { folder: parentFolder, accountId } = getFolder(parent);
+
+ if (
+ parentFolder.hasSubFolders &&
+ parentFolder.subFolders.find(f => f.prettyName == childName)
+ ) {
+ throw new ExtensionError(
+ `folders.create() failed, because ${childName} already exists in ${folderURIToPath(
+ accountId,
+ parentFolder.URI
+ )}`
+ );
+ }
+
+ let childFolderPromise = waitForOperation(
+ MailServices.mfn.folderAdded,
+ parentFolder.URI
+ );
+ parentFolder.createSubfolder(childName, null);
+
+ let childFolder = await childFolderPromise;
+ return convertFolder(childFolder, accountId);
+ },
+ async rename({ accountId, path }, newName) {
+ let { folder } = getFolder({ accountId, path });
+
+ if (!folder.parent) {
+ throw new ExtensionError(
+ `folders.rename() failed, because it cannot rename the root of the account`
+ );
+ }
+ if (folder.server.type == "nntp") {
+ throw new ExtensionError(
+ `folders.rename() is not supported in news accounts`
+ );
+ }
+
+ if (folder.parent.subFolders.find(f => f.prettyName == newName)) {
+ throw new ExtensionError(
+ `folders.rename() failed, because ${newName} already exists in ${folderURIToPath(
+ accountId,
+ folder.parent.URI
+ )}`
+ );
+ }
+
+ let newFolderPromise = waitForOperation(
+ MailServices.mfn.folderRenamed,
+ folder.URI
+ );
+ folder.rename(newName, null);
+
+ let newFolder = await newFolderPromise;
+ return convertFolder(newFolder, accountId);
+ },
+ async move(source, destination) {
+ return doMoveCopyOperation(source, destination, true /* isMove */);
+ },
+ async copy(source, destination) {
+ return doMoveCopyOperation(source, destination, false /* isMove */);
+ },
+ async delete({ accountId, path }) {
+ if (
+ !context.extension.hasPermission("accountsFolders") ||
+ !context.extension.hasPermission("messagesDelete")
+ ) {
+ throw new ExtensionError(
+ 'Using folders.delete() requires the "accountsFolders" and the "messagesDelete" permission'
+ );
+ }
+
+ let { folder } = getFolder({ accountId, path });
+ if (folder.server.type == "nntp") {
+ throw new ExtensionError(
+ `folders.delete() is not supported in news accounts`
+ );
+ }
+
+ if (folder.server.type == "imap") {
+ let inTrash = false;
+ let parent = folder.parent;
+ while (!inTrash && parent) {
+ inTrash = parent.flags & Ci.nsMsgFolderFlags.Trash;
+ parent = parent.parent;
+ }
+ if (inTrash) {
+ // FixMe: The UI is not updated, the folder is still shown, only after
+ // a restart it is removed from trash.
+ let deletedPromise = new Promise(resolve => {
+ MailServices.imap.deleteFolder(
+ folder,
+ {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl(url, status) {
+ resolve(status);
+ },
+ },
+ null
+ );
+ });
+ let status = await deletedPromise;
+ if (!Components.isSuccessCode(status)) {
+ throw new ExtensionError(
+ `folders.delete() failed for unknown reasons`
+ );
+ }
+ } else {
+ // FixMe: Accounts could have their trash folder outside of their
+ // own folder structure.
+ let trash = folder.server.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Trash
+ );
+ let deletedPromise = new Promise(resolve => {
+ MailServices.imap.moveFolder(
+ folder,
+ trash,
+ {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl(url, status) {
+ resolve(status);
+ },
+ },
+ null
+ );
+ });
+ let status = await deletedPromise;
+ if (!Components.isSuccessCode(status)) {
+ throw new ExtensionError(
+ `folders.delete() failed for unknown reasons`
+ );
+ }
+ }
+ } else {
+ let deletedPromise = waitForOperation(
+ MailServices.mfn.folderDeleted |
+ MailServices.mfn.folderMoveCopyCompleted,
+ folder.URI
+ );
+ folder.deleteSelf(null);
+ await deletedPromise;
+ }
+ },
+ async getFolderInfo({ accountId, path }) {
+ let { folder } = getFolder({ accountId, path });
+
+ let mailFolderInfo = {
+ favorite: folder.getFlag(Ci.nsMsgFolderFlags.Favorite),
+ totalMessageCount: folder.getTotalMessages(false),
+ unreadMessageCount: folder.getNumUnread(false),
+ };
+
+ return mailFolderInfo;
+ },
+ async getParentFolders({ accountId, path }, includeFolders) {
+ let { folder } = getFolder({ accountId, path });
+ let parentFolders = [];
+ // We do not consider the absolute root ("/") as a root folder, but
+ // the first real folders (all folders returned in MailAccount.folders
+ // are considered root folders).
+ while (folder.parent != null && folder.parent.parent != null) {
+ folder = folder.parent;
+
+ if (includeFolders) {
+ parentFolders.push(traverseSubfolders(folder, accountId));
+ } else {
+ parentFolders.push(convertFolder(folder, accountId));
+ }
+ }
+ return parentFolders;
+ },
+ async getSubFolders(accountOrFolder, includeFolders) {
+ let { folder, accountId } = getFolder(accountOrFolder);
+ let subFolders = [];
+ if (folder.hasSubFolders) {
+ for (let subFolder of folder.subFolders) {
+ if (includeFolders) {
+ subFolders.push(traverseSubfolders(subFolder, accountId));
+ } else {
+ subFolders.push(convertFolder(subFolder, accountId));
+ }
+ }
+ }
+ return subFolders;
+ },
+ },
+ };
+ }
+};