summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/test/xpcshell')
-rw-r--r--comm/mail/components/extensions/test/xpcshell/.eslintrc.js13
-rw-r--r--comm/mail/components/extensions/test/xpcshell/data/utils.js124
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head-imap.js12
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head-nntp.js12
-rw-r--r--comm/mail/components/extensions/test/xpcshell/head.js298
-rw-r--r--comm/mail/components/extensions/test/xpcshell/images/redPixel.pngbin0 -> 119 bytes
-rw-r--r--comm/mail/components/extensions/test/xpcshell/images/whitePixel.pngbin0 -> 69 bytes
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/alternative.eml23
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml35
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml127
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample01.eml11
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample02.eml121
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample03.eml43
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample04.eml10
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample05.eml10
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample06.eml8
-rw-r--r--comm/mail/components/extensions/test/xpcshell/messages/sample07.eml24
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js1089
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js220
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js2043
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js139
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js238
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js148
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js101
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_alias.js123
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js350
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js279
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_folders.js560
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js374
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js146
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages.js730
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js499
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js1073
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js256
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js121
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js656
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js153
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js333
-rw-r--r--comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js415
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini7
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini23
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini7
-rw-r--r--comm/mail/components/extensions/test/xpcshell/xpcshell.ini17
43 files changed, 10971 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/xpcshell/.eslintrc.js b/comm/mail/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..60d784b53c
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ env: {
+ // The tests in this folder are testing based on WebExtensions, so lets
+ // just define the webextensions environment here.
+ webextensions: true,
+ // Many parts of WebExtensions test definitions (e.g. content scripts) also
+ // interact with the browser environment, so define that here as we don't
+ // have an easy way to handle per-function/scope usage yet.
+ browser: true,
+ },
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/data/utils.js b/comm/mail/components/extensions/test/xpcshell/data/utils.js
new file mode 100644
index 0000000000..9025982e33
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/data/utils.js
@@ -0,0 +1,124 @@
+/* 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/. */
+
+// Functions for extensions to use, so that we avoid repeating ourselves.
+
+function assertDeepEqual(
+ expected,
+ actual,
+ description = "Values should be equal",
+ options = {}
+) {
+ let ok;
+ let strict = !!options?.strict;
+ try {
+ ok = assertDeepEqualNested(expected, actual, strict);
+ } catch (e) {
+ ok = false;
+ }
+ if (!ok) {
+ browser.test.fail(
+ `Deep equal test. \n Expected value: ${JSON.stringify(
+ expected
+ )} \n Actual value: ${JSON.stringify(actual)},
+ ${description}`
+ );
+ }
+}
+
+function assertDeepEqualNested(expected, actual, strict) {
+ if (expected === null) {
+ browser.test.assertTrue(actual === null);
+ return actual === null;
+ }
+
+ if (expected === undefined) {
+ browser.test.assertTrue(actual === undefined);
+ return actual === undefined;
+ }
+
+ if (["boolean", "number", "string"].includes(typeof expected)) {
+ browser.test.assertEq(typeof expected, typeof actual);
+ browser.test.assertEq(expected, actual);
+ return typeof expected == typeof actual && expected == actual;
+ }
+
+ if (Array.isArray(expected)) {
+ browser.test.assertTrue(Array.isArray(actual));
+ browser.test.assertEq(expected.length, actual.length);
+ let ok = 0;
+ let all = 0;
+ for (let i = 0; i < expected.length; i++) {
+ all++;
+ if (assertDeepEqualNested(expected[i], actual[i], strict)) {
+ ok++;
+ }
+ }
+ return (
+ Array.isArray(actual) && expected.length == actual.length && all == ok
+ );
+ }
+
+ let expectedKeys = Object.keys(expected);
+ let actualKeys = Object.keys(actual);
+ // Ignore any extra keys on the actual object in non-strict mode (default).
+ let lengthOk = strict
+ ? expectedKeys.length == actualKeys.length
+ : expectedKeys.length <= actualKeys.length;
+ browser.test.assertTrue(lengthOk);
+
+ let ok = 0;
+ let all = 0;
+ for (let key of expectedKeys) {
+ all++;
+ browser.test.assertTrue(actualKeys.includes(key), `Key ${key} exists`);
+ if (assertDeepEqualNested(expected[key], actual[key], strict)) {
+ ok++;
+ }
+ }
+ return all == ok && lengthOk;
+}
+
+function waitForMessage() {
+ return waitForEvent("test.onMessage");
+}
+
+function waitForEvent(eventName) {
+ let [namespace, name] = eventName.split(".");
+ return new Promise(resolve => {
+ browser[namespace][name].addListener(function listener(...args) {
+ browser[namespace][name].removeListener(listener);
+ resolve(args);
+ });
+ });
+}
+
+async function waitForCondition(condition, msg, interval = 100, maxTries = 50) {
+ let conditionPassed = false;
+ let tries = 0;
+ for (; tries < maxTries && !conditionPassed; tries++) {
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ window.setTimeout(resolve, interval)
+ );
+ try {
+ conditionPassed = await condition();
+ } catch (e) {
+ throw Error(`${msg} - threw exception: ${e}`);
+ }
+ }
+ if (conditionPassed) {
+ browser.test.succeed(
+ `waitForCondition succeeded after ${tries} retries - ${msg}`
+ );
+ } else {
+ browser.test.fail(`${msg} - timed out after ${maxTries} retries`);
+ }
+}
+
+function sendMessage(...args) {
+ let replyPromise = waitForMessage();
+ browser.test.sendMessage(...args);
+ return replyPromise;
+}
diff --git a/comm/mail/components/extensions/test/xpcshell/head-imap.js b/comm/mail/components/extensions/test/xpcshell/head-imap.js
new file mode 100644
index 0000000000..ac85c52b64
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head-imap.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+/* import-globals-from head.js */
+
+var IS_IMAP = true;
+
+let wrappedCreateAccount = createAccount;
+createAccount = function (type = "imap") {
+ return wrappedCreateAccount(type);
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/head-nntp.js b/comm/mail/components/extensions/test/xpcshell/head-nntp.js
new file mode 100644
index 0000000000..0b4a56d0dc
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head-nntp.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+/* import-globals-from head.js */
+
+var IS_NNTP = true;
+
+let wrappedCreateAccount = createAccount;
+createAccount = function (type = "nntp") {
+ return wrappedCreateAccount(type);
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/head.js b/comm/mail/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..f8c0c0e7b9
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,298 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { fsDebugAll, gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Persistent Listener test functionality
+var { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+ExtensionTestUtils.init(this);
+
+var IS_IMAP = false;
+var IS_NNTP = false;
+
+function formatVCard(strings, ...values) {
+ let arr = [];
+ for (let str of strings) {
+ arr.push(str);
+ arr.push(values.shift());
+ }
+ let lines = arr.join("").split("\n");
+ let indent = lines[1].length - lines[1].trimLeft().length;
+ let outLines = [];
+ for (let line of lines) {
+ if (line.length > 0) {
+ outLines.push(line.substring(indent) + "\r\n");
+ }
+ }
+ return outLines.join("");
+}
+
+function createAccount(type = "none") {
+ let account;
+
+ if (type == "local") {
+ MailServices.accounts.createLocalMailAccount();
+ account = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+ } else {
+ account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ `${account.key}user`,
+ "localhost",
+ type
+ );
+ }
+
+ if (type == "imap") {
+ IMAPServer.open();
+ account.incomingServer.port = IMAPServer.port;
+ account.incomingServer.username = "user";
+ account.incomingServer.password = "password";
+ }
+
+ if (type == "nntp") {
+ NNTPServer.open();
+ account.incomingServer.port = NNTPServer.port;
+ }
+ info(`Created account ${account.toString()}`);
+ return account;
+}
+
+function cleanUpAccount(account) {
+ let serverKey = account.incomingServer.key;
+ let serverType = account.incomingServer.type;
+ info(
+ `Cleaning up ${serverType} account ${account.key} and server ${serverKey}`
+ );
+ MailServices.accounts.removeAccount(account, true);
+
+ try {
+ let server = MailServices.accounts.getIncomingServer(serverKey);
+ if (server) {
+ info(`Cleaning up leftover ${serverType} server ${serverKey}`);
+ MailServices.accounts.removeIncomingServer(server, false);
+ }
+ } catch (e) {}
+}
+
+registerCleanupFunction(() => {
+ MailServices.accounts.accounts.forEach(cleanUpAccount);
+});
+
+function addIdentity(account, email = "xpcshell@localhost") {
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = email;
+ account.addIdentity(identity);
+ if (!account.defaultIdentity) {
+ account.defaultIdentity = identity;
+ }
+ info(`Created identity ${identity.toString()}`);
+ return identity;
+}
+
+async function createSubfolder(parent, name) {
+ if (parent.server.type == "nntp") {
+ createNewsgroup(name);
+ let account = MailServices.accounts.FindAccountForServer(parent.server);
+ subscribeNewsgroup(account, name);
+ return parent.getChildNamed(name);
+ }
+
+ let promiseAdded = PromiseTestUtils.promiseFolderAdded(name);
+ parent.createSubfolder(name, null);
+ await promiseAdded;
+ return parent.getChildNamed(name);
+}
+
+function createMessages(folder, makeMessagesArg) {
+ if (typeof makeMessagesArg == "number") {
+ makeMessagesArg = { count: makeMessagesArg };
+ }
+ if (!createMessages.messageGenerator) {
+ createMessages.messageGenerator = new MessageGenerator();
+ }
+
+ let messages = createMessages.messageGenerator.makeMessages(makeMessagesArg);
+ return addGeneratedMessages(folder, messages);
+}
+
+class FakeGeneratedMessage {
+ constructor(msg) {
+ this.msg = msg;
+ }
+ toMessageString() {
+ return this.msg;
+ }
+ toMboxString() {
+ // A cheap hack. It works for existing uses but may not work for future uses.
+ let fromAddress = this.msg.match(/From: .* <(.*@.*)>/)[0];
+ let mBoxString = `From ${fromAddress}\r\n${this.msg}`;
+ // Ensure a trailing empty line.
+ if (!mBoxString.endsWith("\r\n")) {
+ mBoxString = mBoxString + "\r\n";
+ }
+ return mBoxString;
+ }
+}
+
+async function createMessageFromFile(folder, path) {
+ let message = await IOUtils.readUTF8(path);
+ return addGeneratedMessages(folder, [new FakeGeneratedMessage(message)]);
+}
+
+async function createMessageFromString(folder, message) {
+ return addGeneratedMessages(folder, [new FakeGeneratedMessage(message)]);
+}
+
+async function addGeneratedMessages(folder, messages) {
+ if (folder.server.type == "imap") {
+ return IMAPServer.addMessages(folder, messages);
+ }
+ if (folder.server.type == "nntp") {
+ return NNTPServer.addMessages(folder, messages);
+ }
+
+ let messageStrings = messages.map(message => message.toMboxString());
+ folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder.addMessageBatch(messageStrings);
+ folder.callFilterPlugins(null);
+ return Promise.resolve();
+}
+
+async function getUtilsJS() {
+ return IOUtils.readUTF8(do_get_file("data/utils.js").path);
+}
+
+var IMAPServer = {
+ open() {
+ let { ImapDaemon, ImapMessage, IMAP_RFC3501_handler } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+ );
+ IMAPServer.ImapMessage = ImapMessage;
+
+ this.daemon = new ImapDaemon();
+ this.server = new nsMailServer(
+ daemon => new IMAP_RFC3501_handler(daemon),
+ this.daemon
+ );
+ this.server.start();
+
+ registerCleanupFunction(() => this.close());
+ },
+ close() {
+ this.server.stop();
+ },
+ get port() {
+ return this.server.port;
+ },
+
+ addMessages(folder, messages) {
+ let fakeFolder = IMAPServer.daemon.getMailbox(folder.name);
+ messages.forEach(message => {
+ if (typeof message != "string") {
+ message = message.toMessageString();
+ }
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(message)
+ );
+ let imapMsg = new IMAPServer.ImapMessage(
+ msgURI.spec,
+ fakeFolder.uidnext++,
+ []
+ );
+ fakeFolder.addMessage(imapMsg);
+ });
+
+ return new Promise(resolve =>
+ mailTestUtils.updateFolderAndNotify(folder, resolve)
+ );
+ },
+};
+
+function subscribeNewsgroup(account, group) {
+ account.incomingServer.QueryInterface(Ci.nsINntpIncomingServer);
+ account.incomingServer.subscribeToNewsgroup(group);
+ account.incomingServer.maximumConnectionsNumber = 1;
+}
+
+function createNewsgroup(group) {
+ if (!NNTPServer.hasGroup(group)) {
+ NNTPServer.addGroup(group);
+ }
+}
+
+var NNTPServer = {
+ open() {
+ let { NNTP_RFC977_handler, NntpDaemon } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Nntpd.jsm"
+ );
+
+ this.daemon = new NntpDaemon();
+ this.server = new nsMailServer(
+ daemon => new NNTP_RFC977_handler(daemon),
+ this.daemon
+ );
+ this.server.start();
+
+ registerCleanupFunction(() => this.close());
+ },
+
+ close() {
+ this.server.stop();
+ },
+ get port() {
+ return this.server.port;
+ },
+
+ addGroup(group) {
+ return this.daemon.addGroup(group);
+ },
+
+ hasGroup(group) {
+ return this.daemon.getGroup(group) != null;
+ },
+
+ addMessages(folder, messages) {
+ let { NewsArticle } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Nntpd.jsm"
+ );
+
+ let group = folder.name;
+ messages.forEach(message => {
+ if (typeof message != "string") {
+ message = message.toMessageString();
+ }
+ // The NNTP daemon needs a trailing empty line.
+ if (!message.endsWith("\r\n")) {
+ message = message + "\r\n";
+ }
+ let article = new NewsArticle(message);
+ article.groups = [group];
+ this.daemon.addArticle(article);
+ });
+
+ return new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+ },
+};
diff --git a/comm/mail/components/extensions/test/xpcshell/images/redPixel.png b/comm/mail/components/extensions/test/xpcshell/images/redPixel.png
new file mode 100644
index 0000000000..abda018027
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/images/redPixel.png
Binary files differ
diff --git a/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png
new file mode 100644
index 0000000000..5514ad40e9
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png
Binary files differ
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml b/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml
new file mode 100644
index 0000000000..11de6a87d6
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/alternative.eml
@@ -0,0 +1,23 @@
+Message-ID: <alternative.eml@mime.sample>
+Date: Fri, 19 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>, Karl <friedrich@example.com>
+From: Doug Sauder <dwsauder@example.com>
+Subject: Default content-types
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="=====================_714967308==_.ALT"
+
+This message is in MIME format. The first part should be readable text,
+while the remaining parts are likely unreadable without MIME-aware tools.
+
+--=====================_714967308==_.ALT
+Content-Transfer-Encoding: quoted-printable
+
+I am TEXT!
+
+--=====================_714967308==_.ALT
+Content-Type: text/html
+
+<html><body>I <b>am</b> HTML!</body></html>
+
+--=====================_714967308==_.ALT--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml b/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml
new file mode 100644
index 0000000000..85a54b66c5
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml
@@ -0,0 +1,35 @@
+Message-ID: <sample.eml@mime.sample>
+Date: Fri, 20 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+From: Batman <bruce@example.com>
+Subject: Attached message without subject
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------49CVLb1N6p6Spdka4qq7Naeg"
+
+This is a multi-part message in MIME format.
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body>
+ <p>This message has one email attachment with missing headers.<br>
+ </p>
+ </body>
+</html>
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: message/rfc822; charset=UTF-8; name="message1.eml"
+Content-Disposition: attachment; filename="message1.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-attached.eml@mime.sample>
+MIME-Version: 1.0
+
+This is my body
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml b/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml
new file mode 100644
index 0000000000..5ced639ff8
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml
@@ -0,0 +1,127 @@
+Message-ID: <sample.eml@mime.sample>
+Date: Fri, 20 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+Cc: Robin <damian@wayne-enterprises.com>
+From: Batman <bruce@wayne-enterprises.com>
+Subject: Attached message with attachments
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="------------49CVLb1N6p6Spdka4qq7Naeg"
+
+This is a multi-part message in MIME format.
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body>
+ <p>This message has one normal attachment and one email attachment,
+ which itself has 3 attachments.<br>
+ </p>
+ </body>
+</html>
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: message/rfc822; charset=UTF-8; name="message1.eml"
+Content-Disposition: attachment; filename="message1.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-attached.eml@mime.sample>
+From: Superman <clark.kent@dailyplanet.com>
+To: Jimmy <jimmy.olsen@dailyplanet.com>
+Subject: Test message 1
+Date: Wed, 17 May 2000 19:32:47 -0400
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0002_01BFC036.AE309650"
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: 7bit
+
+Message with multiple attachments.
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="whitePixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="whitePixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnn
+AAAAAElFTkSuQmCC
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="greenPixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
+jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+C76AoAAhUBJel4xsMAAAAASUVO
+RK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="redPixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
+jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+hgkAYAAbcApOp/9LEAAAAASUVO
+RK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: message/rfc822; charset=UTF-8; name="message2.eml"
+Content-Disposition: attachment; filename="message2.eml"
+Content-Transfer-Encoding: 7bit
+
+Message-ID: <sample-nested-attached.eml@mime.sample>
+From: Jimmy <jimmy.olsen@dailyplanet.com>
+To: Superman <clark.kent@dailyplanet.com>
+Subject: Test message 2
+Date: Wed, 16 May 2000 19:32:47 -0400
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0003_01BFC036.AE309650"
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0003_01BFC036.AE309650
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: 7bit
+
+This message has an attachment
+
+------=_NextPart_000_0003_01BFC036.AE309650
+Content-Type: image/png;
+ name="whitePixel.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="whitePixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnn
+AAAAAElFTkSuQmCC
+
+------=_NextPart_000_0003_01BFC036.AE309650--
+
+------=_NextPart_000_0002_01BFC036.AE309650--
+
+--------------49CVLb1N6p6Spdka4qq7Naeg
+Content-Type: image/png;
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="yellowPixel.png"
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1B
+AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j/iQEABOUB8pypNlQA
+AAAASUVORK5CYII=
+
+--------------49CVLb1N6p6Spdka4qq7Naeg--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml
new file mode 100644
index 0000000000..f7ac14a07d
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample01.eml
@@ -0,0 +1,11 @@
+From: Bug Reporter <new@thunderbird.bug>
+Newsgroups: gmane.comp.mozilla.thundebird.user
+Subject: =?UTF-8?B?zrHOu8+GzqzOss63z4TOvw==?=
+Date: Thu, 27 May 2021 21:23:35 +0100
+Message-ID: <01.eml@mime.sample>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8;
+Content-Transfer-Encoding: base64
+Content-Disposition: inline
+
+zobOu8+GzrEK
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml
new file mode 100644
index 0000000000..74b60b5665
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample02.eml
@@ -0,0 +1,121 @@
+From: "Doug Sauder" <doug@example.com>
+To: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@example.com>
+Subject: Test message from Microsoft Outlook 00
+Date: Wed, 17 May 2000 19:32:47 -0400
+Message-ID: <02.eml@mime.sample>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_0002_01BFC036.AE309650"
+X-Priority: 3 (Normal)
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
+Importance: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+Die Hasen und die Fr=F6sche=20
+=20
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="blueball1.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="blueball2.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgAAAAA
+CCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQ
+MYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO
+5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaMvee1
+5++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAADBMg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbssceb
+L5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18P
+yqqWUw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC
+UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/MjRxm
+T6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1CSYoOiMOS
+GwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B
+1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD
+/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZz
+O7wAAAAASUVORK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png;
+ name="greenball.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAAIQAA
+CAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAhrQBCvRhj
+xjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBCzhDO55Te563G
+55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY1gAx5wBS7yFr7zlK
+7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAp1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAFtSURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy
++N9ery1bVe9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh
+0fHJaTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm
+kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJVG9Ea
+EjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyexI0ZxBP3a
+fE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWrYUO23hMANUKR
+Rl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0RVh0Q29tbWVudABj
+bGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII=
+
+------=_NextPart_000_0002_01BFC036.AE309650
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="redball.png"
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa
+AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0
+AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM
+AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm
+f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB
+AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2
+AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH
+AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC
+AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe
+AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs
+AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV
+AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM
+AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK
+iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ
+29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW
+SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+
+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q
+m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV
+tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw
+HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5
+QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd
+tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5
+IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg==
+
+------=_NextPart_000_0002_01BFC036.AE309650--
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml
new file mode 100644
index 0000000000..3eb8e06802
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample03.eml
@@ -0,0 +1,43 @@
+From: =?iso-8859-1?Q?Heinz_M=FCller?= <mueller@example.com>
+To: "Joe Blow" <jblow@example.com>
+Subject: Test message from Microsoft Outlook 00
+Date: Wed, 17 May 2000 19:35:05 -0400
+Message-ID: <03.eml@mime.sample>
+MIME-Version: 1.0
+Content-Type: image/png;
+ name="doubelspace ball.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="doubelspace ball.png"
+X-Priority: 3 (Normal)
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
+Importance: Normal
+X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300
+
+iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAVAAAa
+AAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACHAAB9AAB0
+AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABM
+AAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHfhITm
+f3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5PlrKzpmZntZWXvJSXXAADB
+AACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2
+AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABH
+AAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABPAAASAAAC
+AABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADIAADTAADNAACzAACDAABuAAAe
+AAB+AADAAACkAACNAAB/AABpAABQAAAwAACRAACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACs
+AACvAACtAACmAACJAAB6AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABV
+AACOAACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAM
+AAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAD8LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
+MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iK
+iUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ
+29ja2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW
+SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2YnOAj+
+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/uXLzVJ2q
+m6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqV
+tWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZw
+HBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/joOyYed5
+QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms1y9evXid7QZacgOxmSxktNzd
+tSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5
+IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg==
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml
new file mode 100644
index 0000000000..6dd2a94b56
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample04.eml
@@ -0,0 +1,10 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: =?koi8-r?B?4czGwdfJ1Ao=?=
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <04.eml@mime.sample>
+Content-Type: text/plain; charset=koi8-r;
+Content-Transfer-Encoding: base64
+
+98/Q0s/TCg== \ No newline at end of file
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml
new file mode 100644
index 0000000000..6e70eee744
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample05.eml
@@ -0,0 +1,10 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: =?windows-1251?B?wOv04OLo8go=?=
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <05.eml@mime.sample>
+Content-Type: text/plain; charset=windows-1251;
+Content-Transfer-Encoding: base64
+
+wu7v8O7xCg== \ No newline at end of file
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml
new file mode 100644
index 0000000000..a5b3a40ac5
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample06.eml
@@ -0,0 +1,8 @@
+Newsgroups: gmane.comp.mozilla.thundebird.user
+From: Bug Reporter <new@thunderbird.bug>
+Subject: I have no content type
+Date: Sun, 27 May 2001 21:23:35 +0100
+MIME-Version: 1.0
+Message-ID: <06.eml@mime.sample>
+
+No content type
diff --git a/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml b/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml
new file mode 100644
index 0000000000..29283b2ce0
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/messages/sample07.eml
@@ -0,0 +1,24 @@
+Message-ID: <07.eml@mime.sample>
+Date: Fri, 19 May 2000 00:29:55 -0400
+To: Heinz <mueller@example.com>
+From: Doug Sauder <dwsauder@example.com>
+Subject: Default content-types
+Mime-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="=====================_714967308==_.ALT"
+
+This message is in MIME format. The first part should be readable text,
+while the remaining parts are likely unreadable without MIME-aware tools.
+
+--=====================_714967308==_.ALT
+Content-Transfer-Encoding: quoted-printable
+
+Die Hasen
+
+--=====================_714967308==_.ALT
+Content-Type: text/html
+
+<html><body><b>Die Hasen</b></body></html>
+
+--=====================_714967308==_.ALT--
+
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js
new file mode 100644
index 0000000000..ac6f5482ce
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js
@@ -0,0 +1,1089 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_accounts() {
+ // Here all the accounts are local but the first account will behave as
+ // an actual local account and will be kept last always.
+ let files = {
+ "background.js": async () => {
+ let [account1Id, account1Name] = await window.waitForMessage();
+
+ let defaultAccount = await browser.accounts.getDefault();
+ browser.test.assertEq(
+ null,
+ defaultAccount,
+ "The default account should be null, as none is defined."
+ );
+
+ let result1 = await browser.accounts.list();
+ browser.test.assertEq(1, result1.length);
+ window.assertDeepEqual(
+ {
+ id: account1Id,
+ name: account1Name,
+ type: "none",
+ folders: [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ },
+ result1[0]
+ );
+
+ // Test that excluding folders works.
+ let result1WithOutFolders = await browser.accounts.list(false);
+ for (let account of result1WithOutFolders) {
+ browser.test.assertEq(null, account.folders, "Folders not included");
+ }
+
+ let [account2Id, account2Name] = await window.sendMessage(
+ "create account 2"
+ );
+ // The new account is defined as default and should be returned first.
+ let result2 = await browser.accounts.list();
+ browser.test.assertEq(2, result2.length);
+ window.assertDeepEqual(
+ [
+ {
+ id: account2Id,
+ name: account2Name,
+ type: "imap",
+ folders: [
+ {
+ accountId: account2Id,
+ name: "Inbox",
+ path: "/INBOX",
+ type: "inbox",
+ },
+ ],
+ },
+ {
+ id: account1Id,
+ name: account1Name,
+ type: "none",
+ folders: [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ },
+ ],
+ result2
+ );
+
+ let result3 = await browser.accounts.get(account1Id);
+ window.assertDeepEqual(result1[0], result3);
+ let result4 = await browser.accounts.get(account2Id);
+ window.assertDeepEqual(result2[0], result4);
+
+ let result3WithoutFolders = await browser.accounts.get(account1Id, false);
+ browser.test.assertEq(
+ null,
+ result3WithoutFolders.folders,
+ "Folders not included"
+ );
+ let result4WithoutFolders = await browser.accounts.get(account2Id, false);
+ browser.test.assertEq(
+ null,
+ result4WithoutFolders.folders,
+ "Folders not included"
+ );
+
+ await window.sendMessage("create folders");
+ let result5 = await browser.accounts.get(account1Id);
+ let platformInfo = await browser.runtime.getPlatformInfo();
+ window.assertDeepEqual(
+ [
+ {
+ accountId: account1Id,
+ name: "Trash",
+ path: "/Trash",
+ subFolders: [
+ {
+ accountId: account1Id,
+ name: "%foo %test% 'bar'(!)+",
+ path: "/Trash/%foo %test% 'bar'(!)+",
+ },
+ {
+ accountId: account1Id,
+ name: "Ϟ",
+ // This character is not supported on Windows, so it gets hashed,
+ // by NS_MsgHashIfNecessary.
+ path: platformInfo.os == "win" ? "/Trash/b52bc214" : "/Trash/Ϟ",
+ },
+ ],
+ type: "trash",
+ },
+ {
+ accountId: account1Id,
+ name: "Outbox",
+ path: "/Unsent Messages",
+ type: "outbox",
+ },
+ ],
+ result5.folders
+ );
+
+ // Check we can access the folders through folderPathToURI.
+ for (let folder of result5.folders) {
+ await browser.messages.list(folder);
+ }
+
+ let result6 = await browser.accounts.get(account2Id);
+ window.assertDeepEqual(
+ [
+ {
+ accountId: account2Id,
+ name: "Inbox",
+ path: "/INBOX",
+ subFolders: [
+ {
+ accountId: account2Id,
+ name: "%foo %test% 'bar'(!)+",
+ path: "/INBOX/%foo %test% 'bar'(!)+",
+ },
+ {
+ accountId: account2Id,
+ name: "Ϟ",
+ path: "/INBOX/&A94-",
+ },
+ ],
+ type: "inbox",
+ },
+ {
+ // The trash folder magically appears at this point.
+ // It wasn't here before.
+ accountId: "account2",
+ name: "Trash",
+ path: "/Trash",
+ type: "trash",
+ },
+ ],
+ result6.folders
+ );
+
+ // Check we can access the folders through folderPathToURI.
+ for (let folder of result6.folders) {
+ await browser.messages.list(folder);
+ }
+
+ defaultAccount = await browser.accounts.getDefault();
+ browser.test.assertEq(result2[0].id, defaultAccount.id);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ let account1 = createAccount();
+ extension.sendMessage(account1.key, account1.incomingServer.prettyName);
+
+ await extension.awaitMessage("create account 2");
+ let account2 = createAccount("imap");
+ IMAPServer.open();
+ account2.incomingServer.port = IMAPServer.port;
+ account2.incomingServer.username = "user";
+ account2.incomingServer.password = "password";
+ MailServices.accounts.defaultAccount = account2;
+ extension.sendMessage(account2.key, account2.incomingServer.prettyName);
+
+ await extension.awaitMessage("create folders");
+ let inbox1 = account1.incomingServer.rootFolder.subFolders[0];
+ // Test our code can handle characters that might be escaped.
+ inbox1.createSubfolder("%foo %test% 'bar'(!)+", null);
+ inbox1.createSubfolder("Ϟ", null); // Test our code can handle unicode.
+
+ let inbox2 = account2.incomingServer.rootFolder.subFolders[0];
+ inbox2.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter = "/";
+ // Test our code can handle characters that might be escaped.
+ inbox2.createSubfolder("%foo %test% 'bar'(!)+", null);
+ await PromiseTestUtils.promiseFolderAdded("%foo %test% 'bar'(!)+");
+ inbox2.createSubfolder("Ϟ", null); // Test our code can handle unicode.
+ await PromiseTestUtils.promiseFolderAdded("Ϟ");
+
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account1);
+ cleanUpAccount(account2);
+});
+
+add_task(async function test_identities() {
+ let account1 = createAccount();
+ let account2 = createAccount("imap");
+ let identity0 = addIdentity(account1, "id0@invalid");
+ let identity1 = addIdentity(account1, "id1@invalid");
+ let identity2 = addIdentity(account1, "id2@invalid");
+ let identity3 = addIdentity(account2, "id3@invalid");
+ addIdentity(account2, "id4@invalid");
+ identity2.label = "A label";
+ identity2.fullName = "Identity 2!";
+ identity2.organization = "Dis Organization";
+ identity2.replyTo = "reply@invalid";
+ identity2.composeHtml = true;
+ identity2.htmlSigText = "This is me. And this is my Dog.";
+ identity2.htmlSigFormat = false;
+
+ equal(account1.defaultIdentity.key, identity0.key);
+ equal(account2.defaultIdentity.key, identity3.key);
+ let files = {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(2, accounts.length);
+
+ const localAccount = accounts.find(account => account.type == "none");
+ const imapAccount = accounts.find(account => account.type == "imap");
+
+ // Register event listener.
+ let onCreatedLog = [];
+ browser.identities.onCreated.addListener((id, created) => {
+ onCreatedLog.push({ id, created });
+ });
+ let onUpdatedLog = [];
+ browser.identities.onUpdated.addListener((id, changed) => {
+ onUpdatedLog.push({ id, changed });
+ });
+ let onDeletedLog = [];
+ browser.identities.onDeleted.addListener(id => {
+ onDeletedLog.push(id);
+ });
+
+ const { id: accountId, identities } = localAccount;
+ const identityIds = identities.map(i => i.id);
+ browser.test.assertEq(3, identities.length);
+
+ browser.test.assertEq(accountId, identities[0].accountId);
+ browser.test.assertEq("id0@invalid", identities[0].email);
+ browser.test.assertEq(accountId, identities[1].accountId);
+ browser.test.assertEq("id1@invalid", identities[1].email);
+ browser.test.assertEq(accountId, identities[2].accountId);
+ browser.test.assertEq("id2@invalid", identities[2].email);
+ browser.test.assertEq("A label", identities[2].label);
+ browser.test.assertEq("Identity 2!", identities[2].name);
+ browser.test.assertEq("Dis Organization", identities[2].organization);
+ browser.test.assertEq("reply@invalid", identities[2].replyTo);
+ browser.test.assertEq(true, identities[2].composeHtml);
+ browser.test.assertEq(
+ "This is me. And this is my Dog.",
+ identities[2].signature
+ );
+ browser.test.assertEq(true, identities[2].signatureIsPlainText);
+
+ // Testing browser.identities.list().
+
+ let allIdentities = await browser.identities.list();
+ browser.test.assertEq(5, allIdentities.length);
+
+ let localIdentities = await browser.identities.list(localAccount.id);
+ browser.test.assertEq(
+ 3,
+ localIdentities.length,
+ "number of local identities is correct"
+ );
+ for (let i = 0; i < 2; i++) {
+ browser.test.assertEq(
+ localAccount.identities[i].id,
+ localIdentities[i].id,
+ "returned local identity is correct"
+ );
+ }
+
+ let imapIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 2,
+ imapIdentities.length,
+ "number of imap identities is correct"
+ );
+ for (let i = 0; i < 1; i++) {
+ browser.test.assertEq(
+ imapAccount.identities[i].id,
+ imapIdentities[i].id,
+ "returned imap identity is correct"
+ );
+ }
+
+ // Testing browser.identities.get().
+
+ let badIdentity = await browser.identities.get("funny");
+ browser.test.assertEq(null, badIdentity);
+
+ for (let identity of identities) {
+ let testIdentity = await browser.identities.get(identity.id);
+ for (let prop of Object.keys(identity)) {
+ browser.test.assertEq(
+ identity[prop],
+ testIdentity[prop],
+ `Testing identity.${prop}`
+ );
+ }
+ }
+
+ // Testing browser.identities.delete().
+
+ let imapDefaultIdentity = await browser.identities.getDefault(
+ imapAccount.id
+ );
+ let imapNonDefaultIdentity = imapIdentities.find(
+ identity => identity.id != imapDefaultIdentity.id
+ );
+
+ await browser.identities.delete(imapNonDefaultIdentity.id);
+ imapIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 1,
+ imapIdentities.length,
+ "number of imap identities after delete is correct"
+ );
+ browser.test.assertEq(
+ imapDefaultIdentity.id,
+ imapIdentities[0].id,
+ "leftover identity after delete is correct"
+ );
+
+ await browser.test.assertRejects(
+ browser.identities.delete(imapDefaultIdentity.id),
+ `Identity ${imapDefaultIdentity.id} is the default identity of account ${imapAccount.id} and cannot be deleted`,
+ "browser.identities.delete threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.identities.delete("somethingInvalid"),
+ "Identity not found: somethingInvalid",
+ "browser.identities.delete threw exception"
+ );
+
+ // Testing browser.identities.create().
+
+ let createTests = [
+ {
+ // Set all.
+ accountId: imapAccount.id,
+ details: {
+ email: "id0+test@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "id0+test@invalid",
+ signature: "This is Bruce. And this is my Cat.",
+ composeHtml: true,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Set some.
+ accountId: imapAccount.id,
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ },
+ {
+ // Set none.
+ accountId: imapAccount.id,
+ details: {},
+ },
+ {
+ // Set some on an invalid account.
+ accountId: "somethingInvalid",
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expectedThrow: `Account not found: somethingInvalid`,
+ },
+ {
+ // Try to set a protected property.
+ accountId: imapAccount.id,
+ details: {
+ accountId: "accountId5",
+ },
+ expectedThrow: `Setting the accountId property of a MailIdentity is not supported.`,
+ },
+ {
+ // Try to set a protected property together with others.
+ accountId: imapAccount.id,
+ details: {
+ id: "id8",
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ expectedThrow: `Setting the id property of a MailIdentity is not supported.`,
+ },
+ ];
+ for (let createTest of createTests) {
+ if (createTest.expectedThrow) {
+ await browser.test.assertRejects(
+ browser.identities.create(createTest.accountId, createTest.details),
+ createTest.expectedThrow,
+ `It rejects as expected: ${createTest.expectedThrow}.`
+ );
+ } else {
+ let createPromise = new Promise(resolve => {
+ const callback = (id, identity) => {
+ browser.identities.onCreated.removeListener(callback);
+ resolve(identity);
+ };
+ browser.identities.onCreated.addListener(callback);
+ });
+ let createdIdentity = await browser.identities.create(
+ createTest.accountId,
+ createTest.details
+ );
+ let createdIdentity2 = await createPromise;
+
+ let expected = createTest.details;
+ for (let prop of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected[prop],
+ createdIdentity[prop],
+ `Testing created identity.${prop}`
+ );
+ browser.test.assertEq(
+ expected[prop],
+ createdIdentity2[prop],
+ `Testing created identity.${prop}`
+ );
+ }
+ await browser.identities.delete(createdIdentity.id);
+ }
+
+ let foundIdentities = await browser.identities.list(imapAccount.id);
+ browser.test.assertEq(
+ 1,
+ foundIdentities.length,
+ "number of imap identities after create/delete is correct"
+ );
+ }
+
+ // Testing browser.identities.update().
+
+ let updateTests = [
+ {
+ // Set all.
+ identityId: identities[2].id,
+ details: {
+ email: "id0+test@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "id0+test@invalid",
+ signature: "This is Bruce. And this is my Cat.",
+ composeHtml: true,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Set some.
+ identityId: identities[2].id,
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expected: {
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ // Clear.
+ identityId: identities[2].id,
+ details: {
+ email: "",
+ label: "",
+ name: "",
+ organization: "",
+ replyTo: "",
+ signature: "",
+ composeHtml: false,
+ signatureIsPlainText: true,
+ },
+ },
+ {
+ // Try to update an invalid identity.
+ identityId: "somethingInvalid",
+ details: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ },
+ expectedThrow: "Identity not found: somethingInvalid",
+ },
+ {
+ // Try to update a protected property.
+ identityId: identities[2].id,
+ details: {
+ accountId: "accountId5",
+ },
+ expectedThrow:
+ "Setting the accountId property of a MailIdentity is not supported.",
+ },
+ {
+ // Try to update another protected property together with others.
+ identityId: identities[2].id,
+ details: {
+ id: "id8",
+ email: "id0+work@invalid",
+ label: "TestLabel",
+ name: "Mr. Test",
+ organization: "MZLA",
+ replyTo: "",
+ signature: "I am Batman.",
+ composeHtml: false,
+ signatureIsPlainText: false,
+ },
+ expectedThrow:
+ "Setting the id property of a MailIdentity is not supported.",
+ },
+ ];
+ for (let updateTest of updateTests) {
+ if (updateTest.expectedThrow) {
+ await browser.test.assertRejects(
+ browser.identities.update(
+ updateTest.identityId,
+ updateTest.details
+ ),
+ updateTest.expectedThrow,
+ `It rejects as expected: ${updateTest.expectedThrow}.`
+ );
+ continue;
+ }
+
+ let updatePromise = new Promise(resolve => {
+ const callback = (id, changed) => {
+ browser.identities.onUpdated.removeListener(callback);
+ resolve(changed);
+ };
+ browser.identities.onUpdated.addListener(callback);
+ });
+ let updatedIdentity = await browser.identities.update(
+ updateTest.identityId,
+ updateTest.details
+ );
+ await updatePromise;
+
+ let returnedIdentity = await browser.identities.get(
+ updateTest.identityId
+ );
+
+ let expected = updateTest.expected || updateTest.details;
+ for (let prop of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected[prop],
+ updatedIdentity[prop],
+ `Testing updated identity.${prop}`
+ );
+ browser.test.assertEq(
+ expected[prop],
+ returnedIdentity[prop],
+ `Testing returned identity.${prop}`
+ );
+ }
+ }
+
+ // Testing getDefault().
+
+ let defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[0].id, defaultIdentity.id);
+
+ await browser.identities.setDefault(accountId, identityIds[2]);
+ defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[2].id, defaultIdentity.id);
+
+ let { identities: newIdentities } = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, newIdentities.length);
+ browser.test.assertEq(identityIds[2], newIdentities[0].id);
+ browser.test.assertEq(identityIds[0], newIdentities[1].id);
+ browser.test.assertEq(identityIds[1], newIdentities[2].id);
+
+ await browser.identities.setDefault(accountId, identityIds[1]);
+ defaultIdentity = await browser.identities.getDefault(accountId);
+ browser.test.assertEq(identities[1].id, defaultIdentity.id);
+
+ ({ identities: newIdentities } = await browser.accounts.get(accountId));
+ browser.test.assertEq(3, newIdentities.length);
+ browser.test.assertEq(identityIds[1], newIdentities[0].id);
+ browser.test.assertEq(identityIds[2], newIdentities[1].id);
+ browser.test.assertEq(identityIds[0], newIdentities[2].id);
+
+ // Check event listeners.
+ window.assertDeepEqual(
+ onCreatedLog,
+ [
+ {
+ id: "id6",
+ created: {
+ accountId: "account4",
+ id: "id6",
+ label: "TestLabel",
+ name: "Mr. Test",
+ email: "id0+test@invalid",
+ replyTo: "id0+test@invalid",
+ organization: "MZLA",
+ composeHtml: true,
+ signature: "This is Bruce. And this is my Cat.",
+ signatureIsPlainText: false,
+ },
+ },
+ {
+ id: "id7",
+ created: {
+ accountId: "account4",
+ id: "id7",
+ label: "",
+ name: "",
+ email: "id0+work@invalid",
+ replyTo: "",
+ organization: "",
+ composeHtml: false,
+ signature: "I am Batman.",
+ signatureIsPlainText: true,
+ },
+ },
+ {
+ id: "id8",
+ created: {
+ accountId: "account4",
+ id: "id8",
+ label: "",
+ name: "",
+ email: "",
+ replyTo: "",
+ organization: "",
+ composeHtml: true,
+ signature: "",
+ signatureIsPlainText: true,
+ },
+ },
+ ],
+ "captured onCreated events are correct"
+ );
+ window.assertDeepEqual(
+ onUpdatedLog,
+ [
+ {
+ id: "id3",
+ changed: {
+ label: "TestLabel",
+ name: "Mr. Test",
+ email: "id0+test@invalid",
+ replyTo: "id0+test@invalid",
+ organization: "MZLA",
+ signature: "This is Bruce. And this is my Cat.",
+ signatureIsPlainText: false,
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ {
+ id: "id3",
+ changed: {
+ email: "id0+work@invalid",
+ replyTo: "",
+ composeHtml: false,
+ signature: "I am Batman.",
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ {
+ id: "id3",
+ changed: {
+ label: "",
+ name: "",
+ email: "",
+ organization: "",
+ signature: "",
+ signatureIsPlainText: true,
+ accountId: "account3",
+ id: "id3",
+ },
+ },
+ ],
+ "captured onUpdated events are correct"
+ );
+ window.assertDeepEqual(
+ onDeletedLog,
+ ["id5", "id6", "id7", "id8"],
+ "captured onDeleted events are correct"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ equal(account1.defaultIdentity.key, identity1.key);
+
+ cleanUpAccount(account1);
+ cleanUpAccount(account2);
+});
+
+add_task(async function test_identities_without_write_permissions() {
+ let account = createAccount();
+ let identity0 = addIdentity(account, "id0@invalid");
+
+ equal(account.defaultIdentity.key, identity0.key);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ const [{ identities }] = accounts;
+ browser.test.assertEq(1, identities.length);
+
+ // Testing browser.identities.update().
+
+ await browser.test.assertThrows(
+ () => browser.identities.update(identities[0].id, {}),
+ "browser.identities.update is not a function",
+ "It rejects for a missing permission."
+ );
+
+ // Testing browser.identities.delete().
+
+ await browser.test.assertThrows(
+ () => browser.identities.delete(identities[0].id),
+ "browser.identities.delete is not a function",
+ "It rejects for a missing permission."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ permissions: ["accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+});
+
+add_task(async function test_accounts_events() {
+ let account1 = createAccount();
+ addIdentity(account1, "id1@invalid");
+
+ let files = {
+ "background.js": async () => {
+ // Register event listener.
+ let onCreatedLog = [];
+ let onUpdatedLog = [];
+ let onDeletedLog = [];
+
+ let createListener = (id, created) => {
+ onCreatedLog.push({ id, created });
+ };
+ let updateListener = (id, changed) => {
+ onUpdatedLog.push({ id, changed });
+ };
+ let deleteListener = id => {
+ onDeletedLog.push(id);
+ };
+
+ await browser.accounts.onCreated.addListener(createListener);
+ await browser.accounts.onUpdated.addListener(updateListener);
+ await browser.accounts.onDeleted.addListener(deleteListener);
+
+ // Create accounts.
+ let imapAccountKey = await window.sendMessage("createAccount", {
+ type: "imap",
+ identity: "user@invalidImap",
+ });
+ let localAccountKey = await window.sendMessage("createAccount", {
+ type: "none",
+ identity: "user@invalidLocal",
+ });
+ let popAccountKey = await window.sendMessage("createAccount", {
+ type: "pop3",
+ identity: "user@invalidPop",
+ });
+
+ // Update account identities.
+ let accounts = await browser.accounts.list();
+ let imapAccount = accounts.find(a => a.id == imapAccountKey);
+ let localAccount = accounts.find(a => a.id == localAccountKey);
+ let popAccount = accounts.find(a => a.id == popAccountKey);
+
+ let id1 = await browser.identities.create(imapAccount.id, {
+ composeHtml: true,
+ email: "user1@inter.net",
+ name: "user1",
+ });
+ let id2 = await browser.identities.create(localAccount.id, {
+ composeHtml: false,
+ email: "user2@inter.net",
+ name: "user2",
+ });
+ let id3 = await browser.identities.create(popAccount.id, {
+ composeHtml: false,
+ email: "user3@inter.net",
+ name: "user3",
+ });
+
+ await browser.identities.setDefault(imapAccount.id, id1.id);
+ browser.test.assertEq(
+ id1.id,
+ (await browser.identities.getDefault(imapAccount.id)).id
+ );
+ await browser.identities.setDefault(localAccount.id, id2.id);
+ browser.test.assertEq(
+ id2.id,
+ (await browser.identities.getDefault(localAccount.id)).id
+ );
+ await browser.identities.setDefault(popAccount.id, id3.id);
+ browser.test.assertEq(
+ id3.id,
+ (await browser.identities.getDefault(popAccount.id)).id
+ );
+
+ // Update account names.
+ await window.sendMessage("updateAccountName", {
+ accountKey: imapAccountKey,
+ name: "Test1",
+ });
+ await window.sendMessage("updateAccountName", {
+ accountKey: localAccountKey,
+ name: "Test2",
+ });
+ await window.sendMessage("updateAccountName", {
+ accountKey: popAccountKey,
+ name: "Test3",
+ });
+
+ // Delete accounts.
+ await window.sendMessage("removeAccount", {
+ accountKey: imapAccountKey,
+ });
+ await window.sendMessage("removeAccount", {
+ accountKey: localAccountKey,
+ });
+ await window.sendMessage("removeAccount", {
+ accountKey: popAccountKey,
+ });
+
+ await browser.accounts.onCreated.removeListener(createListener);
+ await browser.accounts.onUpdated.removeListener(updateListener);
+ await browser.accounts.onDeleted.removeListener(deleteListener);
+
+ // Check event listeners.
+ browser.test.assertEq(3, onCreatedLog.length);
+ window.assertDeepEqual(
+ [
+ {
+ id: "account7",
+ created: {
+ id: "account7",
+ type: "imap",
+ identities: [],
+ name: "Mail for account7user@localhost",
+ folders: null,
+ },
+ },
+ {
+ id: "account8",
+ created: {
+ id: "account8",
+ type: "none",
+ identities: [],
+ name: "account8user on localhost",
+ folders: null,
+ },
+ },
+ {
+ id: "account9",
+ created: {
+ id: "account9",
+ type: "pop3",
+ identities: [],
+ name: "account9user on localhost",
+ folders: null,
+ },
+ },
+ ],
+ onCreatedLog,
+ "captured onCreated events are correct"
+ );
+ window.assertDeepEqual(
+ [
+ {
+ id: "account7",
+ changed: { id: "account7", name: "Mail for user@localhost" },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ defaultIdentity: { id: "id11" },
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ defaultIdentity: { id: "id12" },
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ defaultIdentity: { id: "id13" },
+ },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ defaultIdentity: { id: "id14" },
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ defaultIdentity: { id: "id15" },
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ defaultIdentity: { id: "id16" },
+ },
+ },
+ {
+ id: "account7",
+ changed: {
+ id: "account7",
+ name: "Test1",
+ },
+ },
+ {
+ id: "account8",
+ changed: {
+ id: "account8",
+ name: "Test2",
+ },
+ },
+ {
+ id: "account9",
+ changed: {
+ id: "account9",
+ name: "Test3",
+ },
+ },
+ ],
+ onUpdatedLog,
+ "captured onUpdated events are correct"
+ );
+ window.assertDeepEqual(
+ ["account7", "account8", "account9"],
+ onDeletedLog,
+ "captured onDeleted events are correct"
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => window.setTimeout(r, 250));
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ },
+ });
+
+ extension.onMessage("createAccount", details => {
+ let account = createAccount(details.type);
+ addIdentity(account, details.identity);
+ extension.sendMessage(account.key);
+ });
+ extension.onMessage("updateAccountName", details => {
+ let account = MailServices.accounts.getAccount(details.accountKey);
+ account.incomingServer.prettyName = details.name;
+ extension.sendMessage();
+ });
+ extension.onMessage("removeAccount", details => {
+ let account = MailServices.accounts.getAccount(details.accountKey);
+ cleanUpAccount(account);
+ extension.sendMessage();
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account1);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js
new file mode 100644
index 0000000000..0ac4394f40
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js
@@ -0,0 +1,220 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_task(async function test_accounts_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, the eventCounter is reset and
+ // allows to observe the order of events fired. In case of a wake-up, the
+ // first observed event is the one that woke up the background.
+ let eventCounter = 0;
+
+ for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
+ browser.accounts[eventName].addListener(async (...args) => {
+ browser.test.sendMessage(`${eventName} event received`, {
+ eventCount: ++eventCounter,
+ args,
+ });
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "accounts.onCreated",
+ "accounts.onUpdated",
+ "accounts.onDeleted",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ let testData = [
+ {
+ type: "imap",
+ identity: "user@invalidImap",
+ expectedUpdate: true,
+ expectedName: accountKey => `Mail for ${accountKey}user@localhost`,
+ expectedType: "imap",
+ updatedName: "Test1",
+ },
+ {
+ type: "pop3",
+ identity: "user@invalidPop",
+ expectedUpdate: false,
+ expectedName: accountKey => `${accountKey}user on localhost`,
+ expectedType: "pop3",
+ updatedName: "Test2",
+ },
+ {
+ type: "none",
+ identity: "user@invalidLocal",
+ expectedUpdate: false,
+ expectedName: accountKey => `${accountKey}user on localhost`,
+ expectedType: "none",
+ updatedName: "Test3",
+ },
+ {
+ type: "local",
+ identity: "user@invalidLocal",
+ expectedUpdate: false,
+ expectedName: accountKey => "Local Folders",
+ expectedType: "none",
+ updatedName: "Test4",
+ },
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+
+ // Create.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ let account = createAccount(details.type);
+ details.account = account;
+
+ {
+ let rv = await extension.awaitMessage("onCreated event received");
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [
+ details.account.key,
+ {
+ id: details.account.key,
+ name: details.expectedName(account.key),
+ type: details.expectedType,
+ folders: null,
+ identities: [],
+ },
+ ],
+ },
+ rv,
+ "The primed onCreated event should return the correct values"
+ );
+ }
+
+ if (details.expectedUpdate) {
+ let rv = await extension.awaitMessage("onUpdated event received");
+ Assert.deepEqual(
+ {
+ eventCount: 2,
+ args: [
+ details.account.key,
+ { id: details.account.key, name: "Mail for user@localhost" },
+ ],
+ },
+ rv,
+ "The non-primed onUpdated event should return the correct values"
+ );
+ }
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ // Update.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ let account = MailServices.accounts.getAccount(details.account.key);
+ account.incomingServer.prettyName = details.updatedName;
+ let rv = await extension.awaitMessage("onUpdated event received");
+
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [
+ details.account.key,
+ {
+ id: details.account.key,
+ name: details.updatedName,
+ },
+ ],
+ },
+ rv,
+ "The primed onUpdated event should return the correct values"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ // Delete.
+
+ for (let details of testData) {
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ cleanUpAccount(details.account);
+ let rv = await extension.awaitMessage("onDeleted event received");
+
+ Assert.deepEqual(
+ {
+ eventCount: 1,
+ args: [details.account.key],
+ },
+ rv,
+ "The primed onDeleted event should return the correct values"
+ );
+
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+ }
+
+ await extension.unload();
+
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
new file mode 100644
index 0000000000..8fcc3ca14f
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js
@@ -0,0 +1,2043 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm",
+ AddrBookUtils: "resource:///modules/AddrBookUtils.jsm",
+});
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_addressBooks() {
+ async function background() {
+ let firstBookId, secondBookId, newContactId;
+
+ let events = [];
+ let eventPromise;
+ let eventPromiseResolve;
+ for (let eventNamespace of ["addressBooks", "contacts", "mailingLists"]) {
+ for (let eventName of [
+ "onCreated",
+ "onUpdated",
+ "onDeleted",
+ "onMemberAdded",
+ "onMemberRemoved",
+ ]) {
+ if (eventName in browser[eventNamespace]) {
+ browser[eventNamespace][eventName].addListener((...args) => {
+ events.push({ namespace: eventNamespace, name: eventName, args });
+ if (eventPromiseResolve) {
+ let resolve = eventPromiseResolve;
+ eventPromiseResolve = null;
+ resolve();
+ }
+ });
+ }
+ }
+ }
+
+ let outsideEvent = function (action, ...args) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ return window.sendMessage("outsideEventsTest", action, ...args);
+ };
+ let checkEvents = async function (...expectedEvents) {
+ if (eventPromiseResolve) {
+ await eventPromise;
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events.length,
+ "Correct number of events"
+ );
+
+ if (expectedEvents.length != events.length) {
+ for (let event of events) {
+ let args = event.args.join(", ");
+ browser.test.log(`${event.namespace}.${event.name}(${args})`);
+ }
+ throw new Error("Wrong number of events, stopping.");
+ }
+
+ for (let [namespace, name, ...expectedArgs] of expectedEvents) {
+ let event = events.shift();
+ browser.test.assertEq(
+ namespace,
+ event.namespace,
+ "Event namespace is correct"
+ );
+ browser.test.assertEq(name, event.name, "Event type is correct");
+ browser.test.assertEq(
+ expectedArgs.length,
+ event.args.length,
+ "Argument count is correct"
+ );
+ window.assertDeepEqual(expectedArgs, event.args);
+ if (expectedEvents.length == 1) {
+ return event.args;
+ }
+ }
+
+ return null;
+ };
+
+ async function addressBookTest() {
+ browser.test.log("Starting addressBookTest");
+ let list = await browser.addressBooks.list();
+ browser.test.assertEq(2, list.length);
+ for (let b of list) {
+ browser.test.assertEq(5, Object.keys(b).length);
+ browser.test.assertEq(36, b.id.length);
+ browser.test.assertEq("addressBook", b.type);
+ browser.test.assertTrue("name" in b);
+ browser.test.assertFalse(b.readOnly);
+ browser.test.assertFalse(b.remote);
+ }
+
+ let completeList = await browser.addressBooks.list(true);
+ browser.test.assertEq(2, completeList.length);
+ for (let b of completeList) {
+ browser.test.assertEq(7, Object.keys(b).length);
+ }
+
+ firstBookId = list[0].id;
+ secondBookId = list[1].id;
+
+ let firstBook = await browser.addressBooks.get(firstBookId);
+ browser.test.assertEq(5, Object.keys(firstBook).length);
+
+ let secondBook = await browser.addressBooks.get(secondBookId, true);
+ browser.test.assertEq(7, Object.keys(secondBook).length);
+ browser.test.assertTrue(Array.isArray(secondBook.contacts));
+ browser.test.assertEq(0, secondBook.contacts.length);
+ browser.test.assertTrue(Array.isArray(secondBook.mailingLists));
+ browser.test.assertEq(0, secondBook.mailingLists.length);
+ let newBookId = await browser.addressBooks.create({ name: "test name" });
+ browser.test.assertEq(36, newBookId.length);
+ await checkEvents([
+ "addressBooks",
+ "onCreated",
+ { type: "addressBook", id: newBookId },
+ ]);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(3, list.length);
+
+ let newBook = await browser.addressBooks.get(newBookId);
+ browser.test.assertEq(newBookId, newBook.id);
+ browser.test.assertEq("addressBook", newBook.type);
+ browser.test.assertEq("test name", newBook.name);
+
+ await browser.addressBooks.update(newBookId, { name: "new name" });
+ await checkEvents([
+ "addressBooks",
+ "onUpdated",
+ { type: "addressBook", id: newBookId },
+ ]);
+ let updatedBook = await browser.addressBooks.get(newBookId);
+ browser.test.assertEq("new name", updatedBook.name);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(3, list.length);
+
+ await browser.addressBooks.delete(newBookId);
+ await checkEvents(["addressBooks", "onDeleted", newBookId]);
+
+ list = await browser.addressBooks.list();
+ browser.test.assertEq(2, list.length);
+
+ for (let operation of ["get", "update", "delete"]) {
+ let args = [newBookId];
+ if (operation == "update") {
+ args.push({ name: "" });
+ }
+
+ try {
+ await browser.addressBooks[operation].apply(
+ browser.addressBooks,
+ args
+ );
+ browser.test.fail(
+ `Calling ${operation} on a non-existent address book should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `addressBook with id=${newBookId} could not be found.`,
+ ex.message,
+ `browser.addressBooks.${operation} threw exception`
+ );
+ }
+ }
+
+ // Test the prevention of creating new address book with an empty name
+ await browser.test.assertRejects(
+ browser.addressBooks.create({ name: "" }),
+ "An unexpected error occurred",
+ "browser.addressBooks.create threw exception"
+ );
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed addressBookTest");
+ }
+
+ async function contactsTest() {
+ browser.test.log("Starting contactsTest");
+ let contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertTrue(Array.isArray(contacts));
+ browser.test.assertEq(0, contacts.length);
+
+ newContactId = await browser.contacts.create(firstBookId, {
+ FirstName: "first",
+ LastName: "last",
+ Notes: "Notes",
+ SomethingCustom: "Custom property",
+ });
+ browser.test.assertEq(36, newContactId.length);
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ ]);
+
+ contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertEq(1, contacts.length, "Contact added to first book.");
+ browser.test.assertEq(contacts[0].id, newContactId);
+
+ contacts = await browser.contacts.list(secondBookId);
+ browser.test.assertEq(
+ 0,
+ contacts.length,
+ "Contact not added to second book."
+ );
+
+ let newContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(6, Object.keys(newContact).length);
+ browser.test.assertEq(newContactId, newContact.id);
+ browser.test.assertEq(firstBookId, newContact.parentId);
+ browser.test.assertEq("contact", newContact.type);
+ browser.test.assertEq(false, newContact.readOnly);
+ browser.test.assertEq(false, newContact.remote);
+ browser.test.assertEq(5, Object.keys(newContact.properties).length);
+ browser.test.assertEq("first", newContact.properties.FirstName);
+ browser.test.assertEq("last", newContact.properties.LastName);
+ browser.test.assertEq("Notes", newContact.properties.Notes);
+ browser.test.assertEq(
+ "Custom property",
+ newContact.properties.SomethingCustom
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ newContact.properties.vCard
+ );
+
+ // Changing the UID should throw.
+ try {
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n`,
+ });
+ browser.test.fail(
+ `Updating a contact with a vCard with a differnt UID should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `The card's UID ${newContactId} may not be changed: BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n.`,
+ ex.message,
+ `browser.contacts.update threw exception`
+ );
+ }
+
+ // Test Custom1.
+ {
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nX-CUSTOM1;VALUE=TEXT:Original custom value\r\nEND:VCARD`,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ Custom1: { oldValue: null, newValue: "Original custom value" },
+ },
+ ]);
+ let updContact1 = await browser.contacts.get(newContactId);
+ browser.test.assertEq(
+ "Original custom value",
+ updContact1.properties.Custom1
+ );
+
+ await browser.contacts.update(newContactId, {
+ Custom1: "Updated custom value",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ Custom1: {
+ oldValue: "Original custom value",
+ newValue: "Updated custom value",
+ },
+ },
+ ]);
+ let updContact2 = await browser.contacts.get(newContactId);
+ browser.test.assertEq(
+ "Updated custom value",
+ updContact2.properties.Custom1
+ );
+ browser.test.assertTrue(
+ updContact2.properties.vCard.includes(
+ "X-CUSTOM1;VALUE=TEXT:Updated custom value"
+ ),
+ "vCard should include the correct x-custom1 entry"
+ );
+ }
+
+ // If a vCard and legacy properties are given, vCard must win.
+ await browser.contacts.update(newContactId, {
+ vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ FirstName: "Superman",
+ PrimaryEmail: "c.kent@dailyplanet.com",
+ PreferDisplayName: "0",
+ OtherCustom: "Yet another custom property",
+ Notes: "Ignored Notes",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ PrimaryEmail: { oldValue: null, newValue: "first@last" },
+ LastName: { oldValue: "last", newValue: null },
+ OtherCustom: {
+ oldValue: null,
+ newValue: "Yet another custom property",
+ },
+ PreferDisplayName: { oldValue: null, newValue: "0" },
+ Custom1: { oldValue: "Updated custom value", newValue: null },
+ },
+ ]);
+
+ let updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(6, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("first", updatedContact.properties.FirstName);
+ browser.test.assertEq(
+ "first@last",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertTrue(!("LastName" in updatedContact.properties));
+ browser.test.assertTrue(
+ !("Notes" in updatedContact.properties),
+ "The vCard is not specifying Notes and the specified Notes property should be ignored."
+ );
+ browser.test.assertEq(
+ "Custom property",
+ updatedContact.properties.SomethingCustom,
+ "Untouched custom properties should not be changed by updating the vCard"
+ );
+ browser.test.assertEq(
+ "Yet another custom property",
+ updatedContact.properties.OtherCustom,
+ "Custom properties should be added even while updating a vCard"
+ );
+ browser.test.assertEq(
+ "0",
+ updatedContact.properties.PreferDisplayName,
+ "Setting non-banished properties parallel to a vCard should update"
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Manually Remove properties.
+ await browser.contacts.update(newContactId, {
+ LastName: "lastname",
+ PrimaryEmail: null,
+ SecondEmail: "test@invalid.de",
+ SomethingCustom: null,
+ OtherCustom: null,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ LastName: { oldValue: null, newValue: "lastname" },
+ // It is how it is. Defining a 2nd email with no 1st, will make it the first.
+ PrimaryEmail: { oldValue: "first@last", newValue: "test@invalid.de" },
+ SomethingCustom: { oldValue: "Custom property", newValue: null },
+ OtherCustom: {
+ oldValue: "Yet another custom property",
+ newValue: null,
+ },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(5, Object.keys(updatedContact.properties).length);
+ // LastName and FirstName are stored in the same multi field property and changing LastName should not change FirstName.
+ browser.test.assertEq("first", updatedContact.properties.FirstName);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "test@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertTrue(
+ !("SomethingCustom" in updatedContact.properties)
+ );
+ browser.test.assertTrue(!("OtherCustom" in updatedContact.properties));
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;first;;;\r\nEMAIL:test@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Add an email address, going from 1 to 2.Also remove FirstName, LastName should stay.
+ await browser.contacts.update(newContactId, {
+ FirstName: null,
+ PrimaryEmail: "new1@invalid.de",
+ SecondEmail: "new2@invalid.de",
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ PrimaryEmail: {
+ oldValue: "test@invalid.de",
+ newValue: "new1@invalid.de",
+ },
+ SecondEmail: { oldValue: null, newValue: "new2@invalid.de" },
+ FirstName: { oldValue: "first", newValue: null },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(5, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "new1@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertEq(
+ "new2@invalid.de",
+ updatedContact.properties.SecondEmail
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEMAIL:new2@invalid.de\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Remove and email address, going from 2 to 1.
+ await browser.contacts.update(newContactId, {
+ SecondEmail: null,
+ });
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: firstBookId, id: newContactId },
+ {
+ SecondEmail: { oldValue: "new2@invalid.de", newValue: null },
+ },
+ ]);
+
+ updatedContact = await browser.contacts.get(newContactId);
+ browser.test.assertEq(4, Object.keys(updatedContact.properties).length);
+ browser.test.assertEq("lastname", updatedContact.properties.LastName);
+ browser.test.assertEq(
+ "new1@invalid.de",
+ updatedContact.properties.PrimaryEmail
+ );
+ browser.test.assertEq(
+ `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`,
+ updatedContact.properties.vCard
+ );
+
+ // Set a fixed UID.
+ let fixedContactId = await browser.contacts.create(
+ firstBookId,
+ "this is a test",
+ {
+ FirstName: "a",
+ LastName: "test",
+ }
+ );
+ browser.test.assertEq("this is a test", fixedContactId);
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: firstBookId, id: "this is a test" },
+ ]);
+
+ let fixedContact = await browser.contacts.get("this is a test");
+ browser.test.assertEq("this is a test", fixedContact.id);
+
+ await browser.contacts.delete("this is a test");
+ await checkEvents([
+ "contacts",
+ "onDeleted",
+ firstBookId,
+ "this is a test",
+ ]);
+
+ try {
+ await browser.contacts.create(firstBookId, newContactId, {
+ FirstName: "uh",
+ LastName: "oh",
+ });
+ browser.test.fail(`Adding a contact with a duplicate id should throw`);
+ } catch (ex) {
+ browser.test.assertEq(
+ `Duplicate contact id: ${newContactId}`,
+ ex.message,
+ `browser.contacts.create threw exception`
+ );
+ }
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed contactsTest");
+ }
+
+ async function mailingListsTest() {
+ browser.test.log("Starting mailingListsTest");
+ let mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertTrue(Array.isArray(mailingLists));
+ browser.test.assertEq(0, mailingLists.length);
+
+ let newMailingListId = await browser.mailingLists.create(firstBookId, {
+ name: "name",
+ });
+ browser.test.assertEq(36, newMailingListId.length);
+ await checkEvents([
+ "mailingLists",
+ "onCreated",
+ { type: "mailingList", parentId: firstBookId, id: newMailingListId },
+ ]);
+
+ mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertEq(
+ 1,
+ mailingLists.length,
+ "List added to first book."
+ );
+
+ mailingLists = await browser.mailingLists.list(secondBookId);
+ browser.test.assertEq(
+ 0,
+ mailingLists.length,
+ "List not added to second book."
+ );
+
+ let newAddressList = await browser.mailingLists.get(newMailingListId);
+ browser.test.assertEq(8, Object.keys(newAddressList).length);
+ browser.test.assertEq(newMailingListId, newAddressList.id);
+ browser.test.assertEq(firstBookId, newAddressList.parentId);
+ browser.test.assertEq("mailingList", newAddressList.type);
+ browser.test.assertEq("name", newAddressList.name);
+ browser.test.assertEq("", newAddressList.nickName);
+ browser.test.assertEq("", newAddressList.description);
+ browser.test.assertEq(false, newAddressList.readOnly);
+ browser.test.assertEq(false, newAddressList.remote);
+
+ // Test that a valid name is ensured for an existing mail list
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "Two spaces invalid name",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(newMailingListId, {
+ name: "><<<",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.mailingLists.update(newMailingListId, {
+ name: "name!",
+ nickName: "nickname!",
+ description: "description!",
+ });
+ await checkEvents([
+ "mailingLists",
+ "onUpdated",
+ { type: "mailingList", parentId: firstBookId, id: newMailingListId },
+ ]);
+
+ let updatedMailingList = await browser.mailingLists.get(newMailingListId);
+ browser.test.assertEq("name!", updatedMailingList.name);
+ browser.test.assertEq("nickname!", updatedMailingList.nickName);
+ browser.test.assertEq("description!", updatedMailingList.description);
+
+ await browser.mailingLists.addMember(newMailingListId, newContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: newMailingListId, id: newContactId },
+ ]);
+
+ let listMembers = await browser.mailingLists.listMembers(
+ newMailingListId
+ );
+ browser.test.assertTrue(Array.isArray(listMembers));
+ browser.test.assertEq(1, listMembers.length);
+
+ let anotherContactId = await browser.contacts.create(firstBookId, {
+ FirstName: "second",
+ LastName: "last",
+ PrimaryEmail: "em@il",
+ });
+ await checkEvents([
+ "contacts",
+ "onCreated",
+ {
+ type: "contact",
+ parentId: firstBookId,
+ id: anotherContactId,
+ readOnly: false,
+ },
+ ]);
+
+ await browser.mailingLists.addMember(newMailingListId, anotherContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: newMailingListId, id: anotherContactId },
+ ]);
+
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(2, listMembers.length);
+
+ await browser.contacts.delete(anotherContactId);
+ await checkEvents(
+ ["contacts", "onDeleted", firstBookId, anotherContactId],
+ ["mailingLists", "onMemberRemoved", newMailingListId, anotherContactId]
+ );
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(1, listMembers.length);
+
+ await browser.mailingLists.removeMember(newMailingListId, newContactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberRemoved",
+ newMailingListId,
+ newContactId,
+ ]);
+ listMembers = await browser.mailingLists.listMembers(newMailingListId);
+ browser.test.assertEq(0, listMembers.length);
+
+ await browser.mailingLists.delete(newMailingListId);
+ await checkEvents([
+ "mailingLists",
+ "onDeleted",
+ firstBookId,
+ newMailingListId,
+ ]);
+
+ mailingLists = await browser.mailingLists.list(firstBookId);
+ browser.test.assertEq(0, mailingLists.length);
+
+ for (let operation of [
+ "get",
+ "update",
+ "delete",
+ "listMembers",
+ "addMember",
+ "removeMember",
+ ]) {
+ let args = [newMailingListId];
+ switch (operation) {
+ case "update":
+ args.push({ name: "" });
+ break;
+ case "addMember":
+ case "removeMember":
+ args.push(newContactId);
+ break;
+ }
+
+ try {
+ await browser.mailingLists[operation].apply(
+ browser.mailingLists,
+ args
+ );
+ browser.test.fail(
+ `Calling ${operation} on a non-existent mailing list should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `mailingList with id=${newMailingListId} could not be found.`,
+ ex.message,
+ `browser.mailingLists.${operation} threw exception`
+ );
+ }
+ }
+
+ // Test that a valid name is ensured for a new mail list
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "Two spaces invalid name",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(firstBookId, {
+ name: "><<<",
+ }),
+ "An unexpected error occurred",
+ "browser.mailingLists.update threw exception"
+ );
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed mailingListsTest");
+ }
+
+ async function contactRemovalTest() {
+ browser.test.log("Starting contactRemovalTest");
+ await browser.contacts.delete(newContactId);
+ await checkEvents(["contacts", "onDeleted", firstBookId, newContactId]);
+
+ for (let operation of ["get", "update", "delete"]) {
+ let args = [newContactId];
+ if (operation == "update") {
+ args.push({});
+ }
+
+ try {
+ await browser.contacts[operation].apply(browser.contacts, args);
+ browser.test.fail(
+ `Calling ${operation} on a non-existent contact should throw`
+ );
+ } catch (ex) {
+ browser.test.assertEq(
+ `contact with id=${newContactId} could not be found.`,
+ ex.message,
+ `browser.contacts.${operation} threw exception`
+ );
+ }
+ }
+
+ let contacts = await browser.contacts.list(firstBookId);
+ browser.test.assertEq(0, contacts.length);
+
+ browser.test.assertEq(0, events.length, "No events left unconsumed");
+ browser.test.log("Completed contactRemovalTest");
+ }
+
+ async function outsideEventsTest() {
+ browser.test.log("Starting outsideEventsTest");
+ let [bookId, newBookPrefId] = await outsideEvent("createAddressBook");
+ let [newBook] = await checkEvents([
+ "addressBooks",
+ "onCreated",
+ { type: "addressBook", id: bookId },
+ ]);
+ browser.test.assertEq("external add", newBook.name);
+
+ await outsideEvent("updateAddressBook", newBookPrefId);
+ let [updatedBook] = await checkEvents([
+ "addressBooks",
+ "onUpdated",
+ { type: "addressBook", id: bookId },
+ ]);
+ browser.test.assertEq("external edit", updatedBook.name);
+
+ await outsideEvent("deleteAddressBook", newBookPrefId);
+ await checkEvents(["addressBooks", "onDeleted", bookId]);
+
+ let [parentId1, contactId] = await outsideEvent("createContact");
+ let [newContact] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId1, id: contactId },
+ ]);
+ browser.test.assertEq("external", newContact.properties.FirstName);
+ browser.test.assertEq("add", newContact.properties.LastName);
+ browser.test.assertTrue(
+ newContact.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+
+ // Update the contact from outside.
+ await outsideEvent("updateContact", contactId);
+ let [updatedContact] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId1, id: contactId },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact.properties.LastName);
+
+ let [parentId2, listId] = await outsideEvent("createMailingList");
+ let [newList] = await checkEvents([
+ "mailingLists",
+ "onCreated",
+ { type: "mailingList", parentId: parentId2, id: listId },
+ ]);
+ browser.test.assertEq("external add", newList.name);
+
+ await outsideEvent("updateMailingList", listId);
+ let [updatedList] = await checkEvents([
+ "mailingLists",
+ "onUpdated",
+ { type: "mailingList", parentId: parentId2, id: listId },
+ ]);
+ browser.test.assertEq("external edit", updatedList.name);
+
+ await outsideEvent("addMailingListMember", listId, contactId);
+ await checkEvents([
+ "mailingLists",
+ "onMemberAdded",
+ { type: "contact", parentId: listId, id: contactId },
+ ]);
+ let listMembers = await browser.mailingLists.listMembers(listId);
+ browser.test.assertEq(1, listMembers.length);
+
+ await outsideEvent("removeMailingListMember", listId, contactId);
+ await checkEvents(["mailingLists", "onMemberRemoved", listId, contactId]);
+
+ await outsideEvent("deleteMailingList", listId);
+ await checkEvents(["mailingLists", "onDeleted", parentId2, listId]);
+
+ await outsideEvent("deleteContact", contactId);
+ await checkEvents(["contacts", "onDeleted", parentId1, contactId]);
+
+ browser.test.log("Completed outsideEventsTest");
+ }
+
+ await addressBookTest();
+ await contactsTest();
+ await mailingListsTest();
+ await contactRemovalTest();
+ await outsideEventsTest();
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+ function findMailingList(id) {
+ for (let list of parent.childNodes) {
+ if (list.UID == id) {
+ return list;
+ }
+ }
+ return null;
+ }
+
+ extension.onMessage("outsideEventsTest", async (action, ...args) => {
+ switch (action) {
+ case "createAddressBook": {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "external add",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ extension.sendMessage(book.UID, dirPrefId);
+ return;
+ }
+ case "updateAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ book.dirName = "external edit";
+ extension.sendMessage();
+ return;
+ }
+ case "deleteAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ MailServices.ab.deleteAddressBook(book.URI);
+ extension.sendMessage();
+ return;
+ }
+ case "createContact": {
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID);
+ return;
+ }
+ case "updateContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.firstName = "external";
+ contact.lastName = "edit";
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "createMailingList": {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ list.dirName = "external add";
+
+ let newList = parent.addMailList(list);
+ extension.sendMessage(parent.UID, newList.UID);
+ return;
+ }
+ case "updateMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ list.dirName = "external edit";
+ list.editMailListToDatabase(null);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ parent.deleteDirectory(list);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "addMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.addCard(contact);
+ equal(1, list.childCards.length);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "removeMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.deleteCards([contact]);
+ equal(0, list.childCards.length);
+ ok(findContact(args[1]), "Contact was not removed");
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
+
+add_task(async function test_addressBooks_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ // Create and register event listener.
+ for (let event of [
+ "addressBooks.onCreated",
+ "addressBooks.onUpdated",
+ "addressBooks.onDeleted",
+ "contacts.onCreated",
+ "contacts.onUpdated",
+ "contacts.onDeleted",
+ "mailingLists.onCreated",
+ "mailingLists.onUpdated",
+ "mailingLists.onDeleted",
+ "mailingLists.onMemberAdded",
+ "mailingLists.onMemberRemoved",
+ ]) {
+ let [apiName, eventName] = event.split(".");
+ browser[apiName][eventName].addListener((...args) => {
+ // Only send the first event after background wake-up, this should be
+ // the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${apiName}.${eventName} received`, args);
+ }
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ browser_specific_settings: { gecko: { id: "addressbook@xpcshell.test" } },
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+ function findMailingList(id) {
+ for (let list of parent.childNodes) {
+ if (list.UID == id) {
+ return list;
+ }
+ }
+ return null;
+ }
+ function outsideEvent(action, ...args) {
+ switch (action) {
+ case "createAddressBook": {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "external add",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ return [book, dirPrefId];
+ }
+ case "updateAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ book.dirName = "external edit";
+ return [];
+ }
+ case "deleteAddressBook": {
+ let book = MailServices.ab.getDirectoryFromId(args[0]);
+ MailServices.ab.deleteAddressBook(book.URI);
+ return [];
+ }
+ case "createContact": {
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+
+ let newContact = parent.addCard(contact);
+ return [parent.UID, newContact.UID];
+ }
+ case "updateContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.firstName = "external";
+ contact.lastName = "edit";
+ parent.modifyCard(contact);
+ return [];
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ return [];
+ }
+ break;
+ }
+ case "createMailingList": {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ list.dirName = "external add";
+
+ let newList = parent.addMailList(list);
+ return [parent.UID, newList.UID];
+ }
+ case "updateMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ list.dirName = "external edit";
+ list.editMailListToDatabase(null);
+ return [];
+ }
+ break;
+ }
+ case "deleteMailingList": {
+ let list = findMailingList(args[0]);
+ if (list) {
+ parent.deleteDirectory(list);
+ return [];
+ }
+ break;
+ }
+ case "addMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.addCard(contact);
+ equal(1, list.childCards.length);
+ return [];
+ }
+ break;
+ }
+ case "removeMailingListMember": {
+ let list = findMailingList(args[0]);
+ let contact = findContact(args[1]);
+
+ if (list && contact) {
+ list.deleteCards([contact]);
+ equal(0, list.childCards.length);
+ ok(findContact(args[1]), "Contact was not removed");
+ return [];
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ }
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "addressBook.onAddressBookCreated",
+ "addressBook.onAddressBookUpdated",
+ "addressBook.onAddressBookDeleted",
+ "addressBook.onContactCreated",
+ "addressBook.onContactUpdated",
+ "addressBook.onContactDeleted",
+ "addressBook.onMailingListCreated",
+ "addressBook.onMailingListUpdated",
+ "addressBook.onMailingListDeleted",
+ "addressBook.onMemberAdded",
+ "addressBook.onMemberRemoved",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [newBook, dirPrefId] = outsideEvent("createAddressBook");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ id: newBook.UID,
+ type: "addressBook",
+ name: "external add",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("addressBooks.onCreated received"),
+ "The primed addressBooks.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateAddressBook", dirPrefId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ id: newBook.UID,
+ type: "addressBook",
+ name: "external edit",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("addressBooks.onUpdated received"),
+ "The primed addressBooks.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // addressBooks.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteAddressBook", dirPrefId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [newBook.UID],
+ await extension.awaitMessage("addressBooks.onDeleted received"),
+ "The primed addressBooks.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [parentId1, contactId] = outsideEvent("createContact");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [createdNode] = await extension.awaitMessage(
+ "contacts.onCreated received"
+ );
+ Assert.deepEqual(
+ {
+ type: "contact",
+ parentId: parentId1,
+ id: contactId,
+ },
+ {
+ type: createdNode.type,
+ parentId: createdNode.parentId,
+ id: createdNode.id,
+ },
+ "The primed contacts.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateContact", contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [updatedNode, changedProperties] = await extension.awaitMessage(
+ "contacts.onUpdated received"
+ );
+ Assert.deepEqual(
+ [
+ { type: "contact", parentId: parentId1, id: contactId },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ],
+ [
+ {
+ type: updatedNode.type,
+ parentId: updatedNode.parentId,
+ id: updatedNode.id,
+ },
+ changedProperties,
+ ],
+ "The primed contacts.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingLists.onCreated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ let [parentId2, listId] = outsideEvent("createMailingList");
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ type: "mailingList",
+ parentId: parentId2,
+ id: listId,
+ name: "external add",
+ nickName: "",
+ description: "",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("mailingLists.onCreated received"),
+ "The primed mailingLists.onCreated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onUpdated.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("updateMailingList", listId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [
+ {
+ type: "mailingList",
+ parentId: parentId2,
+ id: listId,
+ name: "external edit",
+ nickName: "",
+ description: "",
+ readOnly: false,
+ remote: false,
+ },
+ ],
+ await extension.awaitMessage("mailingLists.onUpdated received"),
+ "The primed mailingLists.onUpdated event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onMemberAdded.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("addMailingListMember", listId, contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ let [addedNode] = await extension.awaitMessage(
+ "mailingLists.onMemberAdded received"
+ );
+ Assert.deepEqual(
+ { type: "contact", parentId: listId, id: contactId },
+ { type: addedNode.type, parentId: addedNode.parentId, id: addedNode.id },
+ "The primed mailingLists.onMemberAdded event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onMemberRemoved.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("removeMailingListMember", listId, contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [listId, contactId],
+ await extension.awaitMessage("mailingLists.onMemberRemoved received"),
+ "The primed mailingLists.onMemberRemoved event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // mailingList.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteMailingList", listId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [parentId2, listId],
+ await extension.awaitMessage("mailingLists.onDeleted received"),
+ "The primed mailingLists.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ // contacts.onDeleted.
+
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ checkPersistentListeners({ primed: true });
+ outsideEvent("deleteContact", contactId);
+ // The event should have restarted the background.
+ await extension.awaitMessage("background started");
+ Assert.deepEqual(
+ [parentId1, contactId],
+ await extension.awaitMessage("contacts.onDeleted received"),
+ "The primed contacts.onDeleted event should return the correct values"
+ );
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+
+ await AddonTestUtils.promiseShutdownManager();
+});
+
+add_task(async function test_photos() {
+ async function background() {
+ let events = [];
+ let eventPromise;
+ let eventPromiseResolve;
+ for (let eventNamespace of ["addressBooks", "contacts"]) {
+ for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
+ if (eventName in browser[eventNamespace]) {
+ browser[eventNamespace][eventName].addListener((...args) => {
+ events.push({ namespace: eventNamespace, name: eventName, args });
+ if (eventPromiseResolve) {
+ let resolve = eventPromiseResolve;
+ eventPromiseResolve = null;
+ resolve();
+ }
+ });
+ }
+ }
+ }
+
+ let getDataUrl = function (file) {
+ return new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = function () {
+ resolve(reader.result);
+ };
+ reader.onerror = function (error) {
+ reject(new Error(error));
+ };
+ });
+ };
+
+ let updateAndVerifyPhoto = async function (
+ parentId,
+ id,
+ photoFile,
+ photoData
+ ) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ await browser.contacts.setPhoto(id, photoFile);
+
+ await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId, id },
+ {},
+ ]);
+ let updatedPhoto = await browser.contacts.getPhoto(id);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto.type);
+ browser.test.assertEq(`${id}.png`, updatedPhoto.name);
+ browser.test.assertEq(photoData, await getDataUrl(updatedPhoto));
+ };
+ let normalizeVCard = function (vCard) {
+ return vCard
+ .replaceAll("\r\n", "")
+ .replaceAll("\n", "")
+ .replaceAll(" ", "");
+ };
+ let outsideEvent = function (action, ...args) {
+ eventPromise = new Promise(resolve => {
+ eventPromiseResolve = resolve;
+ });
+ return window.sendMessage("outsideEventsTest", action, ...args);
+ };
+ let checkEvents = async function (...expectedEvents) {
+ if (eventPromiseResolve) {
+ await eventPromise;
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events.length,
+ "Correct number of events"
+ );
+
+ if (expectedEvents.length != events.length) {
+ for (let event of events) {
+ let args = event.args.join(", ");
+ browser.test.log(`${event.namespace}.${event.name}(${args})`);
+ }
+ throw new Error("Wrong number of events, stopping.");
+ }
+
+ for (let [namespace, name, ...expectedArgs] of expectedEvents) {
+ let event = events.shift();
+ browser.test.assertEq(
+ namespace,
+ event.namespace,
+ "Event namespace is correct"
+ );
+ browser.test.assertEq(name, event.name, "Event type is correct");
+ browser.test.assertEq(
+ expectedArgs.length,
+ event.args.length,
+ "Argument count is correct"
+ );
+ window.assertDeepEqual(expectedArgs, event.args);
+ if (expectedEvents.length == 1) {
+ return event.args;
+ }
+ }
+
+ return null;
+ };
+
+ let whitePixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let bluePixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==";
+ let greenPixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY5CeYAMAAbEA6ASxSWcAAAAASUVORK5CYII=";
+ let redPixelData =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY3growIAAycBLhVrvukAAAAASUVORK5CYII=";
+ let vCard3WhitePixel =
+ "PHOTO;ENCODING=B;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let vCard4WhitePixel =
+ "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC";
+ let vCard4BluePixel =
+ "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==";
+
+ // Create a photo file, which is linked to a local file to simulate a file
+ // opened through a filepicker.
+ let [redPixelRealFile] = await window.sendMessage("getRedPixelFile");
+
+ // Create a photo file, which is a simple data blob.
+ let greenPixelFile = await fetch(greenPixelData)
+ .then(res => res.arrayBuffer())
+ .then(buf => new File([buf], "greenPixel.png", { type: "image/png" }));
+
+ // -------------------------------------------------------------------------
+ // Test vCard v4 with a photoName set.
+ // -------------------------------------------------------------------------
+
+ let [parentId1, contactId1, photoName1] = await outsideEvent(
+ "createV4ContactWithPhotoName"
+ );
+ let [newContact] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId1, id: contactId1 },
+ ]);
+ browser.test.assertEq("external", newContact.properties.FirstName);
+ browser.test.assertEq("add", newContact.properties.LastName);
+ browser.test.assertTrue(
+ newContact.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+ browser.test.assertTrue(
+ normalizeVCard(newContact.properties.vCard).includes(vCard4WhitePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact.properties.vCard
+ )}] vs [${vCard4WhitePixel}]`
+ );
+ // Check internal photoUrl is the correct fileUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${photoName1}$`
+ );
+
+ // Test if we can get the photo through the API.
+
+ let photo = await browser.contacts.getPhoto(contactId1);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo instanceof File);
+ browser.test.assertEq("image/png", photo.type);
+ browser.test.assertEq(`${contactId1}.png`, photo.name);
+ browser.test.assertEq(
+ whitePixelData,
+ await getDataUrl(photo),
+ "vCard 4.0 contact with photo from internal fileUrl from photoName should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${photoName1}$`
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had only a photoName set and
+ // its photo stored as a local file, the updated photo should also be stored
+ // as a local file.
+
+ await updateAndVerifyPhoto(
+ parentId1,
+ contactId1,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${contactId1}\.png$`
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId1,
+ contactId1,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ `^file:.*?${contactId1}-1\.png$`
+ );
+
+ // Test if we get the correct photo if it is updated by the user, storing the
+ // photo in its vCard (outside of the API).
+
+ await outsideEvent("updateV4ContactWithBluePixel", contactId1);
+ let [updatedContact1] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId1, id: contactId1 },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact1.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact1.properties.LastName);
+ let updatedPhoto1 = await browser.contacts.getPhoto(contactId1);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto1 instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto1.type);
+ browser.test.assertEq(`${contactId1}.png`, updatedPhoto1.name);
+ browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto1));
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId1,
+ bluePixelData
+ );
+
+ // -------------------------------------------------------------------------
+ // Test vCard v4 with a photoName and also a photo in its vCard.
+ // -------------------------------------------------------------------------
+
+ let [parentId2, contactId2] = await outsideEvent(
+ "createV4ContactWithBothPhotoProps"
+ );
+ let [newContact2] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId2, id: contactId2 },
+ ]);
+ browser.test.assertEq("external", newContact2.properties.FirstName);
+ browser.test.assertEq("add", newContact2.properties.LastName);
+ browser.test.assertTrue(
+ newContact2.properties.vCard.includes("VERSION:4.0"),
+ "vCard should be version 4.0"
+ );
+ // The card should not include vCard4WhitePixel (which photoName points to),
+ // but the value of vCard4BluePixel stored in the vCard photo property.
+ browser.test.assertTrue(
+ normalizeVCard(newContact2.properties.vCard).includes(vCard4BluePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact2.properties.vCard
+ )}] vs [${vCard4BluePixel}]`
+ );
+ // Check internal photoUrl is the correct dataUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ bluePixelData
+ );
+
+ // Test if we can get the correct photo through the API.
+
+ let photo3 = await browser.contacts.getPhoto(contactId2);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo3 instanceof File);
+ browser.test.assertEq("image/png", photo3.type);
+ browser.test.assertEq(`${contactId2}.png`, photo3.name);
+ browser.test.assertEq(
+ bluePixelData,
+ await getDataUrl(photo3),
+ "vCard 4.0 contact with photo from internal dataUrl from vCard (vCard wins over photoName) should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ bluePixelData
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had its photo stored as dataUrl
+ // in the vCard, the updated photo should be stored as a dataUrl as well.
+
+ await updateAndVerifyPhoto(
+ parentId2,
+ contactId2,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ redPixelData
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId2,
+ contactId2,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId2,
+ greenPixelData
+ );
+
+ // -------------------------------------------------------------------------
+ // Test vCard v3 with a photoName set.
+ // -------------------------------------------------------------------------
+
+ let [parentId3, contactId3, photoName4] = await outsideEvent(
+ "createV3ContactWithPhotoName"
+ );
+ let [newContact4] = await checkEvents([
+ "contacts",
+ "onCreated",
+ { type: "contact", parentId: parentId3, id: contactId3 },
+ ]);
+ browser.test.assertEq("external", newContact4.properties.FirstName);
+ browser.test.assertEq("add", newContact4.properties.LastName);
+ browser.test.assertTrue(
+ newContact4.properties.vCard.includes("VERSION:3.0"),
+ "vCard should be version 3.0"
+ );
+ browser.test.assertTrue(
+ normalizeVCard(newContact4.properties.vCard).includes(vCard3WhitePixel),
+ `vCard should include the correct Photo property [${normalizeVCard(
+ newContact4.properties.vCard
+ )}] vs [${vCard3WhitePixel}]`
+ );
+ // Check internal photoUrl is the correct fileUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${photoName4}$`
+ );
+ let photo4 = await browser.contacts.getPhoto(contactId3);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(photo4 instanceof File);
+ browser.test.assertEq("image/png", photo4.type);
+ browser.test.assertEq(`${contactId3}.png`, photo4.name);
+ browser.test.assertEq(
+ whitePixelData,
+ await getDataUrl(photo4),
+ "vCard 3.0 contact with photo from internal fileUrl from photoName should return the correct photo file"
+ );
+ // Re-check internal photoUrl.
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${photoName4}$`
+ );
+
+ // Test if we can update the photo through the API by providing a file which
+ // is linked to a local file. Since this vCard had only a photoName set and
+ // its photo stored as a local file, the updated photo should also be stored
+ // as a local file.
+
+ await updateAndVerifyPhoto(
+ parentId3,
+ contactId3,
+ redPixelRealFile,
+ redPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${contactId3}\.png$`
+ );
+
+ // Test if we can update the photo through the API, by providing a pure data
+ // blob (decoupled from a local file, without file.mozFullPath set).
+
+ await updateAndVerifyPhoto(
+ parentId3,
+ contactId3,
+ greenPixelFile,
+ greenPixelData
+ );
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ `^file:.*?${contactId3}-1\.png$`
+ );
+
+ // Test if we get the correct photo if it is updated by the user, storing the
+ // photo in its vCard (outside of the API).
+
+ await outsideEvent("updateV3ContactWithBluePixel", contactId3);
+ let [updatedContact3] = await checkEvents([
+ "contacts",
+ "onUpdated",
+ { type: "contact", parentId: parentId3, id: contactId3 },
+ { LastName: { oldValue: "add", newValue: "edit" } },
+ ]);
+ browser.test.assertEq("external", updatedContact3.properties.FirstName);
+ browser.test.assertEq("edit", updatedContact3.properties.LastName);
+ let updatedPhoto3 = await browser.contacts.getPhoto(contactId3);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(updatedPhoto3 instanceof File);
+ browser.test.assertEq("image/png", updatedPhoto3.type);
+ browser.test.assertEq(`${contactId3}.png`, updatedPhoto3.name);
+ browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto3));
+ await window.sendMessage(
+ "verifyInternalPhotoUrl",
+ contactId3,
+ bluePixelData
+ );
+
+ // Cleanup. Delete all created contacts.
+
+ await outsideEvent("deleteContact", contactId1);
+ await checkEvents(["contacts", "onDeleted", parentId1, contactId1]);
+ await outsideEvent("deleteContact", contactId2);
+ await checkEvents(["contacts", "onDeleted", parentId2, contactId2]);
+ await outsideEvent("deleteContact", contactId3);
+ await checkEvents(["contacts", "onDeleted", parentId3, contactId3]);
+ browser.test.notifyPass("addressBooksPhotos");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ function findContact(id) {
+ for (let child of parent.childCards) {
+ if (child.UID == id) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ async function getUniqueWhitePixelFile() {
+ // Copy photo file into the required Photos subfolder of the profile folder.
+ let photoName = `${AddrBookUtils.newUID()}.png`;
+ await IOUtils.copy(
+ do_get_file("images/whitePixel.png").path,
+ PathUtils.join(PathUtils.profileDir, "Photos", photoName)
+ );
+ return photoName;
+ }
+
+ extension.onMessage("getRedPixelFile", async () => {
+ let redPixelFile = await File.createFromNsIFile(
+ do_get_file("images/redPixel.png")
+ );
+ extension.sendMessage(redPixelFile);
+ });
+
+ extension.onMessage("verifyInternalPhotoUrl", (id, expected) => {
+ let contact = findContact(id);
+ let photoUrl = contact.photoURL;
+ if (expected.startsWith("data:")) {
+ Assert.equal(expected, photoUrl, `photoURL should be correct`);
+ } else {
+ let regExp = new RegExp(expected);
+ Assert.ok(
+ regExp.test(photoUrl),
+ `photoURL <${photoUrl}> should match expected regExp <${expected}>`
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("outsideEventsTest", async (action, ...args) => {
+ switch (action) {
+ case "createV4ContactWithPhotoName": {
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.firstName = "external";
+ contact.lastName = "add";
+ contact.primaryEmail = "test@invalid";
+ contact.setProperty("PhotoName", photoName);
+
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "createV4ContactWithBothPhotoProps": {
+ // This contact has whitePixel as file but bluePixel in the vCard.
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.setProperty("PhotoName", photoName);
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:test@invalid
+ N:add;external;;;
+ UID:fd9aecf9-2453-4ba1-bec6-574a15bb380b
+ PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA
+ ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "updateV4ContactWithBluePixel": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:test@invalid
+ N:edit;external;;;
+ PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA
+ ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "createV3ContactWithPhotoName": {
+ let photoName = await getUniqueWhitePixelFile();
+ let contact = new AddrBookCard();
+ contact.setProperty("PhotoName", photoName);
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ EMAIL:test@invalid
+ N:add;external
+ UID:fd9aecf9-2453-4ba1-bec6-574a15bb380c
+ END:VCARD
+ `
+ );
+ let newContact = parent.addCard(contact);
+ extension.sendMessage(parent.UID, newContact.UID, photoName);
+ return;
+ }
+ case "updateV3ContactWithBluePixel": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ contact.setProperty(
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ EMAIL:test@invalid
+ N:edit;external
+ PHOTO;ENCODING=b;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD
+ ElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg==
+ END:VCARD
+ `
+ );
+ parent.modifyCard(contact);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ case "deleteContact": {
+ let contact = findContact(args[0]);
+ if (contact) {
+ parent.deleteCards([contact]);
+ extension.sendMessage();
+ return;
+ }
+ break;
+ }
+ }
+ throw new Error(
+ `Message "${action}" passed to handler didn't do anything.`
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooksPhotos");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js
new file mode 100644
index 0000000000..a09540dcbe
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js
@@ -0,0 +1,139 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let id = "9b9074ff-8fa4-4c58-9c3b-bc9ea2e17db1";
+ let dummy = async (node, searchString, query) => {
+ await browser.test.assertTrue(
+ false,
+ "Should have removed this address book"
+ );
+ };
+ await browser.addressBooks.provider.onSearchRequest.addListener(dummy, {
+ addressBookName: "dummy",
+ isSecure: false,
+ id,
+ });
+ await browser.addressBooks.provider.onSearchRequest.removeListener(dummy);
+ id = "00e1d9af-a846-4ef5-a6ac-15e8926bf6d3";
+ await browser.addressBooks.provider.onSearchRequest.addListener(
+ async (node, searchString, query) => {
+ await browser.test.assertEq(
+ id,
+ node.id,
+ "Addressbook should have the id we requested"
+ );
+ return {
+ results: [
+ {
+ DisplayName: searchString,
+ PrimaryEmail: searchString + "@example.com",
+ },
+ ],
+ isCompleteResult: true,
+ };
+ },
+ {
+ addressBookName: "xpcshell",
+ isSecure: false,
+ id,
+ }
+ );
+ await browser.addressBooks.provider.onSearchRequest.addListener(
+ async (node, searchString, query) => {
+ await browser.test.assertTrue(
+ false,
+ "Should not have created a duplicate address book"
+ );
+ },
+ {
+ addressBookName: "xpcshell",
+ isSecure: false,
+ id,
+ }
+ );
+ },
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ await extension.startup();
+
+ const dummyUID = "9b9074ff-8fa4-4c58-9c3b-bc9ea2e17db1";
+ let searchBook = MailServices.ab.getDirectoryFromUID(dummyUID);
+ ok(searchBook == null, "Dummy directory was removed by extension");
+
+ const UID = "00e1d9af-a846-4ef5-a6ac-15e8926bf6d3";
+ searchBook = MailServices.ab.getDirectoryFromUID(UID);
+ ok(searchBook != null, "Extension registered an async directory");
+
+ let foundCards = 0;
+ await new Promise(resolve => {
+ searchBook.search(null, "test", {
+ onSearchFoundCard(card) {
+ ok(card != null, "A card was found.");
+ equal(card.directoryUID, UID, "The card comes from the directory.");
+ equal(
+ card.primaryEmail,
+ "test@example.com",
+ "The card has the correct email address."
+ );
+ equal(
+ card.displayName,
+ "test",
+ "The card has the correct display name."
+ );
+ foundCards++;
+ },
+ onSearchFinished(status, isCompleteResult) {
+ ok(Components.isSuccessCode(status), "Search finished successfully.");
+ equal(foundCards, 1, "One card was found.");
+ ok(isCompleteResult, "A full result set was received.");
+ resolve();
+ },
+ });
+ });
+
+ let autoCompleteSearch = Cc[
+ "@mozilla.org/autocomplete/search;1?name=addrbook"
+ ].createInstance(Ci.nsIAutoCompleteSearch);
+ await new Promise(resolve => {
+ autoCompleteSearch.startSearch("test", null, null, {
+ onSearchResult(aSearch, aResult) {
+ equal(aSearch, autoCompleteSearch, "This is our search.");
+ if (aResult.searchResult == Ci.nsIAutoCompleteResult.RESULT_SUCCESS) {
+ equal(aResult.matchCount, 1, "One match was found.");
+ equal(
+ aResult.getValueAt(0),
+ "test <test@example.com>",
+ "The match had the expected value."
+ );
+ resolve();
+ } else {
+ equal(
+ aResult.searchResult,
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING,
+ "We should be waiting for the extension's results."
+ );
+ }
+ },
+ });
+ });
+
+ await extension.unload();
+ searchBook = MailServices.ab.getDirectoryFromUID(UID);
+ ok(searchBook == null, "Extension directory removed after unload");
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js
new file mode 100644
index 0000000000..9a6bbd8f4e
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js
@@ -0,0 +1,238 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ LDAPServer.close();
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_quickSearch() {
+ async function background() {
+ let book1 = await browser.addressBooks.create({ name: "book1" });
+ let book2 = await browser.addressBooks.create({ name: "book2" });
+
+ let book1contacts = {
+ charlie: await browser.contacts.create(book1, { FirstName: "charlie" }),
+ juliet: await browser.contacts.create(book1, { FirstName: "juliet" }),
+ mike: await browser.contacts.create(book1, { FirstName: "mike" }),
+ oscar: await browser.contacts.create(book1, { FirstName: "oscar" }),
+ papa: await browser.contacts.create(book1, { FirstName: "papa" }),
+ romeo: await browser.contacts.create(book1, { FirstName: "romeo" }),
+ victor: await browser.contacts.create(book1, { FirstName: "victor" }),
+ };
+
+ let book2contacts = {
+ bigBird: await browser.contacts.create(book2, {
+ FirstName: "Big",
+ LastName: "Bird",
+ }),
+ cookieMonster: await browser.contacts.create(book2, {
+ FirstName: "Cookie",
+ LastName: "Monster",
+ }),
+ elmo: await browser.contacts.create(book2, { FirstName: "Elmo" }),
+ grover: await browser.contacts.create(book2, { FirstName: "Grover" }),
+ oscarTheGrouch: await browser.contacts.create(book2, {
+ FirstName: "Oscar",
+ LastName: "The Grouch",
+ }),
+ };
+
+ // A search string without a match in either book.
+ let results = await browser.contacts.quickSearch(book1, "snuffleupagus");
+ browser.test.assertEq(0, results.length);
+
+ // A search string with a match in the book we're searching.
+ results = await browser.contacts.quickSearch(book1, "mike");
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.mike, results[0].id);
+
+ // A search string passed via queryInfo
+ results = await browser.contacts.quickSearch(book1, {
+ searchString: "mike",
+ });
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.mike, results[0].id);
+
+ // A search string with a match in the book we're not searching.
+ results = await browser.contacts.quickSearch(book1, "elmo");
+ browser.test.assertEq(0, results.length);
+
+ // A search string with a match in both books.
+ results = await browser.contacts.quickSearch(book1, "oscar");
+ browser.test.assertEq(1, results.length);
+ browser.test.assertEq(book1contacts.oscar, results[0].id);
+
+ // A search string with a match in both books. Looking in all books.
+ results = await browser.contacts.quickSearch("oscar");
+ browser.test.assertEq(2, results.length);
+ browser.test.assertEq(book1contacts.oscar, results[0].id);
+ browser.test.assertEq(book2contacts.oscarTheGrouch, results[1].id);
+
+ // No valid search strings.
+ results = await browser.contacts.quickSearch(" ");
+ browser.test.assertEq(0, results.length);
+
+ await browser.addressBooks.delete(book1);
+ await browser.addressBooks.delete(book2);
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
+
+add_task(async function test_quickSearch_types() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Add a card to the personal AB.
+ let personaAB = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+
+ let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact.displayName = "personal contact";
+ contact.firstName = "personal";
+ contact.lastName = "contact";
+ contact.primaryEmail = "personal@invalid";
+ contact = personaAB.addCard(contact);
+
+ // Set up the history AB as read-only.
+ let historyAB = MailServices.ab.getDirectory("jsaddrbook://history.sqlite");
+
+ contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxy";
+ contact.displayName = "history contact";
+ contact.firstName = "history";
+ contact.lastName = "contact";
+ contact.primaryEmail = "history@invalid";
+ contact = historyAB.addCard(contact);
+
+ historyAB.setBoolValue("readOnly", true);
+
+ Assert.ok(historyAB.readOnly);
+
+ // Set up an LDAP address book.
+ LDAPServer.open();
+
+ // Create an LDAP directory
+ MailServices.ab.newAddressBook(
+ "test",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ async function background() {
+ function checkCards(cards, expectedNames) {
+ browser.test.assertEq(expectedNames.length, cards.length);
+ let expected = new Set(expectedNames);
+ for (let card of cards) {
+ expected.delete(card.properties.FirstName);
+ }
+ browser.test.assertEq(
+ 0,
+ expected.size,
+ "Should have seen all expected cards"
+ );
+ }
+ // No arguments should get cards from all address books.
+ let results = await browser.contacts.quickSearch("contact");
+ checkCards(results, ["personal", "history", "LDAP"]);
+
+ // An empty argument should get cards from all address books.
+ results = await browser.contacts.quickSearch({ searchString: "contact" });
+ checkCards(results, ["personal", "history", "LDAP"]);
+
+ // Skip remote address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeRemote: false,
+ });
+ checkCards(results, ["personal", "history"]);
+
+ // Skip local address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeLocal: false,
+ });
+ checkCards(results, ["LDAP"]);
+
+ // Skip read-only address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeReadOnly: false,
+ });
+ checkCards(results, ["personal"]);
+
+ // Skip read-write address books.
+ results = await browser.contacts.quickSearch({
+ searchString: "contact",
+ includeReadWrite: false,
+ });
+ checkCards(results, ["LDAP", "history"]);
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: { permissions: ["addressBooks"] },
+ });
+
+ let startupPromise = extension.startup();
+
+ // This for loop handles returning responses for LDAP. It should run once
+ // for each test that queries the remote address book.
+ for (let i = 0; i < 4; i++) {
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry({
+ dn: "uid=ldap,dc=contact,dc=invalid",
+ attributes: {
+ objectClass: "person",
+ cn: "LDAP contact",
+ givenName: "LDAP",
+ mail: "eurus@contact.invalid",
+ sn: "contact",
+ },
+ });
+ LDAPServer.writeSearchResultDone();
+ }
+
+ await startupPromise;
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js
new file mode 100644
index 0000000000..3b40dc67a2
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js
@@ -0,0 +1,148 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_setup(async () => {
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+
+ let historyAB = MailServices.ab.getDirectory("jsaddrbook://history.sqlite");
+
+ let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact1.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact1.displayName = "contact number one";
+ contact1.firstName = "contact";
+ contact1.lastName = "one";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = historyAB.addCard(contact1);
+
+ let mailList = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ mailList.isMailList = true;
+ mailList.dirName = "Mailing";
+ mailList.listNickName = "Mailing";
+ mailList.description = "";
+
+ historyAB.addMailList(mailList);
+ historyAB.setBoolValue("readOnly", true);
+
+ Assert.ok(historyAB.readOnly);
+});
+
+add_task(async function test_addressBooks_readonly() {
+ async function background() {
+ let list = await browser.addressBooks.list();
+
+ // The read only AB should be in the list.
+ let readOnlyAB = list.find(ab => ab.name == "Collected Addresses");
+ browser.test.assertTrue(!!readOnlyAB, "Should have found the address book");
+
+ browser.test.assertTrue(
+ readOnlyAB.readOnly,
+ "Should have marked the address book as read-only"
+ );
+
+ let card = await browser.contacts.get(
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+ );
+ browser.test.assertTrue(!!card, "Should have found the card");
+
+ browser.test.assertTrue(
+ card.readOnly,
+ "Should have marked the card as read-only"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.create(readOnlyAB.id, {
+ email: "test@example.com",
+ }),
+ "Cannot create a contact in a read-only address book",
+ "Should reject creating an address book card"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.update(card.id, card.properties),
+ "Cannot modify a contact in a read-only address book",
+ "Should reject modifying an address book card"
+ );
+
+ await browser.test.assertRejects(
+ browser.contacts.delete(card.id),
+ "Cannot delete a contact in a read-only address book",
+ "Should reject deleting an address book card"
+ );
+
+ // Mailing List
+
+ let mailingLists = await browser.mailingLists.list(readOnlyAB.id);
+ let readOnlyML = mailingLists[0];
+ browser.test.assertTrue(!!readOnlyAB, "Should have found the mailing list");
+
+ browser.test.assertTrue(
+ readOnlyML.readOnly,
+ "Should have marked the mailing list as read-only"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.create(readOnlyAB.id, { name: "Test" }),
+ "Cannot create a mailing list in a read-only address book",
+ "Should reject creating a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.update(readOnlyML.id, { name: "newTest" }),
+ "Cannot modify a mailing list in a read-only address book",
+ "Should reject modifying a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.delete(readOnlyML.id),
+ "Cannot delete a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.addMember(readOnlyML.id, card.id),
+ "Cannot add to a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ await browser.test.assertRejects(
+ browser.mailingLists.removeMember(readOnlyML.id, card.id),
+ "Cannot remove from a mailing list in a read-only address book",
+ "Should reject deleting a mailing list"
+ );
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js
new file mode 100644
index 0000000000..7a34c8ce86
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+add_setup(async () => {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap.
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+ Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1);
+
+ LDAPServer.open();
+
+ // Create an LDAP directory.
+ MailServices.ab.newAddressBook(
+ "test",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ registerCleanupFunction(() => {
+ LDAPServer.close();
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ });
+});
+
+add_task(async function test_addressBooks_remote() {
+ async function background() {
+ let list = await browser.addressBooks.list();
+
+ // The remote AB should be in the list.
+ let remoteAB = list.find(ab => ab.name == "test");
+ browser.test.assertTrue(!!remoteAB, "Should have found the address book");
+
+ browser.test.assertTrue(
+ remoteAB.remote,
+ "Should have marked the address book as remote"
+ );
+
+ let cards = await browser.contacts.quickSearch("eurus");
+ browser.test.assertTrue(
+ cards.length,
+ "Should have found at least one card"
+ );
+
+ browser.test.assertTrue(
+ cards[0].remote,
+ "Should have marked the card as remote"
+ );
+
+ // Mailing lists are not supported for LDAP address books.
+
+ browser.test.notifyPass("addressBooks");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": background,
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["addressBooks"],
+ },
+ });
+
+ let startupPromise = extension.startup();
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry({
+ dn: "uid=eurus,dc=bakerstreet,dc=invalid",
+ attributes: {
+ objectClass: "person",
+ cn: "Eurus Holmes",
+ givenName: "Eurus",
+ mail: "eurus@bakerstreet.invalid",
+ sn: "Holmes",
+ },
+ });
+ LDAPServer.writeSearchResultDone();
+
+ await startupPromise;
+ await extension.awaitFinish("addressBooks");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js b/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js
new file mode 100644
index 0000000000..3fff1e0e08
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_alias.js
@@ -0,0 +1,123 @@
+/* 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 { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+// ExtensionContent.jsm needs to know when it's running from xpcshell,
+// to use the right timeout for content scripts executed at document_idle.
+ExtensionTestUtils.mockAppInfo();
+
+AddonTestUtils.maybeInit(this);
+const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+
+server.registerPathHandler("/dummy", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(
+ "<!DOCTYPE html><html><head><meta charset='utf8'></head><body></body></html>"
+ );
+});
+
+add_task(async function test_alias() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let pending = new Set(["contentscript", "webscript"]);
+
+ browser.runtime.onMessage.addListener(message => {
+ if (message == "contentscript") {
+ pending.delete(message);
+ browser.test.succeed("Content script has completed");
+ } else if (message == "webscript") {
+ pending.delete(message);
+ browser.test.succeed("Web accessible script has completed");
+ }
+
+ if (pending.size == 0) {
+ browser.test.notifyPass("ext_alias");
+ }
+ });
+
+ browser.test.assertEq(
+ "object",
+ typeof browser,
+ "Background script has browser object"
+ );
+ browser.test.assertEq(
+ "object",
+ typeof messenger,
+ "Background script has messenger object"
+ );
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id, // eslint-disable-line no-undef
+ "Background script can access the manifest"
+ );
+ },
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/dummy"],
+ js: ["content.js"],
+ },
+ ],
+
+ applications: { gecko: { id: "alias@xpcshell" } },
+ web_accessible_resources: ["web.html", "web.js"],
+ },
+ files: {
+ "content.js": `
+ browser.test.assertEq("object", typeof browser, "Content script has browser object");
+ browser.test.assertEq("object", typeof messenger, "Content script has messenger object");
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id,
+ "Content script can access manifest"
+ );
+
+ // Unprivileged content in a frame
+ let frame = document.createElement("iframe");
+ frame.src = browser.runtime.getURL("web.html");
+ document.body.appendChild(frame);
+
+ browser.runtime.sendMessage("contentscript");
+ `,
+ "web.html": `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset='utf8'>
+ <script src="web.js"></script>
+ </head>
+ <body>
+ </body>
+ </html>
+ `,
+ "web.js": `
+ browser.test.assertEq("object", typeof browser, "Web accessible script has browser object");
+ browser.test.assertEq("object", typeof messenger, "Web accessible script has messenger object");
+ browser.test.assertEq(
+ "alias@xpcshell",
+ messenger.runtime.getManifest().applications.gecko.id,
+ "Web accessible script can access manifest"
+ );
+
+ browser.runtime.sendMessage("webscript");
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ const contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/dummy"
+ );
+ await extension.awaitFinish("ext_alias");
+
+ await contentPage.close();
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js b/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js
new file mode 100644
index 0000000000..ef2687af68
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js
@@ -0,0 +1,350 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+var { getCachedAllowedSpaces, setCachedAllowedSpaces } = ChromeUtils.import(
+ "resource:///modules/ExtensionToolbarButtons.jsm"
+);
+var { storeState, getState } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizationState.mjs"
+);
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const {
+ createAppInfo,
+ createHttpServer,
+ createTempXPIFile,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+} = AddonTestUtils;
+
+// Prepare test environment to be able to load add-on updates.
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+let gProfD = do_get_profile();
+let profileDir = gProfD.clone();
+profileDir.append("extensions");
+const stageDir = profileDir.clone();
+stageDir.append("staged");
+
+let server = createHttpServer({
+ hosts: ["example.com"],
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "102");
+
+async function enforceState(state) {
+ const stateChangeObserved = TestUtils.topicObserved(
+ "unified-toolbar-state-change"
+ );
+ storeState(state);
+ await stateChangeObserved;
+}
+
+function check(testType, expectedCache, expectedMail, expectedCalendar) {
+ let extensionId = `browser_action_spaces_${testType}@mochi.test`;
+
+ Assert.equal(
+ getCachedAllowedSpaces().has(extensionId),
+ expectedCache != null,
+ "CachedAllowedSpaces should include the test extension"
+ );
+ if (expectedCache != null) {
+ Assert.deepEqual(
+ getCachedAllowedSpaces().get(extensionId),
+ expectedCache,
+ "CachedAllowedSpaces should be correct"
+ );
+ }
+ Assert.equal(
+ getState().mail.includes(`ext-${extensionId}`),
+ expectedMail,
+ "The mail state should include the action button of the test extension"
+ );
+ Assert.equal(
+ getState().calendar.includes(`ext-${extensionId}`),
+ expectedCalendar,
+ "The calendar state should include the action button of the test extension"
+ );
+}
+
+function addXPI(testType, thisVersion, nextVersion, browser_action) {
+ server.registerFile(
+ `/addons/${testType}_v${thisVersion}.xpi`,
+ createTempXPIFile({
+ "manifest.json": {
+ manifest_version: 2,
+ name: testType,
+ version: `${thisVersion}.0`,
+ background: { scripts: ["background.js"] },
+ applications: {
+ gecko: {
+ id: `browser_action_spaces_${testType}@mochi.test`,
+ update_url: nextVersion
+ ? `http://example.com/${testType}_updates_v${nextVersion}.json`
+ : null,
+ },
+ },
+ browser_action,
+ },
+ "background.js": `
+ if (browser.runtime.getManifest().name == "delayed") {
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ browser.test.sendMessage("update postponed by ${thisVersion}");
+ });
+ }
+ browser.test.log(" ===== ready ${testType} ${thisVersion}");
+ browser.test.sendMessage("ready ${thisVersion}");`,
+ })
+ );
+ if (nextVersion) {
+ addUpdateJSON(testType, nextVersion);
+ }
+}
+
+function addUpdateJSON(testType, nextVersion) {
+ let extensionId = `browser_action_spaces_${testType}@mochi.test`;
+
+ AddonTestUtils.registerJSON(
+ server,
+ `/${testType}_updates_v${nextVersion}.json`,
+ {
+ addons: {
+ [extensionId]: {
+ updates: [
+ {
+ version: `${nextVersion}.0`,
+ update_link: `http://example.com/addons/${testType}_v${nextVersion}.xpi`,
+ applications: {
+ gecko: {
+ strict_min_version: "1",
+ },
+ },
+ },
+ ],
+ },
+ },
+ }
+ );
+}
+
+async function checkForExtensionUpdate(testType, extension) {
+ let update = await promiseFindAddonUpdates(extension.addon);
+ let install = update.updateAvailable;
+ await promiseCompleteAllInstalls([install]);
+
+ if (testType == "normal") {
+ Assert.equal(
+ install.state,
+ AddonManager.STATE_INSTALLED,
+ "Update should have been installed"
+ );
+ } else {
+ Assert.equal(
+ install.state,
+ AddonManager.STATE_POSTPONED,
+ "Update should have been postponed"
+ );
+ }
+}
+
+async function runTest(testType) {
+ // Simulate starting up the app.
+ await promiseStartupManager();
+
+ // Set a customized state for the spaces we are working with in this test.
+ await enforceState({
+ mail: ["spacer", "search-bar", "spacer"],
+ calendar: ["spacer", "search-bar", "spacer"],
+ });
+
+ // Check conditions before installing the add-on.
+ check(testType, null, false, false);
+
+ // Add the required update JSON to our test server, to be able to update to v2.
+ addUpdateJSON(testType, 2);
+ // Install addon v1 without a browserAction.
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ files: {
+ "background.js": function () {
+ if (browser.runtime.getManifest().name == "delayed") {
+ function handleUpdateAvailable(details) {
+ browser.test.sendMessage("update postponed by 1");
+ }
+ browser.runtime.onUpdateAvailable.addListener(handleUpdateAvailable);
+ }
+ browser.test.sendMessage("ready 1");
+ },
+ },
+ manifest: {
+ background: { scripts: ["background.js"] },
+ version: "1.0",
+ name: testType,
+ applications: {
+ gecko: {
+ id: `browser_action_spaces_${testType}@mochi.test`,
+ update_url: `http://example.com/${testType}_updates_v2.json`,
+ },
+ },
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready 1");
+
+ // State should not have changed.
+ check(testType, null, false, false);
+
+ // v2 will add the mail space and the default space.
+ addXPI(testType, 2, 3, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 1");
+ // Restart to install the update v2.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 2");
+
+ // The button should have been added to the mail space.
+ check(testType, ["mail", "default"], true, false);
+
+ // Remove our extension button from all customized states.
+ await enforceState({
+ mail: ["spacer", "search-bar", "spacer"],
+ calendar: ["spacer", "search-bar", "spacer"],
+ });
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 2");
+
+ // The button should not be re-added to any space after the restart.
+ check(testType, ["mail", "default"], false, false);
+
+ // v3 will add the calendar space.
+ addXPI(testType, 3, 4, {
+ allowed_spaces: ["mail", "calendar", "default"],
+ });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 2");
+ // Restart to install the update v3.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 3");
+
+ // The button should have been added to the calendar space.
+ check(testType, ["mail", "calendar", "default"], false, true);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 3");
+
+ // Should not have changed.
+ check(testType, ["mail", "calendar", "default"], false, true);
+
+ // v4 will remove the calendar space again.
+ addXPI(testType, 4, 5, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 3");
+ // Restart to install the update v4.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 4");
+
+ // The calendar space should no longer be known and the button should be removed
+ // from the calendar space.
+ check(testType, ["mail", "default"], false, false);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 4");
+
+ // Should not have changed.
+ check(testType, ["mail", "default"], false, false);
+
+ // v5 will remove the entire browser_action. Testing the onUpdate code path in
+ // ext-browserAction.
+ addXPI(testType, 5, 6, null);
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 4");
+ // Restart to install the update v5.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 5");
+
+ // There should no longer be a cached entry for any known spaces.
+ check(testType, null, false, false);
+
+ // Simulate restarting the app.
+ await promiseRestartManager();
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 5");
+
+ // Should not have changed.
+ check(testType, null, false, false);
+
+ // v6 will add the mail space again.
+ addXPI(testType, 6, null, { allowed_spaces: ["mail", "default"] });
+ await checkForExtensionUpdate(testType, extension);
+
+ if (testType == "delayed") {
+ await extension.awaitMessage("update postponed by 5");
+ // Restart to install the update v6.
+ await promiseRestartManager();
+ }
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("ready 6");
+
+ // The button should have been added to the mail space.
+ check(testType, ["mail", "default"], true, false);
+
+ // Unload the extension. Testing the onUninstall code path in ext-browserAction.
+ await extension.unload();
+
+ // There should no longer be a cached entry for any known spaces.
+ check(testType, null, false, false);
+
+ await promiseShutdownManager();
+}
+
+add_task(async function test_normal_updates() {
+ await runTest("normal");
+});
+
+add_task(async function test_delayed_updates() {
+ await runTest("delayed");
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js b/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js
new file mode 100644
index 0000000000..d8ccd58da6
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -0,0 +1,279 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_managers() {
+ let account = createAccount();
+ let folder = await createSubfolder(
+ account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(folder, 5);
+
+ let files = {
+ "background.js": async () => {
+ let [testAccount] = await browser.accounts.list();
+ let testFolder = testAccount.folders.find(f => f.name == "test1");
+ let {
+ messages: [testMessage],
+ } = await browser.messages.list(testFolder);
+
+ let messageCount = await browser.testapi.testCanGetFolder(testFolder);
+ browser.test.assertEq(5, messageCount);
+
+ let convertedFolder = await browser.testapi.testCanConvertFolder();
+ browser.test.assertEq(testFolder.accountId, convertedFolder.accountId);
+ browser.test.assertEq(testFolder.path, convertedFolder.path);
+
+ let subject = await browser.testapi.testCanGetMessage(testMessage.id);
+ browser.test.assertEq(testMessage.subject, subject);
+
+ let convertedMessage = await browser.testapi.testCanConvertMessage();
+ browser.test.log(JSON.stringify(convertedMessage));
+ browser.test.assertEq(testMessage.id, convertedMessage.id);
+ browser.test.assertEq(testMessage.subject, convertedMessage.subject);
+
+ let messageList = await browser.testapi.testCanStartMessageList();
+ browser.test.assertEq(36, messageList.id.length);
+ browser.test.assertEq(4, messageList.messages.length);
+ browser.test.assertEq(
+ testMessage.subject,
+ messageList.messages[0].subject
+ );
+
+ messageList = await browser.messages.continueList(messageList.id);
+ browser.test.assertEq(null, messageList.id);
+ browser.test.assertEq(1, messageList.messages.length);
+ browser.test.assertTrue(
+ testMessage.subject != messageList.messages[0].subject
+ );
+
+ let [bookUID, contactUID, listUID] = await window.sendMessage("get UIDs");
+ let [foundBook, foundContact, foundList] =
+ await browser.testapi.testCanFindAddressBookItems(
+ bookUID,
+ contactUID,
+ listUID
+ );
+ browser.test.assertEq("new book", foundBook.name);
+ browser.test.assertEq("new contact", foundContact.properties.DisplayName);
+ browser.test.assertEq("new list", foundList.name);
+
+ browser.test.notifyPass("finished");
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ ...files,
+ "schema.json": [
+ {
+ namespace: "testapi",
+ functions: [
+ {
+ name: "testCanGetFolder",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "folder",
+ $ref: "folders.MailFolder",
+ },
+ ],
+ },
+ {
+ name: "testCanConvertFolder",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanGetMessage",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "messageId",
+ type: "integer",
+ },
+ ],
+ },
+ {
+ name: "testCanConvertMessage",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanStartMessageList",
+ type: "function",
+ async: true,
+ parameters: [],
+ },
+ {
+ name: "testCanFindAddressBookItems",
+ type: "function",
+ async: true,
+ parameters: [
+ { name: "bookUID", type: "string" },
+ { name: "contactUID", type: "string" },
+ { name: "listUID", type: "string" },
+ ],
+ },
+ ],
+ },
+ ],
+ "implementation.js": () => {
+ var { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+ );
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ this.testapi = class extends ExtensionCommon.ExtensionAPI {
+ getAPI(context) {
+ return {
+ testapi: {
+ async testCanGetFolder({ accountId, path }) {
+ let realFolder = context.extension.folderManager.get(
+ accountId,
+ path
+ );
+ return realFolder.getTotalMessages(false);
+ },
+ async testCanConvertFolder() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ return context.extension.folderManager.convert(realFolder);
+ },
+ async testCanGetMessage(messageId) {
+ let realMessage =
+ context.extension.messageManager.get(messageId);
+ return realMessage.subject;
+ },
+ async testCanConvertMessage() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ let realMessage = [...realFolder.messages][0];
+ return context.extension.messageManager.convert(realMessage);
+ },
+ async testCanStartMessageList() {
+ let realFolder = MailServices.accounts.allFolders.find(
+ f => f.name == "test1"
+ );
+ return context.extension.messageManager.startMessageList(
+ realFolder.messages
+ );
+ },
+ async testCanFindAddressBookItems(
+ bookUID,
+ contactUID,
+ listUID
+ ) {
+ let foundBook =
+ context.extension.addressBookManager.findAddressBookById(
+ bookUID
+ );
+ let foundContact =
+ context.extension.addressBookManager.findContactById(
+ contactUID
+ );
+ let foundList =
+ context.extension.addressBookManager.findMailingListById(
+ listUID
+ );
+
+ return [
+ await context.extension.addressBookManager.convert(
+ foundBook
+ ),
+ await context.extension.addressBookManager.convert(
+ foundContact
+ ),
+ await context.extension.addressBookManager.convert(
+ foundList
+ ),
+ ];
+ },
+ },
+ };
+ }
+ };
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "addressBooks", "messagesRead"],
+ experiment_apis: {
+ testapi: {
+ schema: "schema.json",
+ parent: {
+ scopes: ["addon_parent"],
+ paths: [["testapi"]],
+ script: "implementation.js",
+ },
+ },
+ },
+ },
+ });
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ let contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.displayName = "new contact";
+ contact.firstName = "new";
+ contact.lastName = "contact";
+ contact.primaryEmail = "new.contact@invalid";
+ contact = book.addCard(contact);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ list.addCard(contact);
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 4);
+
+ await extension.startup();
+ await extension.awaitMessage("get UIDs");
+ extension.sendMessage(book.UID, contact.UID, list.UID);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+
+ await new Promise(resolve => {
+ let observer = {
+ observe() {
+ Services.obs.removeObserver(observer, "addrbook-directory-deleted");
+ resolve();
+ },
+ };
+ Services.obs.addObserver(observer, "addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(book.URI);
+ });
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js b/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js
new file mode 100644
index 0000000000..39a8d63016
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_folders.js
@@ -0,0 +1,560 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_folders() {
+ let files = {
+ "background.js": async () => {
+ let [accountId, IS_IMAP] = await window.waitForMessage();
+
+ let account = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, account.folders.length);
+
+ // Test create.
+
+ let onCreatedPromise = window.waitForEvent("folders.onCreated");
+ let folder1 = await browser.folders.create(account, "folder1");
+ let [createdFolder] = await onCreatedPromise;
+ for (let folder of [folder1, createdFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder1", folder.name);
+ browser.test.assertEq("/folder1", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ // Check order of the returned folders being correct (new folder not last).
+ browser.test.assertEq(4, account.folders.length);
+ if (IS_IMAP) {
+ browser.test.assertEq("Inbox", account.folders[0].name);
+ browser.test.assertEq("Trash", account.folders[1].name);
+ } else {
+ browser.test.assertEq("Trash", account.folders[0].name);
+ browser.test.assertEq("Outbox", account.folders[1].name);
+ }
+ browser.test.assertEq("folder1", account.folders[2].name);
+ browser.test.assertEq("unused", account.folders[3].name);
+
+ let folder2 = await browser.folders.create(folder1, "folder+2");
+ browser.test.assertEq(accountId, folder2.accountId);
+ browser.test.assertEq("folder+2", folder2.name);
+ browser.test.assertEq("/folder1/folder+2", folder2.path);
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq(
+ "/folder1/folder+2",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on creating already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.create(folder1, "folder+2"),
+ `folders.create() failed, because folder+2 already exists in /folder1`,
+ "browser.folders.create threw exception"
+ );
+
+ // Test rename.
+
+ {
+ let onRenamedPromise = window.waitForEvent("folders.onRenamed");
+ let folder3 = await browser.folders.rename(
+ { accountId, path: "/folder1/folder+2" },
+ "folder3"
+ );
+ let [originalFolder, renamedFolder] = await onRenamedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder+2", originalFolder.name);
+ browser.test.assertEq("/folder1/folder+2", originalFolder.path);
+ // Test the renamed folder.
+ for (let folder of [folder3, renamedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder3", folder.name);
+ browser.test.assertEq("/folder1/folder3", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq(
+ "/folder1/folder3",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on renaming absolute root.
+ await browser.test.assertRejects(
+ browser.folders.rename({ accountId, path: "/" }, "UhhOh"),
+ `folders.rename() failed, because it cannot rename the root of the account`,
+ "browser.folders.rename threw exception"
+ );
+
+ // Test reject on renaming to existing folder.
+ await browser.test.assertRejects(
+ browser.folders.rename(
+ { accountId, path: "/folder1/folder3" },
+ "folder3"
+ ),
+ `folders.rename() failed, because folder3 already exists in /folder1`,
+ "browser.folders.rename threw exception"
+ );
+ }
+
+ // Test delete (and onMoved).
+
+ {
+ // The delete request will trigger an onDelete event for IMAP and an
+ // onMoved event for local folders.
+ let deletePromise = window.waitForEvent(
+ `folders.${IS_IMAP ? "onDeleted" : "onMoved"}`
+ );
+ await browser.folders.delete({ accountId, path: "/folder1/folder3" });
+ // The onMoved event returns the original/deleted and the new folder.
+ // The onDeleted event returns just the original/deleted folder.
+ let [originalFolder, folderMovedToTrash] = await deletePromise;
+
+ // Test the originalFolder folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder3", originalFolder.name);
+ browser.test.assertEq("/folder1/folder3", originalFolder.path);
+
+ // Check if it really is in trash folder.
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ let trashFolder = account.folders.find(f => f.name == "Trash");
+ browser.test.assertTrue(trashFolder);
+ browser.test.assertEq("/Trash", trashFolder.path);
+ browser.test.assertEq(1, trashFolder.subFolders.length);
+ browser.test.assertEq(
+ "/Trash/folder3",
+ trashFolder.subFolders[0].path
+ );
+ browser.test.assertEq("/folder1", account.folders[2].path);
+
+ if (!IS_IMAP) {
+ // For non IMAP folders, the delete request has triggered an onMoved
+ // event, check if that has reported moving the folder to trash.
+ browser.test.assertEq(accountId, folderMovedToTrash.accountId);
+ browser.test.assertEq("folder3", folderMovedToTrash.name);
+ browser.test.assertEq("/Trash/folder3", folderMovedToTrash.path);
+
+ // Delete the folder from trash.
+ let onDeletedPromise = window.waitForEvent("folders.onDeleted");
+ await browser.folders.delete({ accountId, path: "/Trash/folder3" });
+ let [deletedFolder] = await onDeletedPromise;
+ browser.test.assertEq(accountId, deletedFolder.accountId);
+ browser.test.assertEq("folder3", deletedFolder.name);
+ browser.test.assertEq("/Trash/folder3", deletedFolder.path);
+ // Check if the folder is gone.
+ let trashSubfolders = await browser.folders.getSubFolders(
+ trashFolder,
+ false
+ );
+ browser.test.assertEq(
+ 0,
+ trashSubfolders.length,
+ "Folder has been deleted from trash."
+ );
+ } else {
+ // The IMAP test server signals success for the delete request, but
+ // keeps the folder. Testing for this broken behavior to get notified
+ // via test fails, if this behaviour changes.
+ await browser.folders.delete({ accountId, path: "/Trash/folder3" });
+ let trashSubfolders = await browser.folders.getSubFolders(
+ trashFolder,
+ false
+ );
+ browser.test.assertEq(
+ "/Trash/folder3",
+ trashSubfolders[0].path,
+ "IMAP test server cannot delete from trash, the folder is still there."
+ );
+ }
+
+ // Test reject on deleting non-existing folder.
+ await browser.test.assertRejects(
+ browser.folders.delete({ accountId, path: "/folder1/folder5" }),
+ `Folder not found: /folder1/folder5`,
+ "browser.folders.delete threw exception"
+ );
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, account.folders.length);
+ browser.test.assertEq("/folder1", account.folders[2].path);
+ }
+
+ // Test move.
+
+ {
+ await browser.folders.create(folder1, "folder4");
+ let onMovedPromise = window.waitForEvent("folders.onMoved");
+ let folder4_moved = await browser.folders.move(
+ { accountId, path: "/folder1/folder4" },
+ { accountId, path: "/" }
+ );
+ let [originalFolder, movedFolder] = await onMovedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder4", originalFolder.name);
+ browser.test.assertEq("/folder1/folder4", originalFolder.path);
+ // Test the moved folder.
+ for (let folder of [folder4_moved, movedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder4", folder.name);
+ browser.test.assertEq("/folder4", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(5, account.folders.length);
+ browser.test.assertEq("/folder4", account.folders[3].path);
+
+ // Test reject on moving to already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.move(folder4_moved, account),
+ `folders.move() failed, because folder4 already exists in /`,
+ "browser.folders.move threw exception"
+ );
+ }
+
+ // Test copy.
+
+ {
+ let onCopiedPromise = window.waitForEvent("folders.onCopied");
+ let folder4_copied = await browser.folders.copy(
+ { accountId, path: "/folder4" },
+ { accountId, path: "/folder1" }
+ );
+ let [originalFolder, copiedFolder] = await onCopiedPromise;
+ // Test the original folder.
+ browser.test.assertEq(accountId, originalFolder.accountId);
+ browser.test.assertEq("folder4", originalFolder.name);
+ browser.test.assertEq("/folder4", originalFolder.path);
+ // Test the copied folder.
+ for (let folder of [folder4_copied, copiedFolder]) {
+ browser.test.assertEq(accountId, folder.accountId);
+ browser.test.assertEq("folder4", folder.name);
+ browser.test.assertEq("/folder1/folder4", folder.path);
+ }
+
+ account = await browser.accounts.get(accountId);
+ browser.test.assertEq(5, account.folders.length);
+ browser.test.assertEq(1, account.folders[2].subFolders.length);
+ browser.test.assertEq("/folder4", account.folders[3].path);
+ browser.test.assertEq(
+ "/folder1/folder4",
+ account.folders[2].subFolders[0].path
+ );
+
+ // Test reject on copy to already existing folder.
+ await browser.test.assertRejects(
+ browser.folders.copy(folder4_copied, folder1),
+ `folders.copy() failed, because folder4 already exists in /folder1`,
+ "browser.folders.copy threw exception"
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders", "messagesDelete"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox, Trash,
+ // and unused. Otherwise they are Trash, Unsent Messages and unused.
+
+ await extension.startup();
+ extension.sendMessage(account.key, IS_IMAP);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_without_delete_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+
+ // Test reject on delete without messagesDelete permission.
+ await browser.test.assertRejects(
+ browser.folders.delete({ accountId, path: "/unused" }),
+ `Using folders.delete() requires the "accountsFolders" and the "messagesDelete" permission`,
+ "It rejects for a missing permission."
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox,
+ // Trash, and unused. Otherwise they are Trash, Unsent Messages and unused.
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(async function test_getParentFolders_getSubFolders() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let account = await browser.accounts.get(accountId);
+
+ async function createSubFolder(folderOrAccount, name) {
+ let subFolder = await browser.folders.create(folderOrAccount, name);
+ let basePath = folderOrAccount.path || "/";
+ if (!basePath.endsWith("/")) {
+ basePath = basePath + "/";
+ }
+ browser.test.assertEq(accountId, subFolder.accountId);
+ browser.test.assertEq(name, subFolder.name);
+ browser.test.assertEq(`${basePath}${name}`, subFolder.path);
+ return subFolder;
+ }
+
+ // Create a new root folder in the account.
+ let root = await createSubFolder(account, "MyRoot");
+
+ // Build a flat list of newly created nested folders in MyRoot.
+ let flatFolders = [root];
+ for (let i = 0; i < 10; i++) {
+ flatFolders.push(await createSubFolder(flatFolders[i], `level${i}`));
+ }
+
+ // Test getParentFolders().
+
+ // Pop out the last child folder and get its parents.
+ let lastChild = flatFolders.pop();
+ let parentsWithSubDefault = await browser.folders.getParentFolders(
+ lastChild
+ );
+ let parentsWithSubFalse = await browser.folders.getParentFolders(
+ lastChild,
+ false
+ );
+ let parentsWithSubTrue = await browser.folders.getParentFolders(
+ lastChild,
+ true
+ );
+
+ browser.test.assertEq(10, parentsWithSubDefault.length, "Correct depth.");
+ browser.test.assertEq(10, parentsWithSubFalse.length, "Correct depth.");
+ browser.test.assertEq(10, parentsWithSubTrue.length, "Correct depth.");
+
+ // Reverse the flatFolders array, to match the expected return value of
+ // getParentFolders().
+ flatFolders.reverse();
+
+ // Build expected nested subfolder structure.
+ lastChild.subFolders = [];
+ let flatFoldersWithSub = [];
+ for (let i = 0; i < 10; i++) {
+ let f = {};
+ Object.assign(f, flatFolders[i]);
+ if (i == 0) {
+ f.subFolders = [lastChild];
+ } else {
+ f.subFolders = [flatFoldersWithSub[i - 1]];
+ }
+ flatFoldersWithSub.push(f);
+ }
+
+ // Test return values of getParentFolders(). The way the flatFolder array
+ // has been created, its entries do not have subFolder properties.
+ for (let i = 0; i < 10; i++) {
+ window.assertDeepEqual(parentsWithSubFalse[i], flatFolders[i]);
+ window.assertDeepEqual(flatFolders[i], parentsWithSubFalse[i]);
+
+ window.assertDeepEqual(parentsWithSubTrue[i], flatFoldersWithSub[i]);
+ window.assertDeepEqual(flatFoldersWithSub[i], parentsWithSubTrue[i]);
+
+ // Default = false
+ window.assertDeepEqual(parentsWithSubDefault[i], flatFolders[i]);
+ window.assertDeepEqual(flatFolders[i], parentsWithSubDefault[i]);
+ }
+
+ // Test getSubFolders().
+
+ let expectedSubsWithSub = [flatFoldersWithSub[8]];
+ let expectedSubsWithoutSub = [flatFolders[8]];
+
+ // Test excluding subfolders (so only the direct subfolder are reported).
+ let subsWithSubFalse = await browser.folders.getSubFolders(root, false);
+ window.assertDeepEqual(expectedSubsWithoutSub, subsWithSubFalse);
+ window.assertDeepEqual(subsWithSubFalse, expectedSubsWithoutSub);
+
+ // Test including all subfolders.
+ let subsWithSubTrue = await browser.folders.getSubFolders(root, true);
+ window.assertDeepEqual(expectedSubsWithSub, subsWithSubTrue);
+ window.assertDeepEqual(subsWithSubTrue, expectedSubsWithSub);
+
+ // Test default subfolder handling of getSubFolders (= true).
+ let subsWithSubDefault = await browser.folders.getSubFolders(root);
+ window.assertDeepEqual(subsWithSubDefault, subsWithSubTrue);
+ window.assertDeepEqual(subsWithSubTrue, subsWithSubDefault);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ await createSubfolder(account.incomingServer.rootFolder, "unused");
+
+ // We should now have three folders. For IMAP accounts they are Inbox,
+ // Trash, and unused. Otherwise they are Trash, Unsent Messages and unused.
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+add_task(async function test_getFolderInfo() {
+ let files = {
+ "background.js": async () => {
+ let [accountId, IS_NNTP] = await window.waitForMessage();
+
+ let account = await browser.accounts.get(accountId);
+ browser.test.assertEq(IS_NNTP ? 1 : 3, account.folders.length);
+ let folders = await browser.folders.getSubFolders(account, false);
+ let InfoTestFolder = folders.find(f => f.name == "InfoTest");
+
+ // Verify initial state.
+ let info = await browser.folders.getFolderInfo(InfoTestFolder);
+ window.assertDeepEqual(
+ { totalMessageCount: 12, unreadMessageCount: 12, favorite: false },
+ info
+ );
+
+ // Test flipping favorite to true and marking all messages as read.
+ let onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("markAllAsRead");
+ await window.sendMessage("setFavorite", true);
+ let [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual(
+ { unreadMessageCount: 0, favorite: true },
+ mailFolderInfo
+ );
+ browser.test.assertEq(InfoTestFolder.path, mailFolder.path);
+
+ info = await browser.folders.getFolderInfo(InfoTestFolder);
+ window.assertDeepEqual(
+ { totalMessageCount: 12, unreadMessageCount: 0, favorite: true },
+ info
+ );
+
+ // Test flipping favorite back to false.
+ onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("setFavorite", false);
+ [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual({ favorite: false }, mailFolderInfo);
+ browser.test.assertEq(InfoTestFolder.path, mailFolder.path);
+
+ // Test setting some messages back to unread.
+ onFolderInfoChangedPromise = window.waitForEvent(
+ "folders.onFolderInfoChanged"
+ );
+ await window.sendMessage("markSomeAsUnread", 5);
+ [mailFolder, mailFolderInfo] = await onFolderInfoChangedPromise;
+ window.assertDeepEqual({ unreadMessageCount: 5 }, mailFolderInfo);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsFolders", "messagesDelete"],
+ },
+ });
+
+ let account = createAccount();
+ // Not all folders appear immediately on IMAP. Creating a new one causes them to appear.
+ let InfoTestFolder = await createSubfolder(
+ account.incomingServer.rootFolder,
+ "InfoTest"
+ );
+ await createMessages(InfoTestFolder, 12);
+
+ extension.onMessage("markAllAsRead", () => {
+ InfoTestFolder.markAllMessagesRead(null);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("markSomeAsUnread", count => {
+ let messages = InfoTestFolder.messages;
+ while (messages.hasMoreElements() && count > 0) {
+ let msg = messages.getNext();
+ msg.markRead(false);
+ count--;
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("setFavorite", value => {
+ if (value) {
+ InfoTestFolder.setFlag(Ci.nsMsgFolderFlags.Favorite);
+ } else {
+ InfoTestFolder.clearFlag(Ci.nsMsgFolderFlags.Favorite);
+ }
+ extension.sendMessage();
+ });
+
+ // We should now have three folders. For IMAP accounts they are Inbox, Trash,
+ // and InfoTest. Otherwise they are Trash, Unsent Messages and InfoTest.
+
+ await extension.startup();
+ extension.sendMessage(account.key, IS_NNTP);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js
new file mode 100644
index 0000000000..eac947cda8
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js
@@ -0,0 +1,374 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// Test events and persistent events for Manifest V3 for onCreated, onRenamed,
+// onMoved, onCopied and onDeleted.
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_folders_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ addIdentity(account, "id1@invalid");
+
+ let files = {
+ "background.js": () => {
+ for (let eventName of [
+ "onCreated",
+ "onDeleted",
+ "onCopied",
+ "onRenamed",
+ "onMoved",
+ ]) {
+ browser.folders[eventName].addListener(async (...args) => {
+ browser.test.log(`${eventName} received: ${JSON.stringify(args)}`);
+ browser.test.sendMessage(`${eventName} received`, args);
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead"],
+ },
+ });
+
+ // Function to start an event page extension (MV3), which can be called whenever
+ // the main test is about to trigger an event. The extension terminates its
+ // background and listens for that single event, verifying it is waking up correctly.
+ async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let _eventName = browser.runtime.getManifest().description;
+
+ browser.folders[_eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${_eventName} received`, args);
+ }
+ });
+
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ permissions: ["accountsRead"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "folders", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "folders", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "folders", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("background started");
+
+ // Create a test folder before terminating the background script, to make sure
+ // everything is sane.
+
+ rootFolder.createSubfolder("TestFolder", null);
+ await extension.awaitMessage("onCreated received");
+ if (IS_IMAP) {
+ // IMAP creates a default Trash folder on the fly.
+ await extension.awaitMessage("onCreated received");
+ }
+
+ // Create SubFolder1.
+
+ {
+ rootFolder.createSubfolder("SubFolder1", null);
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder1",
+ path: "/SubFolder1",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct values"
+ );
+ }
+
+ // Create SubFolder2 (used for primed onFolderInfoChanged).
+
+ {
+ let primedChangeData = await event_page_extension(
+ "onFolderInfoChanged",
+ () => {
+ rootFolder.createSubfolder("SubFolder3", null);
+ }
+ );
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct values"
+ );
+ // Testing for onFolderInfoChanged is difficult, because it may not be for
+ // the last created folder, but for one of the folders created earlier. We
+ // therefore do not check the folder, but only the value.
+ Assert.deepEqual(
+ { totalMessageCount: 0, unreadMessageCount: 0 },
+ primedChangeData[1],
+ "The primed onFolderInfoChanged event should return the correct values"
+ );
+ }
+
+ // Copy.
+
+ {
+ let primedCopyData = await event_page_extension("onCopied", () => {
+ MailServices.copy.copyFolder(
+ rootFolder.getChildNamed("SubFolder3"),
+ rootFolder.getChildNamed("SubFolder1"),
+ false,
+ null,
+ null
+ );
+ });
+ let copyData = await extension.awaitMessage("onCopied received");
+ Assert.deepEqual(
+ primedCopyData,
+ copyData,
+ "The primed onCopied event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ copyData,
+ "The onCopied event should return the correct values"
+ );
+
+ if (IS_IMAP) {
+ // IMAP fires an additional create event.
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ createData,
+ "The onCreated event should return the correct MailFolder values."
+ );
+ }
+ }
+
+ // Move.
+
+ {
+ let primedMoveData = await event_page_extension("onMoved", () => {
+ MailServices.copy.copyFolder(
+ rootFolder.getChildNamed("SubFolder1").getChildNamed("SubFolder3"),
+ rootFolder.getChildNamed("SubFolder3"),
+ true,
+ null,
+ null
+ );
+ });
+
+ let moveData = await extension.awaitMessage("onMoved received");
+ Assert.deepEqual(
+ primedMoveData,
+ moveData,
+ "The primed onMoved event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ moveData,
+ "The onMoved event should return the correct values"
+ );
+
+ if (IS_IMAP) {
+ // IMAP fires additional rename and delete events.
+ let renameData = await extension.awaitMessage("onRenamed received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ renameData,
+ "The onRenamed event should return the correct MailFolder values."
+ );
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder1/SubFolder3",
+ },
+ ],
+ deleteData,
+ "The onDeleted event should return the correct MailFolder values."
+ );
+ }
+ }
+
+ // Delete.
+
+ {
+ let primedDeleteData = await event_page_extension("onDeleted", () => {
+ let subFolder1 = rootFolder.getChildNamed("SubFolder3");
+ subFolder1.propagateDelete(
+ subFolder1.getChildNamed("SubFolder3"),
+ true
+ );
+ });
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ primedDeleteData,
+ deleteData,
+ "The primed onDeleted event should return the correct values"
+ );
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "SubFolder3",
+ path: "/SubFolder3/SubFolder3",
+ },
+ ],
+ deleteData,
+ "The onDeleted event should return the correct values"
+ );
+ }
+
+ // Rename.
+
+ {
+ let primedRenameData = await event_page_extension("onRenamed", () => {
+ rootFolder.getChildNamed("TestFolder").rename("TestFolder2", null);
+ });
+ let renameData = await extension.awaitMessage("onRenamed received");
+ Assert.deepEqual(
+ primedRenameData,
+ renameData,
+ "The primed onRenamed event should return the correct values"
+ );
+ if (IS_IMAP) {
+ // IMAP server sends an additional onDeleted and onCreated.
+ await extension.awaitMessage("onDeleted received");
+ await extension.awaitMessage("onCreated received");
+ }
+ Assert.deepEqual(
+ [
+ {
+ accountId: account.key,
+ name: "TestFolder",
+ path: "/TestFolder",
+ },
+ {
+ accountId: account.key,
+ name: "TestFolder2",
+ path: "/TestFolder2",
+ },
+ ],
+ renameData,
+ "The onRenamed event should return the correct values"
+ );
+ }
+
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js
new file mode 100644
index 0000000000..0b12f8ca1c
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js
@@ -0,0 +1,146 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+add_task(async function test_identities_MV3_event_pages() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account1 = createAccount();
+ addIdentity(account1, "id1@invalid");
+
+ let files = {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+
+ for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) {
+ browser.identities[eventName].addListener((...args) => {
+ // Only send the first event after background wake-up, this should be the
+ // only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${eventName} received`, args);
+ }
+ });
+ }
+
+ browser.test.sendMessage("background started");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ manifest_version: 3,
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "accountsIdentities"],
+ browser_specific_settings: { gecko: { id: "identities@xpcshell.test" } },
+ },
+ });
+
+ function checkPersistentListeners({ primed }) {
+ // A persistent event is referenced by its moduleName as defined in
+ // ext-mails.json, not by its actual namespace.
+ const persistent_events = [
+ "identities.onCreated",
+ "identities.onUpdated",
+ "identities.onDeleted",
+ ];
+
+ for (let event of persistent_events) {
+ let [moduleName, eventName] = event.split(".");
+ assertPersistentListeners(extension, moduleName, eventName, {
+ primed,
+ });
+ }
+ }
+
+ await extension.startup();
+
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Create.
+
+ let id2 = addIdentity(account1, "id2@invalid");
+ let createData = await extension.awaitMessage("onCreated received");
+ Assert.deepEqual(
+ [
+ "id2",
+ {
+ accountId: "account1",
+ id: "id2",
+ label: "",
+ name: "",
+ email: "id2@invalid",
+ replyTo: "",
+ organization: "",
+ composeHtml: true,
+ signature: "",
+ signatureIsPlainText: true,
+ },
+ ],
+ createData,
+ "The primed onCreated event should return the correct values"
+ );
+
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Update
+
+ id2.fullName = "Updated Name";
+ let updateData = await extension.awaitMessage("onUpdated received");
+ Assert.deepEqual(
+ ["id2", { name: "Updated Name", accountId: "account1", id: "id2" }],
+ updateData,
+ "The primed onUpdated event should return the correct values"
+ );
+ await extension.awaitMessage("background started");
+ // Verify persistent listener, not yet primed.
+ checkPersistentListeners({ primed: false });
+ await extension.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listeners.
+ checkPersistentListeners({ primed: true });
+
+ // Delete
+
+ account1.removeIdentity(id2);
+ let deleteData = await extension.awaitMessage("onDeleted received");
+ Assert.deepEqual(
+ ["id2"],
+ deleteData,
+ "The primed onDeleted event should return the correct values"
+ );
+ // The background should have been restarted.
+ await extension.awaitMessage("background started");
+ // The listener should no longer be primed.
+ checkPersistentListeners({ primed: false });
+
+ await extension.unload();
+
+ cleanUpAccount(account1);
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js
new file mode 100644
index 0000000000..24b2cb1484
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages.js
@@ -0,0 +1,730 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+
+let account, rootFolder, subFolders;
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function setup() {
+ account = createAccount();
+ rootFolder = account.incomingServer.rootFolder;
+ subFolders = {
+ test3: await createSubfolder(rootFolder, "test3"),
+ test4: await createSubfolder(rootFolder, "test4"),
+ trash: rootFolder.getChildNamed("Trash"),
+ };
+ await createMessages(subFolders.trash, 99);
+ await createMessages(subFolders.test4, 1);
+ }
+);
+
+add_task(async function non_canonical_permission_description_mapping() {
+ let { msgs } = ExtensionsUI._buildStrings({
+ addon: { name: "FakeExtension" },
+ permissions: {
+ origins: [],
+ permissions: ["accountsRead", "messagesMove"],
+ },
+ });
+ equal(2, msgs.length, "Correct amount of descriptions");
+ equal(
+ "See your mail accounts, their identities and their folders",
+ msgs[0],
+ "Correct description for accountsRead"
+ );
+ equal(
+ "Copy or move your email messages (including moving them to the trash folder)",
+ msgs[1],
+ "Correct description for messagesMove"
+ );
+});
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_pagination() {
+ let files = {
+ "background.js": async () => {
+ // Test a response of 99 messages at 10 messages per page.
+ let [folder] = await window.waitForMessage();
+ let page = await browser.messages.list(folder);
+ browser.test.assertEq(36, page.id.length);
+ browser.test.assertEq(10, page.messages.length);
+
+ let originalPageId = page.id;
+ let numPages = 1;
+ let numMessages = 10;
+ while (page.id) {
+ page = await browser.messages.continueList(page.id);
+ browser.test.assertTrue(page.messages.length > 0);
+ numPages++;
+ numMessages += page.messages.length;
+ if (numMessages < 99) {
+ browser.test.assertEq(originalPageId, page.id);
+ } else {
+ browser.test.assertEq(null, page.id);
+ }
+ }
+ browser.test.assertEq(10, numPages);
+ browser.test.assertEq(99, numMessages);
+
+ browser.test.assertRejects(
+ browser.messages.continueList(originalPageId),
+ /No message list for id .*\. Have you reached the end of a list\?/
+ );
+
+ await window.sendMessage("setPref");
+
+ // Do the same test, but with the default 100 messages per page.
+ page = await browser.messages.list(folder);
+ browser.test.assertEq(null, page.id);
+ browser.test.assertEq(99, page.messages.length);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ Services.prefs.setIntPref("extensions.webextensions.messagesPerPage", 10);
+
+ await extension.startup();
+ extension.sendMessage({ accountId: account.key, path: "/Trash" });
+
+ await extension.awaitMessage("setPref");
+ Services.prefs.clearUserPref("extensions.webextensions.messagesPerPage");
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_delete_without_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ // Try to delete a message.
+ await browser.test.assertThrows(
+ () => browser.messages.delete([folder4Messages[0].id], true),
+ `browser.messages.delete is not a function`,
+ "Should reject deleting without proper permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "messages.delete@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_move_and_copy_without_permission() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+ let testFolder3 = folders.find(f => f.name == "test3");
+
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ // Try to move a message.
+ await browser.test.assertRejects(
+ browser.messages.move([folder4Messages[0].id], testFolder3),
+ `Using messages.move() requires the "accountsRead" and the "messagesMove" permission`,
+ "Should reject move without proper permission"
+ );
+
+ // Try to copy a message.
+ await browser.test.assertRejects(
+ browser.messages.copy([folder4Messages[0].id], testFolder3),
+ `Using messages.copy() requires the "accountsRead" and the "messagesMove" permission`,
+ "Should reject copy without proper permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "messages.move@mochi.test" },
+ },
+ permissions: ["messagesRead", "accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_tags() {
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder4 = folders.find(f => f.name == "test4");
+ let { messages: folder4Messages } = await browser.messages.list(
+ testFolder4
+ );
+
+ let tags1 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ ],
+ tags1
+ );
+
+ // Test some allowed special chars and that the key is created as lower
+ // case.
+ let goodKeys = [
+ "TestKey",
+ "Test_Key",
+ "Test\\Key",
+ "Test}Key",
+ "Test&Key",
+ "Test!Key",
+ "Test§Key",
+ "Test$Key",
+ "Test=Key",
+ "Test?Key",
+ ];
+ for (let key of goodKeys) {
+ await browser.messages.createTag(key, "Test Tag", "#123456");
+ let goodTags = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ {
+ key: key.toLowerCase(),
+ tag: "Test Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ goodTags
+ );
+ await browser.messages.deleteTag(key.toLowerCase());
+ }
+
+ await browser.messages.createTag("custom_tag", "Custom Tag", "#123456");
+ let tags2 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Later",
+ color: "#993399",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags2
+ );
+
+ await browser.messages.updateTag("$label5", {
+ tag: "Much Later",
+ color: "#225599",
+ });
+ let tags3 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "$label5",
+ tag: "Much Later",
+ color: "#225599",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags3
+ );
+
+ // Test rejects for createTag().
+ let badKeys = [
+ "Bad Key",
+ "Bad%Key",
+ "Bad/Key",
+ "Bad*Key",
+ 'Bad"Key',
+ "Bad{Key}",
+ "Bad(Key)",
+ "Bad<Key>",
+ ];
+ for (let badKey of badKeys) {
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(badKey, "Important Stuff", "#223344"),
+ /Type error for parameter key/,
+ `Should reject creating an invalid key: ${badKey}`
+ );
+ }
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "GoodKeyBadColor",
+ "Important Stuff",
+ "#223"
+ ),
+ /Type error for parameter color /,
+ "Should reject creating a key using an invalid short color"
+ );
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "GoodKeyBadColor",
+ "Important Stuff",
+ "123223"
+ ),
+ /Type error for parameter color /,
+ "Should reject creating a key using an invalid color without leading #"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag("$label5", "Important Stuff", "#223344"),
+ `Specified key already exists: $label5`,
+ "Should reject creating a key which exists already"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag(
+ "Custom_Tag",
+ "Important Stuff",
+ "#223344"
+ ),
+ `Specified key already exists: custom_tag`,
+ "Should reject creating a key which exists already"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.createTag("GoodKey", "Important", "#223344"),
+ `Specified tag already exists: Important`,
+ "Should reject creating a key using a tag which exists already"
+ );
+
+ // Test rejects for updateTag();
+ await browser.test.assertThrows(
+ () => browser.messages.updateTag("Bad Key", { tag: "Much Later" }),
+ /Type error for parameter key/,
+ "Should reject updating an invalid key"
+ );
+
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.updateTag("GoodKeyBadColor", { color: "123223" }),
+ /Error processing color/,
+ "Should reject updating a key using an invalid color"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.updateTag("$label50", { tag: "Much Later" }),
+ `Specified key does not exist: $label50`,
+ "Should reject updating an unknown key"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.updateTag("$label5", { tag: "Important" }),
+ `Specified tag already exists: Important`,
+ "Should reject updating a key using a tag which exists already"
+ );
+
+ // Test rejects for deleteTag();
+ await browser.test.assertThrows(
+ () => browser.messages.deleteTag("Bad Key"),
+ /Type error for parameter key/,
+ "Should reject deleting an invalid key"
+ );
+
+ await browser.test.assertRejects(
+ browser.messages.deleteTag("$label50"),
+ `Specified key does not exist: $label50`,
+ "Should reject deleting an unknown key"
+ );
+
+ // Test tagging messages, deleting tag and re-creating tag.
+ await browser.messages.update(folder4Messages[0].id, {
+ tags: ["custom_tag"],
+ });
+ let message1 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual(["custom_tag"], message1.tags);
+
+ await browser.messages.deleteTag("custom_tag");
+ let message2 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual([], message2.tags);
+
+ await browser.messages.createTag("custom_tag", "Custom Tag", "#123456");
+ let message3 = await browser.messages.get(folder4Messages[0].id);
+ window.assertDeepEqual(["custom_tag"], message3.tags);
+
+ // Test deleting built-in tag.
+ await browser.messages.deleteTag("$label5");
+ let tags4 = await browser.messages.listTags();
+ window.assertDeepEqual(
+ [
+ {
+ key: "$label1",
+ tag: "Important",
+ color: "#FF0000",
+ ordinal: "",
+ },
+ {
+ key: "$label2",
+ tag: "Work",
+ color: "#FF9900",
+ ordinal: "",
+ },
+ {
+ key: "$label3",
+ tag: "Personal",
+ color: "#009900",
+ ordinal: "",
+ },
+ {
+ key: "$label4",
+ tag: "To Do",
+ color: "#3333FF",
+ ordinal: "",
+ },
+ {
+ key: "custom_tag",
+ tag: "Custom Tag",
+ color: "#123456",
+ ordinal: "",
+ },
+ ],
+ tags4
+ );
+
+ // Clean up.
+ await browser.messages.update(folder4Messages[0].id, { tags: [] });
+ await browser.messages.deleteTag("custom_tag");
+ await browser.messages.createTag("$label5", "Later", "#993399");
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead", "messagesTags"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_tags_no_permission() {
+ let files = {
+ "background.js": async () => {
+ await browser.test.assertThrows(
+ () =>
+ browser.messages.createTag(
+ "custom_tag",
+ "Important Stuff",
+ "#223344"
+ ),
+ /browser.messages.createTag is not a function/,
+ "Should reject creating tags without messagesTags permission"
+ );
+
+ await browser.test.assertThrows(
+ () => browser.messages.updateTag("$label5", { tag: "Much Later" }),
+ /browser.messages.updateTag is not a function/,
+ "Should reject updating tags without messagesTags permission"
+ );
+
+ await browser.test.assertThrows(
+ () => browser.messages.deleteTag("$label5"),
+ /browser.messages.deleteTag is not a function/,
+ "Should reject deleting tags without messagesTags permission"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["messagesRead", "accountsRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+// The IMAP fakeserver just can't handle this.
+add_task({ skip_if: () => IS_IMAP || IS_NNTP }, async function test_archive() {
+ let account2 = createAccount();
+ account2.addIdentity(MailServices.accounts.createIdentity());
+ let inbox2 = await createSubfolder(
+ account2.incomingServer.rootFolder,
+ "test"
+ );
+ await createMessages(inbox2, 15);
+
+ let month = 10;
+ for (let message of inbox2.messages) {
+ message.date = new Date(2018, month++, 15) * 1000;
+ }
+
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+
+ let accountBefore = await browser.accounts.get(accountId);
+ browser.test.assertEq(3, accountBefore.folders.length);
+ browser.test.assertEq("/test", accountBefore.folders[2].path);
+
+ let messagesBefore = await browser.messages.list(
+ accountBefore.folders[2]
+ );
+ browser.test.assertEq(15, messagesBefore.messages.length);
+ await browser.messages.archive(messagesBefore.messages.map(m => m.id));
+
+ let accountAfter = await browser.accounts.get(accountId);
+ browser.test.assertEq(4, accountAfter.folders.length);
+ browser.test.assertEq("/test", accountAfter.folders[3].path);
+ browser.test.assertEq("/Archives", accountAfter.folders[0].path);
+ browser.test.assertEq(3, accountAfter.folders[0].subFolders.length);
+ browser.test.assertEq(
+ "/Archives/2018",
+ accountAfter.folders[0].subFolders[0].path
+ );
+ browser.test.assertEq(
+ "/Archives/2019",
+ accountAfter.folders[0].subFolders[1].path
+ );
+ browser.test.assertEq(
+ "/Archives/2020",
+ accountAfter.folders[0].subFolders[2].path
+ );
+
+ let messagesAfter = await browser.messages.list(accountAfter.folders[3]);
+ browser.test.assertEq(0, messagesAfter.messages.length);
+
+ let messages2018 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[0]
+ );
+ browser.test.assertEq(2, messages2018.messages.length);
+
+ let messages2019 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[1]
+ );
+ browser.test.assertEq(12, messages2019.messages.length);
+
+ let messages2020 = await browser.messages.list(
+ accountAfter.folders[0].subFolders[2]
+ );
+ browser.test.assertEq(1, messages2020.messages.length);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account2.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js
new file mode 100644
index 0000000000..e46e35afe7
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js
@@ -0,0 +1,499 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_setup() {
+ let _account = createAccount();
+ let _testFolder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+ let binaryAttachment = {
+ body: btoa("binaryAttachment"),
+ filename: "test",
+ contentType: "application/octet-stream",
+ encoding: "base64",
+ };
+
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "0 attachments",
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "1 text attachment",
+ attachments: [textAttachment],
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "1 binary attachment",
+ attachments: [binaryAttachment],
+ });
+ await createMessages(_testFolder, {
+ count: 1,
+ subject: "2 attachments",
+ attachments: [binaryAttachment, textAttachment],
+ });
+ await createMessageFromFile(
+ _testFolder,
+ do_get_file("messages/nestedMessages.eml").path
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let testFolder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(5, messages.length);
+
+ let attachments, attachment, file;
+
+ // "0 attachments" message.
+
+ attachments = await browser.messages.listAttachments(messages[0].id);
+ browser.test.assertEq("0 attachments", messages[0].subject);
+ browser.test.assertEq(0, attachments.length);
+
+ // "1 text attachment" message.
+
+ attachments = await browser.messages.listAttachments(messages[1].id);
+ browser.test.assertEq("1 text attachment", messages[1].subject);
+ browser.test.assertEq(1, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq("text/plain", attachment.contentType);
+ browser.test.assertEq("test.txt", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(14, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[1].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test.txt", file.name);
+ browser.test.assertEq(14, file.size);
+
+ browser.test.assertEq("textAttachment", await file.text());
+
+ let reader = new FileReader();
+ let data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+
+ browser.test.assertEq(
+ "data:text/plain;base64,dGV4dEF0dGFjaG1lbnQ=",
+ data
+ );
+
+ // "1 binary attachment" message.
+
+ attachments = await browser.messages.listAttachments(messages[2].id);
+ browser.test.assertEq("1 binary attachment", messages[2].subject);
+ browser.test.assertEq(1, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq(
+ attachment.contentType,
+ "application/octet-stream"
+ );
+ browser.test.assertEq("test", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(16, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[2].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test", file.name);
+ browser.test.assertEq(16, file.size);
+
+ browser.test.assertEq("binaryAttachment", await file.text());
+
+ reader = new FileReader();
+ data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+
+ browser.test.assertEq(
+ "data:application/octet-stream;base64,YmluYXJ5QXR0YWNobWVudA==",
+ data
+ );
+
+ // "2 attachments" message.
+
+ attachments = await browser.messages.listAttachments(messages[3].id);
+ browser.test.assertEq("2 attachments", messages[3].subject);
+ browser.test.assertEq(2, attachments.length);
+
+ attachment = attachments[0];
+ browser.test.assertEq(
+ attachment.contentType,
+ "application/octet-stream"
+ );
+ browser.test.assertEq("test", attachment.name);
+ browser.test.assertEq("1.2", attachment.partName);
+ browser.test.assertEq(16, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[3].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test", file.name);
+ browser.test.assertEq(16, file.size);
+
+ browser.test.assertEq("binaryAttachment", await file.text());
+
+ attachment = attachments[1];
+ browser.test.assertEq("text/plain", attachment.contentType);
+ browser.test.assertEq("test.txt", attachment.name);
+ browser.test.assertEq("1.3", attachment.partName);
+ browser.test.assertEq(14, attachment.size);
+
+ file = await browser.messages.getAttachmentFile(
+ messages[3].id,
+ attachment.partName
+ );
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq("test.txt", file.name);
+ browser.test.assertEq(14, file.size);
+
+ browser.test.assertEq("textAttachment", await file.text());
+
+ await browser.test.assertRejects(
+ browser.messages.listAttachments(0),
+ /^Message not found: \d+\.$/,
+ "Bad message ID should throw"
+ );
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(0, "1.2"),
+ /^Message not found: \d+\.$/,
+ "Bad message ID should throw"
+ );
+ browser.test.assertThrows(
+ () => browser.messages.getAttachmentFile(messages[3].id, "silly"),
+ /^Type error for parameter partName .* for messages\.getAttachmentFile\.$/,
+ "Bad part name should throw"
+ );
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(messages[3].id, "1.42"),
+ /Part 1.42 not found in message \d+\./,
+ "Non-existent part should throw"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => IS_IMAP,
+ },
+ async function test_messages_as_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let testFolder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(5, messages.length);
+ let message = messages[4];
+
+ function validateMessage(msg, expectedValues) {
+ for (let expectedValueName in expectedValues) {
+ let value = msg[expectedValueName];
+ let expected = expectedValues[expectedValueName];
+ if (Array.isArray(expected)) {
+ browser.test.assertTrue(
+ Array.isArray(value),
+ `Value for ${expectedValueName} should be an Array.`
+ );
+ browser.test.assertEq(
+ expected.length,
+ value.length,
+ `Value for ${expectedValueName} should have the correct Array size.`
+ );
+ for (let i = 0; i < expected.length; i++) {
+ browser.test.assertEq(
+ expected[i],
+ value[i],
+ `Value for ${expectedValueName}[${i}] should be correct.`
+ );
+ }
+ } else if (expected instanceof Date) {
+ browser.test.assertTrue(
+ value instanceof Date,
+ `Value for ${expectedValueName} should be a Date.`
+ );
+ browser.test.assertEq(
+ expected.getTime(),
+ value.getTime(),
+ `Date value for ${expectedValueName} should be correct.`
+ );
+ } else {
+ browser.test.assertEq(
+ expected,
+ value,
+ `Value for ${expectedValueName} should be correct.`
+ );
+ }
+ }
+ }
+
+ // Request attachments.
+ let attachments = await browser.messages.listAttachments(message.id);
+ browser.test.assertEq(2, attachments.length);
+ browser.test.assertEq("1.2", attachments[0].partName);
+ browser.test.assertEq("1.3", attachments[1].partName);
+
+ browser.test.assertEq("message1.eml", attachments[0].name);
+ browser.test.assertEq("yellowPixel.png", attachments[1].name);
+
+ // Validate the returned MessageHeader for attached message1.eml.
+ let subMessage = attachments[0].message;
+ browser.test.assertTrue(
+ subMessage.id != message.id,
+ `Id of attached SubMessage (${subMessage.id}) should be different from the id of the outer message (${message.id})`
+ );
+ validateMessage(subMessage, {
+ date: new Date(958606367000),
+ author: "Superman <clark.kent@dailyplanet.com>",
+ recipients: ["Jimmy <jimmy.olsen@dailyplanet.com>"],
+ ccList: [],
+ bccList: [],
+ subject: "Test message 1",
+ new: false,
+ headersOnly: false,
+ flagged: false,
+ junk: false,
+ junkScore: 0,
+ headerMessageId: "sample-attached.eml@mime.sample",
+ size: 0,
+ tags: [],
+ external: true,
+ });
+
+ // Get attachments of sub-message messag1.eml.
+ let subAttachments = await browser.messages.listAttachments(
+ subMessage.id
+ );
+ browser.test.assertEq(4, subAttachments.length);
+ browser.test.assertEq("1.2.1.2", subAttachments[0].partName);
+ browser.test.assertEq("1.2.1.3", subAttachments[1].partName);
+ browser.test.assertEq("1.2.1.4", subAttachments[2].partName);
+ browser.test.assertEq("1.2.1.5", subAttachments[3].partName);
+
+ browser.test.assertEq("whitePixel.png", subAttachments[0].name);
+ browser.test.assertEq("greenPixel.png", subAttachments[1].name);
+ browser.test.assertEq("redPixel.png", subAttachments[2].name);
+ browser.test.assertEq("message2.eml", subAttachments[3].name);
+
+ // Validate the returned MessageHeader for sub-message message2.eml
+ // attached to sub-message message1.eml.
+ let subSubMessage = subAttachments[3].message;
+ browser.test.assertTrue(
+ ![message.id, subMessage.id].includes(subSubMessage.id),
+ `Id of attached SubSubMessage (${subSubMessage.id}) should be different from the id of the outer message (${message.id}) and from the SubMessage (${subMessage.id})`
+ );
+ validateMessage(subSubMessage, {
+ date: new Date(958519967000),
+ author: "Jimmy <jimmy.olsen@dailyplanet.com>",
+ recipients: ["Superman <clark.kent@dailyplanet.com>"],
+ ccList: [],
+ bccList: [],
+ subject: "Test message 2",
+ new: false,
+ headersOnly: false,
+ flagged: false,
+ junk: false,
+ junkScore: 0,
+ headerMessageId: "sample-nested-attached.eml@mime.sample",
+ size: 0,
+ tags: [],
+ external: true,
+ });
+
+ // Test getAttachmentFile().
+ // Note: This function has x-ray vision into sub-messages and can get
+ // any part inside the message, even if - technically - the attachments
+ // belong to subMessages. There is no difference between requesting
+ // part 1.2.1.2 from the main message or from message1.eml (part 1.2).
+ // X-ray vision from a sub-message back into a parent is not allowed.
+ let platform = await browser.runtime.getPlatformInfo();
+ let fileTests = [
+ {
+ partName: "1.2",
+ name: "message1.eml",
+ size:
+ platform.os != "win" &&
+ (account.type == "none" || account.type == "nntp")
+ ? 2517
+ : 2601,
+ text: "Message-ID: <sample-attached.eml@mime.sample>",
+ },
+ {
+ partName: "1.2.1.2",
+ name: "whitePixel.png",
+ size: 69,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
+ },
+ {
+ partName: "1.2.1.3",
+ name: "greenPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+C76AoAAhUBJel4xsMAAAAASUVORK5CYII=",
+ },
+ {
+ partName: "1.2.1.4",
+ name: "redPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY+hgkAYAAbcApOp/9LEAAAAASUVORK5CYII=",
+ },
+ {
+ partName: "1.2.1.5",
+ name: "message2.eml",
+ size:
+ platform.os != "win" &&
+ (account.type == "none" || account.type == "nntp")
+ ? 838
+ : 867,
+ text: "Message-ID: <sample-nested-attached.eml@mime.sample>",
+ },
+ {
+ partName: "1.2.1.5.1.2",
+ name: "whitePixel.png",
+ size: 69,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC",
+ },
+ {
+ partName: "1.3",
+ name: "yellowPixel.png",
+ size: 119,
+ data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j/iQEABOUB8pypNlQAAAAASUVORK5CYII=",
+ },
+ ];
+ let testMessages = [
+ {
+ id: message.id,
+ expectedFileCounts: 7,
+ },
+ {
+ id: subMessage.id,
+ subPart: "1.2.",
+ expectedFileCounts: 5,
+ },
+ {
+ id: subSubMessage.id,
+ subPart: "1.2.1.5.",
+ expectedFileCounts: 1,
+ },
+ ];
+ for (let msg of testMessages) {
+ let fileCounts = 0;
+ for (let test of fileTests) {
+ if (msg.subPart && !test.partName.startsWith(msg.subPart)) {
+ await browser.test.assertRejects(
+ browser.messages.getAttachmentFile(msg.id, test.partName),
+ `Part ${test.partName} not found in message ${msg.id}.`,
+ "Sub-message should not be able to get parts from parent message"
+ );
+ continue;
+ }
+ fileCounts++;
+
+ let file = await browser.messages.getAttachmentFile(
+ msg.id,
+ test.partName
+ );
+
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(file instanceof File);
+ browser.test.assertEq(test.name, file.name);
+ browser.test.assertEq(test.size, file.size);
+
+ if (test.text) {
+ browser.test.assertTrue(
+ (await file.text()).startsWith(test.text)
+ );
+ }
+
+ if (test.data) {
+ let reader = new FileReader();
+ let data = await new Promise(resolve => {
+ reader.onload = e => resolve(e.target.result);
+ reader.readAsDataURL(file);
+ });
+ browser.test.assertEq(
+ test.data,
+ data.replaceAll("\r\n", "\n").trim()
+ );
+ }
+ }
+ browser.test.assertEq(
+ msg.expectedFileCounts,
+ fileCounts,
+ "Should have requested to correct amount of attachment files."
+ );
+ }
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js
new file mode 100644
index 0000000000..2872a2141f
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js
@@ -0,0 +1,1073 @@
+/* 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/. */
+
+"use strict";
+
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const OPENPGP_TEST_DIR = do_get_file("../../../../test/browser/openpgp");
+const OPENPGP_KEY_PATH = PathUtils.join(
+ OPENPGP_TEST_DIR.path,
+ "data",
+ "keys",
+ "alice@openpgp.example-0xf231550c4f47e38e-secret.asc"
+);
+
+/**
+ * Test the messages.getRaw and messages.getFull functions. Since each message
+ * is unique and there are minor differences between the account
+ * implementations, we don't compare exactly with a reference message.
+ */
+add_task(async function test_plain_mv2() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(_folder, 1);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+
+ // Expected message content:
+ // -------------------------
+ // From andy@anway.invalid
+ // Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+ // Subject: Big Meeting Today
+ // From: "Andy Anway" <andy@anway.invalid>
+ // To: "Bob Bell" <bob@bell.invalid>
+ // Message-Id: <0@made.up.invalid>
+ // Date: Wed, 06 Nov 2019 22:37:40 +1300
+ //
+ // Hello Bob Bell!
+ //
+
+ browser.test.assertEq("Big Meeting Today", message.subject);
+ browser.test.assertEq(
+ '"Andy Anway" <andy@anway.invalid>',
+ message.author
+ );
+
+ // The msgHdr of NNTP messages have no recipients.
+ if (account.type != "nntp") {
+ browser.test.assertEq(
+ "Bob Bell <bob@bell.invalid>",
+ message.recipients[0]
+ );
+ }
+
+ let strMessage_1 = await browser.messages.getRaw(message.id);
+ browser.test.assertEq("string", typeof strMessage_1);
+ let strMessage_2 = await browser.messages.getRaw(message.id, {
+ data_format: "BinaryString",
+ });
+ browser.test.assertEq("string", typeof strMessage_2);
+ let fileMessage_3 = await browser.messages.getRaw(message.id, {
+ data_format: "File",
+ });
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_3 instanceof File);
+ // Since we do not have utf-8 chars in the test message, the returned BinaryString is
+ // identical to the return value of File.text().
+ let strMessage_3 = await fileMessage_3.text();
+
+ for (let strMessage of [strMessage_1, strMessage_2, strMessage_3]) {
+ // Fold Windows line-endings \r\n to \n.
+ strMessage = strMessage.replace(/\r/g, "");
+ browser.test.assertTrue(
+ strMessage.includes("Subject: Big Meeting Today\n")
+ );
+ browser.test.assertTrue(
+ strMessage.includes('From: "Andy Anway" <andy@anway.invalid>\n')
+ );
+ browser.test.assertTrue(
+ strMessage.includes('To: "Bob Bell" <bob@bell.invalid>\n')
+ );
+ browser.test.assertTrue(strMessage.includes("Hello Bob Bell!"));
+ }
+
+ // {
+ // "contentType": "message/rfc822",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"],
+ // "subject": ["Big Meeting Today"],
+ // "from": ["\"Andy Anway\" <andy@anway.invalid>"],
+ // "to": ["\"Bob Bell\" <bob@bell.invalid>"],
+ // "message-id": ["<0@made.up.invalid>"],
+ // "date": ["Wed, 06 Nov 2019 22:37:40 +1300"]
+ // },
+ // "partName": "",
+ // "size": 17,
+ // "parts": [
+ // {
+ // "body": "Hello Bob Bell!\n\n",
+ // "contentType": "text/plain",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"]
+ // },
+ // "partName": "1",
+ // "size": 17
+ // }
+ // ]
+ // }
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq(
+ "Big Meeting Today",
+ fullMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ '"Andy Anway" <andy@anway.invalid>',
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ '"Bob Bell" <bob@bell.invalid>',
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+ browser.test.assertEq("object", typeof fullMessage.parts[0]);
+ browser.test.assertEq(
+ "Hello Bob Bell!",
+ fullMessage.parts[0].body.trimRight()
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: { permissions: ["accountsRead", "messagesRead"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+add_task(async function test_plain_mv3() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+ await createMessages(_folder, 1);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+
+ // Expected message content:
+ // -------------------------
+ // From chris@clarke.invalid
+ // Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+ // Subject: Small Party Tomorrow
+ // From: "Chris Clarke" <chris@clarke.invalid>
+ // To: "David Davol" <david@davol.invalid>
+ // Message-Id: <1@made.up.invalid>
+ // Date: Tue, 01 Feb 2000 01:00:00 +0100
+ //
+ // Hello David Davol!
+ //
+
+ browser.test.assertEq("Small Party Tomorrow", message.subject);
+ browser.test.assertEq(
+ '"Chris Clarke" <chris@clarke.invalid>',
+ message.author
+ );
+
+ // The msgHdr of NNTP messages have no recipients.
+ if (account.type != "nntp") {
+ browser.test.assertEq(
+ "David Davol <david@davol.invalid>",
+ message.recipients[0]
+ );
+ }
+
+ let fileMessage_1 = await browser.messages.getRaw(message.id);
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_1 instanceof File);
+ // Since we do not have utf-8 chars in the test message, the returned
+ // BinaryString is identical to the return value of File.text().
+ let strMessage_1 = await fileMessage_1.text();
+
+ let strMessage_2 = await browser.messages.getRaw(message.id, {
+ data_format: "BinaryString",
+ });
+ browser.test.assertEq("string", typeof strMessage_2);
+
+ let fileMessage_3 = await browser.messages.getRaw(message.id, {
+ data_format: "File",
+ });
+ // eslint-disable-next-line mozilla/use-isInstance
+ browser.test.assertTrue(fileMessage_3 instanceof File);
+ let strMessage_3 = await fileMessage_3.text();
+
+ for (let strMessage of [strMessage_1, strMessage_2, strMessage_3]) {
+ // Fold Windows line-endings \r\n to \n.
+ strMessage = strMessage.replace(/\r/g, "");
+ browser.test.assertTrue(
+ strMessage.includes("Subject: Small Party Tomorrow\n")
+ );
+ browser.test.assertTrue(
+ strMessage.includes('From: "Chris Clarke" <chris@clarke.invalid>\n')
+ );
+ browser.test.assertTrue(
+ strMessage.includes('To: "David Davol" <david@davol.invalid>\n')
+ );
+ browser.test.assertTrue(strMessage.includes("Hello David Davol!"));
+ }
+
+ // {
+ // "contentType": "message/rfc822",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"],
+ // "subject": ["Small Party Tomorrow"],
+ // "from": ["\"Chris Clarke\" <chris@clarke.invalid>"],
+ // "to": ["\"David Davol\" <David Davol>"],
+ // "message-id": ["<1@made.up.invalid>"],
+ // "date": ["Tue, 01 Feb 2000 01:00:00 +0100"]
+ // },
+ // "partName": "",
+ // "size": 20,
+ // "parts": [
+ // {
+ // "body": "David Davol!\n\n",
+ // "contentType": "text/plain",
+ // "headers": {
+ // "content-type": ["text/plain; charset=ISO-8859-1; format=flowed"]
+ // },
+ // "partName": "1",
+ // "size": 20
+ // }
+ // ]
+ // }
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq(
+ "Small Party Tomorrow",
+ fullMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ '"Chris Clarke" <chris@clarke.invalid>',
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ '"David Davol" <david@davol.invalid>',
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+ browser.test.assertEq("object", typeof fullMessage.parts[0]);
+ browser.test.assertEq(
+ "Hello David Davol!",
+ fullMessage.parts[0].body.trimRight()
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: {
+ manifest_version: 3,
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+/**
+ * Test that mime parsers for all message types retrieve the correctly decoded
+ * headers and bodies. Bodies should no not be returned, if it is an attachment.
+ * Sizes are not checked for.
+ */
+add_task(async function test_encoding() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ // Main body with disposition inline, base64 encoded,
+ // subject is UTF-8 encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample01.eml").path
+ );
+ // A multipart/mixed mime message, to header is iso-8859-1 encoded word,
+ // body is quoted printable with iso-8859-1, attachments with different names
+ // and filenames.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample02.eml").path
+ );
+ // Message with attachment only, From header is iso-8859-1 encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample03.eml").path
+ );
+ // Message with koi8-r + base64 encoded body, subject is koi8-r encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample04.eml").path
+ );
+ // Message with windows-1251 + base64 encoded body, subject is windows-1251
+ // encoded word.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample05.eml").path
+ );
+ // Message without plain/text content-type.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample06.eml").path
+ );
+ // A multipart/alternative message without plain/text content-type.
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/sample07.eml").path
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ let expectedData = {
+ "01.eml@mime.sample": {
+ msgHeaders: {
+ subject: "αλφάβητο",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["αλφάβητο"],
+ date: ["Thu, 27 May 2021 21:23:35 +0100"],
+ "message-id": ["<01.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=utf-8;"],
+ "content-transfer-encoding": ["base64"],
+ "content-disposition": ["inline"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Άλφα\n",
+ headers: {
+ "content-type": ["text/plain; charset=utf-8;"],
+ },
+ },
+ ],
+ },
+ },
+ "02.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Test message from Microsoft Outlook 00",
+ author: '"Doug Sauder" <doug@example.com>',
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ['"Doug Sauder" <doug@example.com>'],
+ to: ["Heinz Müller <mueller@example.com>"],
+ subject: ["Test message from Microsoft Outlook 00"],
+ date: ["Wed, 17 May 2000 19:32:47 -0400"],
+ "message-id": ["<02.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": [
+ 'multipart/mixed; boundary="----=_NextPart_000_0002_01BFC036.AE309650"',
+ ],
+ "x-priority": ["3 (Normal)"],
+ "x-msmail-priority": ["Normal"],
+ "x-mailer": [
+ "Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)",
+ ],
+ importance: ["Normal"],
+ "x-mimeole": ["Produced By Microsoft MimeOLE V5.00.2314.1300"],
+ },
+ parts: [
+ {
+ contentType: "multipart/mixed",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": [
+ 'multipart/mixed; boundary="----=_NextPart_000_0002_01BFC036.AE309650"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1.1",
+ size: 0,
+ body: `\nDie Hasen und die Frösche \n \n`,
+ headers: {
+ "content-type": ['text/plain; charset="iso-8859-1"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.2",
+ size: 0,
+ name: "blueball2.png",
+ headers: {
+ "content-type": ['image/png; name="blueball1.png"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.3",
+ size: 0,
+ name: "greenball.png",
+ headers: {
+ "content-type": ['image/png; name="greenball.png"'],
+ },
+ },
+ {
+ contentType: "image/png",
+ partName: "1.4",
+ size: 0,
+ name: "redball.png",
+ headers: {
+ "content-type": ["image/png"],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ "03.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Test message from Microsoft Outlook 00",
+ author: "Heinz Müller <mueller@example.com>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Heinz Müller <mueller@example.com>"],
+ to: ['"Joe Blow" <jblow@example.com>'],
+ subject: ["Test message from Microsoft Outlook 00"],
+ date: ["Wed, 17 May 2000 19:35:05 -0400"],
+ "message-id": ["<03.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ['image/png; name="doubelspace ball.png"'],
+ "content-transfer-encoding": ["base64"],
+ "content-disposition": [
+ 'attachment; filename="doubelspace ball.png"',
+ ],
+ "x-priority": ["3 (Normal)"],
+ "x-msmail-priority": ["Normal"],
+ "x-mailer": [
+ "Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)",
+ ],
+ importance: ["Normal"],
+ "x-mimeole": ["Produced By Microsoft MimeOLE V5.00.2314.1300"],
+ },
+ parts: [
+ {
+ contentType: "image/png",
+ name: "doubelspace ball.png",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": ['image/png; name="doubelspace ball.png"'],
+ },
+ },
+ ],
+ },
+ },
+ "04.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Алфавит",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["Алфавит"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<04.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=koi8-r;"],
+ "content-transfer-encoding": ["base64"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Вопрос\n",
+ headers: {
+ "content-type": ["text/plain; charset=koi8-r;"],
+ },
+ },
+ ],
+ },
+ },
+ "05.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Алфавит",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["Алфавит"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<05.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": ["text/plain; charset=windows-1251;"],
+ "content-transfer-encoding": ["base64"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "Вопрос\n",
+ headers: {
+ "content-type": ["text/plain; charset=windows-1251;"],
+ },
+ },
+ ],
+ },
+ },
+ "06.eml@mime.sample": {
+ msgHeaders: {
+ subject: "I have no content type",
+ author: "Bug Reporter <new@thunderbird.bug>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Bug Reporter <new@thunderbird.bug>"],
+ newsgroups: ["gmane.comp.mozilla.thundebird.user"],
+ subject: ["I have no content type"],
+ date: ["Sun, 27 May 2001 21:23:35 +0100"],
+ "message-id": ["<06.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1",
+ size: 0,
+ body: "No content type\n",
+ headers: {
+ "content-type": ["text/plain"],
+ },
+ },
+ ],
+ },
+ },
+ "07.eml@mime.sample": {
+ msgHeaders: {
+ subject: "Default content-types",
+ author: "Doug Sauder <dwsauder@example.com>",
+ },
+ msgParts: {
+ contentType: "message/rfc822",
+ partName: "",
+ size: 0,
+ headers: {
+ from: ["Doug Sauder <dwsauder@example.com>"],
+ to: ["Heinz <mueller@example.com>"],
+ subject: ["Default content-types"],
+ date: ["Fri, 19 May 2000 00:29:55 -0400"],
+ "message-id": ["<07.eml@mime.sample>"],
+ "mime-version": ["1.0"],
+ "content-type": [
+ 'multipart/alternative; boundary="=====================_714967308==_.ALT"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "multipart/alternative",
+ partName: "1",
+ size: 0,
+ headers: {
+ "content-type": [
+ 'multipart/alternative; boundary="=====================_714967308==_.ALT"',
+ ],
+ },
+ parts: [
+ {
+ contentType: "text/plain",
+ partName: "1.1",
+ size: 0,
+ body: "Die Hasen\n",
+ headers: {
+ "content-type": ["text/plain"],
+ },
+ },
+ {
+ contentType: "text/html",
+ partName: "1.2",
+ size: 0,
+ body: "<html><body><b>Die Hasen</b></body></html>\n",
+ headers: {
+ "content-type": ["text/html"],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ };
+
+ function checkMsgHeaders(expected, actual) {
+ // Check if all expected properties are there.
+ for (let property of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `expected property ${property} is present`
+ );
+ // Check property content.
+ browser.test.assertEq(
+ expected[property],
+ actual[property],
+ `property ${property} is correct`
+ );
+ }
+ }
+
+ function checkMsgParts(expected, actual) {
+ // Check if all expected properties are there.
+ for (let property of Object.keys(expected)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `expected property ${property} is present`
+ );
+ if (
+ ["parts", "headers", "size"].includes(property) ||
+ (["body"].includes(property) && expected[property] == "")
+ ) {
+ continue;
+ }
+ // Check property content.
+ browser.test.assertEq(
+ JSON.stringify(expected[property].replaceAll("\r\n", "\n")),
+ JSON.stringify(actual[property].replaceAll("\r\n", "\n")),
+ `property ${property} is correct`
+ );
+ }
+
+ // Check for unexpected properties.
+ for (let property of Object.keys(actual)) {
+ browser.test.assertEq(
+ expected.hasOwnProperty(property),
+ actual.hasOwnProperty(property),
+ `property ${property} is expected`
+ );
+ }
+
+ // Check if all expected headers are there.
+ if (expected.headers) {
+ for (let header of Object.keys(expected.headers)) {
+ browser.test.assertEq(
+ expected.headers.hasOwnProperty(header),
+ actual.headers.hasOwnProperty(header),
+ `expected header ${header} is present`
+ );
+ // Check header content.
+ // Note: jsmime does not eat TABs after a CLRF.
+ browser.test.assertEq(
+ expected.headers[header].toString().replaceAll("\t", " "),
+ actual.headers[header].toString().replaceAll("\t", " "),
+ `header ${header} is correct`
+ );
+ }
+ // Check for unexpected headers.
+ for (let header of Object.keys(actual.headers)) {
+ browser.test.assertEq(
+ expected.headers.hasOwnProperty(header),
+ actual.headers.hasOwnProperty(header),
+ `header ${header} is expected`
+ );
+ }
+ }
+
+ // Check sub-parts.
+ browser.test.assertEq(
+ Array.isArray(expected.parts),
+ Array.isArray(actual.parts),
+ `has sub-parts`
+ );
+ if (Array.isArray(expected.parts)) {
+ browser.test.assertEq(
+ expected.parts.length,
+ actual.parts.length,
+ "number of parts"
+ );
+ for (let i in expected.parts) {
+ checkMsgParts(expected.parts[i], actual.parts[i]);
+ }
+ }
+ }
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(7, messages.length);
+
+ for (let message of messages) {
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.assertEq("object", typeof fullMessage);
+
+ let expected = expectedData[message.headerMessageId];
+ checkMsgHeaders(expected.msgHeaders, message);
+ checkMsgParts(expected.msgParts, fullMessage);
+ }
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ manifest: { permissions: ["accountsRead", "messagesRead"] },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_openpgp() {
+ let _account = createAccount();
+ let _identity = addIdentity(_account);
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ // Load an encrypted message.
+
+ let messagePath = PathUtils.join(
+ OPENPGP_TEST_DIR.path,
+ "data",
+ "eml",
+ "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330.eml"
+ );
+ await createMessageFromFile(_folder, messagePath);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [account] = await browser.accounts.list();
+ let folder = account.folders.find(f => f.name == "test1");
+
+ // Read the message, without the key set up. The headers should be
+ // readable, but not the message itself.
+
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let [message] = messages;
+ browser.test.assertEq("...", message.subject);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ message.author
+ );
+ browser.test.assertEq("alice@openpgp.example", message.recipients[0]);
+
+ let fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq("...", fullMessage.headers.subject[0]);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ "alice@openpgp.example",
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+
+ let part = fullMessage.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/encrypted", part.contentType);
+ browser.test.assertEq(undefined, part.parts);
+
+ // Now set up the key and read the message again. It should all be
+ // there this time.
+
+ await window.sendMessage("load key");
+
+ ({ messages } = await browser.messages.list(folder));
+ browser.test.assertEq(1, messages.length);
+ [message] = messages;
+ browser.test.assertEq("...", message.subject);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ message.author
+ );
+ browser.test.assertEq("alice@openpgp.example", message.recipients[0]);
+
+ fullMessage = await browser.messages.getFull(message.id);
+ browser.test.log(JSON.stringify(fullMessage));
+ browser.test.assertEq("object", typeof fullMessage);
+ browser.test.assertEq("message/rfc822", fullMessage.contentType);
+
+ browser.test.assertEq("object", typeof fullMessage.headers);
+ for (let header of [
+ "content-type",
+ "date",
+ "from",
+ "message-id",
+ "subject",
+ "to",
+ ]) {
+ browser.test.assertTrue(Array.isArray(fullMessage.headers[header]));
+ browser.test.assertEq(1, fullMessage.headers[header].length);
+ }
+ browser.test.assertEq("...", fullMessage.headers.subject[0]);
+ browser.test.assertEq(
+ "Bob Babbage <bob@openpgp.example>",
+ fullMessage.headers.from[0]
+ );
+ browser.test.assertEq(
+ "alice@openpgp.example",
+ fullMessage.headers.to[0]
+ );
+
+ browser.test.assertTrue(Array.isArray(fullMessage.parts));
+ browser.test.assertEq(1, fullMessage.parts.length);
+
+ part = fullMessage.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/encrypted", part.contentType);
+ browser.test.assertTrue(Array.isArray(part.parts));
+ browser.test.assertEq(1, part.parts.length);
+
+ part = part.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("multipart/fake-container", part.contentType);
+ browser.test.assertTrue(Array.isArray(part.parts));
+ browser.test.assertEq(1, part.parts.length);
+
+ part = part.parts[0];
+ browser.test.assertEq("object", typeof part);
+ browser.test.assertEq("text/plain", part.contentType);
+ browser.test.assertEq(
+ "Sundays are nothing without callaloo.",
+ part.body.trimRight()
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("load key");
+ info(`Adding key from ${OPENPGP_KEY_PATH}`);
+ await OpenPGPTestUtils.initOpenPGP();
+ let [id] = await OpenPGPTestUtils.importPrivateKey(
+ null,
+ new FileUtils.File(OPENPGP_KEY_PATH)
+ );
+ _identity.setUnicharAttribute("openpgp_key_id", id);
+ extension.sendMessage();
+
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+ }
+);
+
+add_task(async function test_attached_message_with_missing_headers() {
+ let _account = createAccount();
+ let _folder = await createSubfolder(
+ _account.incomingServer.rootFolder,
+ "test1"
+ );
+
+ await createMessageFromFile(
+ _folder,
+ do_get_file("messages/attachedMessageWithMissingHeaders.eml").path
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+
+ for (let account of accounts) {
+ let folder = account.folders.find(f => f.name == "test1");
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(1, messages.length);
+
+ let msg = messages[0];
+ let attachments = await browser.messages.listAttachments(msg.id);
+ browser.test.assertEq(
+ attachments.length,
+ 1,
+ "Should have found the correct number of attachments"
+ );
+
+ let attachedMessage = attachments[0].message;
+ browser.test.assertTrue(
+ !!attachedMessage,
+ "Should have found an attached message"
+ );
+ browser.test.assertEq(
+ attachedMessage.date.getTime(),
+ 0,
+ "The date should be correct"
+ );
+ browser.test.assertEq(
+ attachedMessage.subject,
+ "",
+ "The subject should be empty"
+ );
+ browser.test.assertEq(
+ attachedMessage.author,
+ "",
+ "The author should be empty"
+ );
+ browser.test.assertEq(
+ attachedMessage.headerMessageId,
+ "sample-attached.eml@mime.sample",
+ "The headerMessageId should be correct"
+ );
+ window.assertDeepEqual(
+ attachedMessage.recipients,
+ [],
+ "The recipients should be correct"
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js
new file mode 100644
index 0000000000..dac01fa514
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js
@@ -0,0 +1,256 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var subFolders;
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function setup() {
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ subFolders = {
+ test1: await createSubfolder(rootFolder, "test1"),
+ test2: await createSubfolder(rootFolder, "test2"),
+ test3: await createSubfolder(rootFolder, "test3"),
+ attachment: await createSubfolder(rootFolder, "attachment"),
+ };
+ await createMessages(subFolders.test1, 5);
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+ await createMessages(subFolders.attachment, {
+ count: 1,
+ subject: "Msg with text attachment",
+ attachments: [textAttachment],
+ });
+ }
+);
+
+// In this test we'll move and copy some messages around between
+// folders. Every operation should result in the message's id property
+// changing to a never-seen-before value.
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_identifiers() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let [{ folders }] = await browser.accounts.list();
+ let testFolder1 = folders.find(f => f.name == "test1");
+ let testFolder2 = folders.find(f => f.name == "test2");
+ let testFolder3 = folders.find(f => f.name == "test3");
+
+ let { messages } = await browser.messages.list(testFolder1);
+ browser.test.assertEq(
+ 5,
+ messages.length,
+ "message count in testFolder1"
+ );
+ browser.test.assertEq(1, messages[0].id);
+ browser.test.assertEq(2, messages[1].id);
+ browser.test.assertEq(3, messages[2].id);
+ browser.test.assertEq(4, messages[3].id);
+ browser.test.assertEq(5, messages[4].id);
+
+ let subjects = messages.map(m => m.subject);
+
+ // Move two messages. We could do this in one operation, but to be
+ // sure of the order, do it in separate operations.
+
+ await browser.messages.move([1], testFolder2);
+ await browser.messages.move([3], testFolder2);
+
+ ({ messages } = await browser.messages.list(testFolder1));
+ browser.test.assertEq(
+ 3,
+ messages.length,
+ "message count in testFolder1"
+ );
+ browser.test.assertEq(2, messages[0].id);
+ browser.test.assertEq(4, messages[1].id);
+ browser.test.assertEq(5, messages[2].id);
+ browser.test.assertEq(subjects[1], messages[0].subject);
+ browser.test.assertEq(subjects[3], messages[1].subject);
+ browser.test.assertEq(subjects[4], messages[2].subject);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 2,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id, "new id created");
+ browser.test.assertEq(7, messages[1].id, "new id created");
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+
+ // Copy one message.
+
+ await browser.messages.copy([6], testFolder3);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 2,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id);
+ browser.test.assertEq(7, messages[1].id);
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+
+ ({ messages } = await browser.messages.list(testFolder3));
+ browser.test.assertEq(
+ 1,
+ messages.length,
+ "message count in testFolder3"
+ );
+ browser.test.assertEq(8, messages[0].id, "new id created");
+ browser.test.assertEq(subjects[0], messages[0].subject);
+
+ // Move the copied message back to the previous folder. There should
+ // now be two copies there, each with their own ID.
+
+ await browser.messages.move([8], testFolder2);
+
+ ({ messages } = await browser.messages.list(testFolder2));
+ browser.test.assertEq(
+ 3,
+ messages.length,
+ "message count in testFolder2"
+ );
+ browser.test.assertEq(6, messages[0].id);
+ browser.test.assertEq(7, messages[1].id);
+ browser.test.assertEq(
+ 9,
+ messages[2].id,
+ "new id created, not a duplicate"
+ );
+ browser.test.assertEq(subjects[0], messages[0].subject);
+ browser.test.assertEq(subjects[2], messages[1].subject);
+ browser.test.assertEq(
+ subjects[0],
+ messages[2].subject,
+ "same message as another in this folder"
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesMove", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
+
+// In this test we'll remove an attachment from a message and its id property
+// should not change. (Bug 1645595). Test does not work with IMAP test server,
+// which has issues with attachments.
+add_task(
+ {
+ skip_if: () => IS_NNTP || IS_IMAP,
+ },
+ async function test_attachments() {
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ let id;
+
+ browser.test.onMessage.addListener(async () => {
+ // This listener gets called once the attachment has been removed.
+ // Make sure we still get the message and it no longer has the
+ // attachment.
+ let modifiedMessage = await browser.messages.getFull(id);
+ browser.test.assertEq(
+ "Msg with text attachment",
+ modifiedMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ "text/x-moz-deleted",
+ modifiedMessage.parts[0].parts[1].contentType
+ );
+ browser.test.assertEq(
+ "Deleted: test.txt",
+ modifiedMessage.parts[0].parts[1].name
+ );
+ browser.test.notifyPass("finished");
+ });
+
+ let [{ folders }] = await browser.accounts.list();
+ let testFolder = folders.find(f => f.name == "attachment");
+ let { messages } = await browser.messages.list(testFolder);
+ browser.test.assertEq(1, messages.length);
+ id = messages[0].id;
+
+ let originalMessage = await browser.messages.getFull(id);
+ browser.test.assertEq(
+ "Msg with text attachment",
+ originalMessage.headers.subject[0]
+ );
+ browser.test.assertEq(
+ "text/plain",
+ originalMessage.parts[0].parts[1].contentType
+ );
+ browser.test.assertEq(
+ "test.txt",
+ originalMessage.parts[0].parts[1].name
+ );
+ browser.test.sendMessage("removeAttachment", id);
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ let observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "attachment-delete-msgkey-changed") {
+ extension.sendMessage();
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "attachment-delete-msgkey-changed");
+
+ extension.onMessage("removeAttachment", () => {
+ let msgHdr = subFolders.attachment.messages.getNext();
+ let msgUri = msgHdr.folder.getUriForMsg(msgHdr);
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ messenger.detachAttachment(
+ "text/plain",
+ `${msgUri}?part=1.2&filename=test.txt`,
+ "test.txt",
+ msgUri,
+ false /* do not save */,
+ true /* do not ask */
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js
new file mode 100644
index 0000000000..c3bef58835
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+add_task(async function test_import() {
+ let _account = createAccount();
+ await createSubfolder(_account.incomingServer.rootFolder, "test1");
+ await createSubfolder(_account.incomingServer.rootFolder, "test2");
+ await createSubfolder(_account.incomingServer.rootFolder, "test3");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ async function do_import(expected, file, folder, options) {
+ let msg = await browser.messages.import(file, folder, options);
+ browser.test.assertEq(
+ "alternative.eml@mime.sample",
+ msg.headerMessageId,
+ "should find the correct message after import"
+ );
+ let { messages } = await browser.messages.list(folder);
+ browser.test.assertEq(
+ 1,
+ messages.length,
+ "should find the imported message in the destination folder"
+ );
+ for (let [propName, value] of Object.entries(expected)) {
+ window.assertDeepEqual(
+ value,
+ messages[0][propName],
+ `Property ${propName} should be correct`
+ );
+ }
+ }
+
+ let accounts = await browser.accounts.list();
+ browser.test.assertEq(1, accounts.length);
+ let [account] = accounts;
+ let folder1 = account.folders.find(f => f.name == "test1");
+ let folder2 = account.folders.find(f => f.name == "test2");
+ let folder3 = account.folders.find(f => f.name == "test3");
+ browser.test.assertTrue(folder1, "Test folder should exist");
+ browser.test.assertTrue(folder2, "Test folder should exist");
+ browser.test.assertTrue(folder3, "Test folder should exist");
+
+ let [emlFileContent] = await window.sendMessage(
+ "getFileContent",
+ "messages/alternative.eml"
+ );
+ let file = new File([emlFileContent], "test.eml");
+
+ if (account.type == "nntp" || account.type == "imap") {
+ // nsIMsgCopyService.copyFileMessage() not implemented for NNTP.
+ // offline/online behavior of IMAP nsIMsgCopyService.copyFileMessage()
+ // is too erratic to be supported ATM.
+ await browser.test.assertRejects(
+ browser.messages.import(file, folder1),
+ `browser.messenger.import() is not supported for ${account.type} accounts`,
+ "Should throw for unsupported accounts"
+ );
+ } else {
+ await do_import(
+ {
+ new: false,
+ read: false,
+ flagged: false,
+ },
+ file,
+ folder1
+ );
+ await do_import(
+ {
+ new: true,
+ read: true,
+ flagged: true,
+ tags: ["$label1"],
+ },
+ file,
+ folder2,
+ {
+ new: true,
+ read: true,
+ flagged: true,
+ tags: ["$label1"],
+ }
+ );
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ },
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead", "messagesImport"],
+ },
+ });
+
+ extension.onMessage("getFileContent", async path => {
+ let raw = await IOUtils.read(do_get_file(path).path);
+ extension.sendMessage(MailStringUtils.uint8ArrayToByteString(raw));
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(_account);
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js
new file mode 100644
index 0000000000..81011374e3
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js
@@ -0,0 +1,656 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// Function to start an event page extension (MV3), which can be called whenever
+// the main test is about to trigger an event. The extension terminates its
+// background and listens for that single event, verifying it is waking up correctly.
+async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let _eventName = browser.runtime.getManifest().description;
+
+ browser.messages[_eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${_eventName} received`, args);
+ }
+ });
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_move_copy_delete() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let subFolders = {
+ test1: await createSubfolder(rootFolder, "test1"),
+ test2: await createSubfolder(rootFolder, "test2"),
+ test3: await createSubfolder(rootFolder, "test3"),
+ trash: rootFolder.getChildNamed("Trash"),
+ };
+ await createMessages(subFolders.trash, 4);
+ // 4 messages must be created before this line or test_move_copy_delete will break.
+ await createMessages(subFolders.test1, 5);
+
+ let files = {
+ "background.js": async () => {
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ async function checkMessagesInFolder(expectedKeys, folder) {
+ let expectedSubjects = expectedKeys.map(k => messages[k].subject);
+
+ let { messages: actualMessages } = await browser.messages.list(
+ folder
+ );
+ browser.test.log("expect: " + expectedSubjects.sort());
+ browser.test.log(
+ "actual: " + actualMessages.map(m => m.subject).sort()
+ );
+
+ browser.test.assertEq(
+ expectedSubjects.sort().toString(),
+ actualMessages
+ .map(m => m.subject)
+ .sort()
+ .toString(),
+ "Messages on server should be correct"
+ );
+ for (let m of actualMessages) {
+ browser.test.assertTrue(
+ expectedSubjects.includes(m.subject),
+ `${m.subject} at ${m.id}`
+ );
+ messages[m.subject.split(" ")[0]].id = m.id;
+ }
+
+ // Return the messages for convenience.
+ return actualMessages;
+ }
+
+ function newMovePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenSrcMsgs = [];
+ let seenDstMsgs = [];
+ const listener = (srcMsgs, dstMsgs) => {
+ seenEvents++;
+ seenSrcMsgs.push(...srcMsgs.messages);
+ seenDstMsgs.push(...dstMsgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onMoved.removeListener(listener);
+ resolve({ srcMsgs: seenSrcMsgs, dstMsgs: seenDstMsgs });
+ }
+ };
+ browser.messages.onMoved.addListener(listener);
+ });
+ }
+
+ function newCopyPromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenSrcMsgs = [];
+ let seenDstMsgs = [];
+ const listener = (srcMsgs, dstMsgs) => {
+ seenEvents++;
+ seenSrcMsgs.push(...srcMsgs.messages);
+ seenDstMsgs.push(...dstMsgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onCopied.removeListener(listener);
+ resolve({ srcMsgs: seenSrcMsgs, dstMsgs: seenDstMsgs });
+ }
+ };
+ browser.messages.onCopied.addListener(listener);
+ });
+ }
+
+ function newDeletePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = 0;
+ let seenMsgs = [];
+ const listener = msgs => {
+ seenEvents++;
+ seenMsgs.push(...msgs.messages);
+ if (seenEvents == numberOfEventsToCollapse) {
+ browser.messages.onDeleted.removeListener(listener);
+ resolve(seenMsgs);
+ }
+ };
+ browser.messages.onDeleted.addListener(listener);
+ });
+ }
+
+ async function checkEventInformation(
+ infoPromise,
+ expected,
+ messages,
+ dstFolder
+ ) {
+ let eventInfo = await infoPromise;
+ browser.test.assertEq(eventInfo.srcMsgs.length, expected.length);
+ browser.test.assertEq(eventInfo.dstMsgs.length, expected.length);
+ for (let msg of expected) {
+ let idx = eventInfo.srcMsgs.findIndex(
+ e => e.id == messages[msg].id
+ );
+ browser.test.assertEq(
+ eventInfo.srcMsgs[idx].subject,
+ messages[msg].subject
+ );
+ browser.test.assertEq(
+ eventInfo.dstMsgs[idx].subject,
+ messages[msg].subject
+ );
+ browser.test.assertEq(
+ eventInfo.dstMsgs[idx].folder.path,
+ dstFolder.path
+ );
+ }
+ }
+
+ let [accountId] = await window.sendMessage("getAccount");
+ let { folders } = await browser.accounts.get(accountId);
+ let testFolder1 = folders.find(f => f.name == "test1");
+ let testFolder2 = folders.find(f => f.name == "test2");
+ let testFolder3 = folders.find(f => f.name == "test3");
+ let trashFolder = folders.find(f => f.name == "Trash");
+
+ let { messages: folder1Messages } = await browser.messages.list(
+ testFolder1
+ );
+
+ // Since the ID of a message changes when it is moved, track by subject.
+ let messages = {};
+ for (let m of folder1Messages) {
+ messages[m.subject.split(" ")[0]] = { id: m.id, subject: m.subject };
+ }
+
+ // To help with debugging, output the IDs of our five messages.
+ browser.test.log(JSON.stringify(messages)); // Red:1, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move one message to another folder.");
+ let movePromise = newMovePromise();
+ let primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Red.id], testFolder2)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Red"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+
+ await checkMessagesInFolder(
+ ["Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder(["Red"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:6, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> And back again.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Red.id], testFolder1)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Red"],
+ messages,
+ testFolder1
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:2, Blue:3, My:4, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move two messages to another folder.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move(
+ [messages.Green.id, messages.My.id],
+ testFolder2
+ )
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder(["Green", "My"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:8, Blue:3, My:9, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move one back again: " + messages.My.id);
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.My.id], testFolder1)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(movePromise, ["My"], messages, testFolder1);
+
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder(["Green"], testFolder2);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:8, Blue:3, My:10, Happy:5
+
+ browser.test.log("");
+ browser.test.log(
+ " --> Move messages from different folders to a third folder."
+ );
+ // We collapse the two events (one for each source folder).
+ movePromise = newMovePromise(2);
+ await browser.messages.move(
+ [messages.Green.id, messages.My.id],
+ testFolder3
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder3
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ browser.test.log("");
+ browser.test.log(
+ " --> The following tests should not trigger move events."
+ );
+ let listenerCalls = 0;
+ const listenerFunc = () => {
+ listenerCalls++;
+ };
+ browser.messages.onMoved.addListener(listenerFunc);
+
+ // Move a message to the folder it's already in.
+ await browser.messages.move([messages.Green.id], testFolder3);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ // Move no messages.
+ await browser.messages.move([], testFolder3);
+ await checkMessagesInFolder(["Red", "Blue", "Happy"], testFolder1);
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder(["Green", "My"], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:11, Blue:3, My:12, Happy:5
+
+ // Move a non-existent message.
+ await browser.test.assertRejects(
+ browser.messages.move([9999], testFolder1),
+ /Error moving message/,
+ "something should happen"
+ );
+
+ // Move to a non-existent folder.
+ await browser.test.assertRejects(
+ browser.messages.move([messages.Red.id], {
+ accountId,
+ path: "/missing",
+ }),
+ /Error moving message/,
+ "something should happen"
+ );
+
+ // Check that no move event was triggered.
+ browser.messages.onMoved.removeListener(listenerFunc);
+ browser.test.assertEq(0, listenerCalls);
+
+ browser.test.log("");
+ browser.test.log(
+ " --> Put everything back where it was at the start of the test."
+ );
+ movePromise = newMovePromise();
+ await browser.messages.move(
+ [messages.My.id, messages.Green.id],
+ testFolder1
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green", "My"],
+ messages,
+ testFolder1
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Copy one message to another folder.");
+ let copyPromise = newCopyPromise();
+ let primedCopyInfo = await capturePrimedEvent("onCopied", () =>
+ browser.messages.copy([messages.Happy.id], testFolder2)
+ );
+ window.assertDeepEqual(
+ await copyPromise,
+ {
+ srcMsgs: primedCopyInfo[0].messages,
+ dstMsgs: primedCopyInfo[1].messages,
+ },
+ "The primed and non-primed onCopied events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ copyPromise,
+ ["Happy"],
+ messages,
+ testFolder2
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ let { messages: folder2Messages } = await browser.messages.list(
+ testFolder2
+ );
+ browser.test.assertEq(1, folder2Messages.length);
+ browser.test.assertEq(
+ messages.Happy.subject,
+ folder2Messages[0].subject
+ );
+ browser.test.assertTrue(folder2Messages[0].id != messages.Happy.id);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Delete the copied message.");
+ let deletePromise = newDeletePromise();
+ let primedDeleteLog = await capturePrimedEvent("onDeleted", () =>
+ browser.messages.delete([folder2Messages[0].id], true)
+ );
+ // Check if the delete information is correct.
+ let deleteLog = await deletePromise;
+ window.assertDeepEqual(
+ [
+ {
+ id: null,
+ messages: deleteLog,
+ },
+ ],
+ primedDeleteLog,
+ "The primed and non-primed onDeleted events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(1, deleteLog.length);
+ browser.test.assertEq(folder2Messages[0].id, deleteLog[0].id);
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ // Check if the message was deleted.
+ await checkMessagesInFolder(
+ ["Red", "Green", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+ browser.test.log(JSON.stringify(messages)); // Red:7, Green:13, Blue:3, My:14, Happy:5
+
+ browser.test.log("");
+ browser.test.log(" --> Move a message to the trash.");
+ movePromise = newMovePromise();
+ primedMoveInfo = await capturePrimedEvent("onMoved", () =>
+ browser.messages.move([messages.Green.id], trashFolder)
+ );
+ window.assertDeepEqual(
+ await movePromise,
+ {
+ srcMsgs: primedMoveInfo[0].messages,
+ dstMsgs: primedMoveInfo[1].messages,
+ },
+ "The primed and non-primed onMoved events should return the same values",
+ { strict: true }
+ );
+ await checkEventInformation(
+ movePromise,
+ ["Green"],
+ messages,
+ trashFolder
+ );
+
+ await window.sendMessage("forceServerUpdate", testFolder1.name);
+ await window.sendMessage("forceServerUpdate", testFolder2.name);
+ await window.sendMessage("forceServerUpdate", testFolder3.name);
+
+ await checkMessagesInFolder(
+ ["Red", "Blue", "My", "Happy"],
+ testFolder1
+ );
+ await checkMessagesInFolder([], testFolder2);
+ await checkMessagesInFolder([], testFolder3);
+
+ let { messages: trashFolderMessages } = await browser.messages.list(
+ trashFolder
+ );
+ browser.test.assertTrue(
+ trashFolderMessages.find(m => m.subject == messages.Green.subject)
+ );
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: [
+ "accountsRead",
+ "messagesMove",
+ "messagesRead",
+ "messagesDelete",
+ ],
+ browser_specific_settings: {
+ gecko: { id: "messages.move@mochi.test" },
+ },
+ },
+ });
+
+ extension.onMessage("forceServerUpdate", async foldername => {
+ if (IS_IMAP) {
+ let folder = rootFolder
+ .getChildNamed(foldername)
+ .QueryInterface(Ci.nsIMsgImapMailFolder);
+
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ folder.updateFolderWithListener(null, listener);
+ await listener.promise;
+
+ // ...and download for offline use.
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("capturePrimedEvent", async eventName => {
+ let primedEventData = await event_page_extension(eventName, () => {
+ // Resume execution in the main test, after the event page extension is
+ // ready to capture the event with deactivated background.
+ extension.sendMessage();
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ extension.onMessage("getAccount", () => {
+ extension.sendMessage(account.key);
+ });
+
+ // The sync between the IMAP Service and the fake IMAP Server is partially
+ // broken: It is not possible to re-move messages cleanly. The move commands
+ // are send to the server about 500ms after the local operation and the server
+ // will update the local state wrongly.
+ // In this test we enforce a server update after each operation. If this is
+ // still causing intermittent fails, enable the offline mode for this test.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1797764#c24
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js
new file mode 100644
index 0000000000..5c8e62872d
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js
@@ -0,0 +1,153 @@
+/* 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 { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// Function to start an event page extension (MV3), which can be called whenever
+// the main test is about to trigger an event. The extension terminates its
+// background and listens for that single event, verifying it is waking up correctly.
+async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let _eventName = browser.runtime.getManifest().description;
+
+ browser.messages[_eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${_eventName} received`, args);
+ }
+ });
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(async function () {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let inbox = await createSubfolder(account.incomingServer.rootFolder, "test1");
+
+ let files = {
+ "background.js": async () => {
+ browser.messages.onNewMailReceived.addListener((folder, messageList) => {
+ window.assertDeepEqual(
+ { accountId: "account1", name: "test1", path: "/test1" },
+ folder
+ );
+ browser.test.sendMessage("onNewMailReceived event received", [
+ folder,
+ messageList,
+ ]);
+ });
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+
+ // Create a new message.
+
+ await createMessages(inbox, 1);
+ inbox.hasNewMessages = true;
+ inbox.setNumNewMessages(1);
+ inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+
+ let inboxMessages = [...inbox.messages];
+ let newMessages = await extension.awaitMessage(
+ "onNewMailReceived event received"
+ );
+ equal(newMessages[1].messages.length, 1);
+ equal(newMessages[1].messages[0].subject, inboxMessages[0].subject);
+
+ // Create 2 more new messages.
+
+ let primedOnNewMailReceivedEventData = await event_page_extension(
+ "onNewMailReceived",
+ async () => {
+ await createMessages(inbox, 2);
+ inbox.hasNewMessages = true;
+ inbox.setNumNewMessages(2);
+ inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ }
+ );
+
+ inboxMessages = [...inbox.messages];
+ newMessages = await extension.awaitMessage(
+ "onNewMailReceived event received"
+ );
+ Assert.deepEqual(
+ primedOnNewMailReceivedEventData,
+ newMessages,
+ "The primed and non-primed onNewMailReceived events should return the same values"
+ );
+ equal(newMessages[1].messages.length, 2);
+ equal(newMessages[1].messages[0].subject, inboxMessages[1].subject);
+ equal(newMessages[1].messages[1].subject, inboxMessages[2].subject);
+
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
new file mode 100644
index 0000000000..9d9e5d8595
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
@@ -0,0 +1,333 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_query() {
+ let account = createAccount();
+
+ let textAttachment = {
+ body: "textAttachment",
+ filename: "test.txt",
+ contentType: "text/plain",
+ };
+
+ let subFolders = {
+ test1: await createSubfolder(account.incomingServer.rootFolder, "test1"),
+ test2: await createSubfolder(account.incomingServer.rootFolder, "test2"),
+ };
+ await createMessages(subFolders.test1, { count: 9, age_incr: { days: 2 } });
+
+ let messages = [...subFolders.test1.messages];
+ // NB: Here, the messages are zero-indexed. In the test they're one-indexed.
+ subFolders.test1.markMessagesRead([messages[0]], true);
+ subFolders.test1.markMessagesFlagged([messages[1]], true);
+ subFolders.test1.markMessagesFlagged([messages[6]], true);
+
+ subFolders.test1.addKeywordsToMessages(messages.slice(0, 1), "notATag");
+ subFolders.test1.addKeywordsToMessages(messages.slice(2, 4), "$label2");
+ subFolders.test1.addKeywordsToMessages(messages.slice(3, 6), "$label3");
+
+ addIdentity(account, messages[5].author.replace(/.*<(.*)>/, "$1"));
+ // No recipient support for NNTP.
+ if (account.incomingServer.type != "nntp") {
+ addIdentity(account, messages[2].recipients.replace(/.*<(.*)>/, "$1"));
+ }
+
+ await createMessages(subFolders.test2, { count: 7, age_incr: { days: 2 } });
+ // Email with multipart/alternative.
+ await createMessageFromFile(
+ subFolders.test2,
+ do_get_file("messages/alternative.eml").path
+ );
+
+ await createMessages(subFolders.test2, {
+ count: 1,
+ subject: "1 text attachment",
+ attachments: [textAttachment],
+ });
+
+ let files = {
+ "background.js": async () => {
+ let [accountId] = await window.waitForMessage();
+ let _account = await browser.accounts.get(accountId);
+ let accountType = _account.type;
+
+ let messages1 = await browser.messages.list({
+ accountId,
+ path: "/test1",
+ });
+ browser.test.assertEq(9, messages1.messages.length);
+ let messages2 = await browser.messages.list({
+ accountId,
+ path: "/test2",
+ });
+ browser.test.assertEq(9, messages2.messages.length);
+
+ // Check all messages are returned.
+ let { messages: allMessages } = await browser.messages.query({});
+ browser.test.assertEq(18, allMessages.length);
+
+ let folder1 = { accountId, path: "/test1" };
+ let folder2 = { accountId, path: "/test2" };
+ let rootFolder = { accountId, path: "/" };
+
+ // Query messages from test1. No messages from test2 should be returned.
+ // We'll use these messages as a reference for further tests.
+ let { messages: referenceMessages } = await browser.messages.query({
+ folder: folder1,
+ });
+ browser.test.assertEq(9, referenceMessages.length);
+ browser.test.assertTrue(
+ referenceMessages.every(m => m.folder.path == "/test1")
+ );
+
+ // Test includeSubFolders: Default (False).
+ let { messages: searchRecursiveDefault } = await browser.messages.query({
+ folder: rootFolder,
+ });
+ browser.test.assertEq(
+ 0,
+ searchRecursiveDefault.length,
+ "includeSubFolders: Default"
+ );
+
+ // Test includeSubFolders: True.
+ let { messages: searchRecursiveTrue } = await browser.messages.query({
+ folder: rootFolder,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(
+ 18,
+ searchRecursiveTrue.length,
+ "includeSubFolders: True"
+ );
+
+ // Test includeSubFolders: False.
+ let { messages: searchRecursiveFalse } = await browser.messages.query({
+ folder: rootFolder,
+ includeSubFolders: false,
+ });
+ browser.test.assertEq(
+ 0,
+ searchRecursiveFalse.length,
+ "includeSubFolders: False"
+ );
+
+ // Test attachment query: False.
+ let { messages: searchAttachmentFalse } = await browser.messages.query({
+ attachment: false,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(
+ 17,
+ searchAttachmentFalse.length,
+ "attachment: False"
+ );
+
+ // Test attachment query: True.
+ let { messages: searchAttachmentTrue } = await browser.messages.query({
+ attachment: true,
+ includeSubFolders: true,
+ });
+ browser.test.assertEq(1, searchAttachmentTrue.length, "attachment: True");
+
+ // Dump the reference messages to the console for easier debugging.
+ browser.test.log("Reference messages:");
+ for (let m of referenceMessages) {
+ let date = m.date.toISOString().substring(0, 10);
+ let author = m.author.replace(/"(.*)".*/, "$1").padEnd(16, " ");
+ // No recipient support for NNTP.
+ let recipients =
+ accountType == "nntp"
+ ? ""
+ : m.recipients[0].replace(/(.*) <.*>/, "$1").padEnd(16, " ");
+ browser.test.log(
+ `[${m.id}] ${date} From: ${author} To: ${recipients} Subject: ${m.subject}`
+ );
+ }
+
+ let subtest = async function (queryInfo, ...expectedMessageIndices) {
+ if (!queryInfo.folder) {
+ queryInfo.folder = folder1;
+ }
+ browser.test.log("Testing " + JSON.stringify(queryInfo));
+ let { messages: actualMessages } = await browser.messages.query(
+ queryInfo
+ );
+
+ browser.test.assertEq(
+ expectedMessageIndices.length,
+ actualMessages.length,
+ "Correct number of messages"
+ );
+ for (let index of expectedMessageIndices) {
+ // browser.test.log(`Looking for message ${index}`);
+ if (!actualMessages.some(am => am.id == index)) {
+ browser.test.fail(`Message ${index} was not returned`);
+ browser.test.log(
+ "These messages were returned: " + actualMessages.map(am => am.id)
+ );
+ }
+ }
+ };
+
+ // Date range query. The messages are 0 days old, 2 days old, 4 days old, etc..
+ let today = new Date();
+ let date1 = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 5
+ );
+ let date2 = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 11
+ );
+ await subtest({ fromDate: today });
+ await subtest({ fromDate: date1 }, 1, 2, 3);
+ await subtest({ fromDate: date2 }, 1, 2, 3, 4, 5, 6);
+ await subtest({ toDate: date1 }, 4, 5, 6, 7, 8, 9);
+ await subtest({ toDate: date2 }, 7, 8, 9);
+ await subtest({ fromDate: date1, toDate: date2 });
+ await subtest({ fromDate: date2, toDate: date1 }, 4, 5, 6);
+
+ // Unread query. Only message 1 has been read.
+ await subtest({ unread: false }, 1);
+ await subtest({ unread: true }, 2, 3, 4, 5, 6, 7, 8, 9);
+
+ // Flagged query. Messages 2 and 7 are flagged.
+ await subtest({ flagged: true }, 2, 7);
+ await subtest({ flagged: false }, 1, 3, 4, 5, 6, 8, 9);
+
+ // Subject query.
+ let keyword = referenceMessages[1].subject.split(" ")[1];
+ await subtest({ subject: keyword }, 2);
+ await subtest({ fullText: keyword }, 2);
+
+ // Author query.
+ keyword = referenceMessages[2].author.replace('"', "").split(" ")[0];
+ await subtest({ author: keyword }, 3);
+ await subtest({ fullText: keyword }, 3);
+
+ // Recipients query.
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ keyword = referenceMessages[7].recipients[0].split(" ")[0];
+ await subtest({ recipients: keyword }, 8);
+ await subtest({ fullText: keyword }, 8);
+ await subtest({ body: keyword }, 8);
+ }
+
+ // From Me and To Me. These use the identities added to account.
+ await subtest({ fromMe: true }, 6);
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ await subtest({ toMe: true }, 3);
+ }
+
+ // Tags query.
+ await subtest({ tags: { mode: "any", tags: { notATag: true } } });
+ await subtest({ tags: { mode: "any", tags: { $label2: true } } }, 3, 4);
+ await subtest(
+ { tags: { mode: "any", tags: { $label3: true } } },
+ 4,
+ 5,
+ 6
+ );
+ await subtest(
+ { tags: { mode: "any", tags: { $label2: true, $label3: true } } },
+ 3,
+ 4,
+ 5,
+ 6
+ );
+ await subtest({
+ tags: { mode: "all", tags: { $label1: true, $label2: true } },
+ });
+ await subtest(
+ { tags: { mode: "all", tags: { $label2: true, $label3: true } } },
+ 4
+ );
+ await subtest(
+ { tags: { mode: "any", tags: { $label2: false, $label3: false } } },
+ 1,
+ 2,
+ 7,
+ 8,
+ 9
+ );
+ await subtest(
+ { tags: { mode: "all", tags: { $label2: false, $label3: false } } },
+ 1,
+ 2,
+ 3,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ );
+
+ // headerMessageId query
+ await subtest({ headerMessageId: "0@made.up.invalid" }, 1);
+ await subtest({ headerMessageId: "7@made.up.invalid" }, 8);
+ await subtest({ headerMessageId: "8@made.up.invalid" }, 9);
+ await subtest({ headerMessageId: "unknown@made.up.invalid" });
+
+ // attachment query
+ await subtest({ folder: folder2, attachment: true }, 18);
+
+ // text in nested html part of multipart/alternative
+ await subtest({ folder: folder2, body: "I am HTML!" }, 17);
+
+ // No recipient support for NNTP.
+ if (accountType != "nntp") {
+ // advanced search on recipients
+ await subtest({ folder: folder2, recipients: "karl; heinz" }, 17);
+ await subtest(
+ { folder: folder2, recipients: "<friedrich@example.COM>; HEINZ" },
+ 17
+ );
+ await subtest(
+ {
+ folder: folder2,
+ recipients: "karl <friedrich@example.COM>; HEINZ",
+ },
+ 17
+ );
+ await subtest({
+ folder: folder2,
+ recipients: "Heinz <friedrich@example.COM>; Karl",
+ });
+ }
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ },
+ });
+
+ await extension.startup();
+ extension.sendMessage(account.key);
+ await extension.awaitFinish("finished");
+ await extension.unload();
+});
+
+registerCleanupFunction(() => {
+ // Make sure any open address book database is given a chance to close.
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+});
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js
new file mode 100644
index 0000000000..4771f3ee17
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js
@@ -0,0 +1,415 @@
+/* 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/. */
+
+"use strict";
+
+var { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ExtensionTestUtils.mockAppInfo();
+AddonTestUtils.maybeInit(this);
+
+registerCleanupFunction(async () => {
+ // Remove the temporary MozillaMailnews folder, which is not deleted in time when
+ // the cleanupFunction registered by AddonTestUtils.maybeInit() checks for left over
+ // files in the temp folder.
+ // Note: PathUtils.tempDir points to the system temp folder, which is different.
+ let path = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "MozillaMailnews"
+ );
+ await IOUtils.remove(path, { recursive: true });
+});
+
+// Function to start an event page extension (MV3), which can be called whenever
+// the main test is about to trigger an event. The extension terminates its
+// background and listens for that single event, verifying it is waking up correctly.
+async function event_page_extension(eventName, actionCallback) {
+ let ext = ExtensionTestUtils.loadExtension({
+ files: {
+ "background.js": async () => {
+ // Whenever the extension starts or wakes up, hasFired is set to false. In
+ // case of a wake-up, the first fired event is the one that woke up the background.
+ let hasFired = false;
+ let _eventName = browser.runtime.getManifest().description;
+
+ browser.messages[_eventName].addListener(async (...args) => {
+ // Only send the first event after background wake-up, this should
+ // be the only one expected.
+ if (!hasFired) {
+ hasFired = true;
+ browser.test.sendMessage(`${_eventName} received`, args);
+ }
+ });
+ browser.test.sendMessage("background started");
+ },
+ },
+ manifest: {
+ manifest_version: 3,
+ description: eventName,
+ background: { scripts: ["background.js"] },
+ browser_specific_settings: {
+ gecko: { id: "event_page_extension@mochi.test" },
+ },
+ permissions: ["accountsRead", "messagesRead", "messagesMove"],
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.terminateBackground({ disableResetIdleForTest: true });
+ // Verify the primed persistent listener.
+ assertPersistentListeners(ext, "messages", eventName, { primed: true });
+
+ await actionCallback();
+ let rv = await ext.awaitMessage(`${eventName} received`);
+ await ext.awaitMessage("background started");
+ // The listener should be persistent, but not primed.
+ assertPersistentListeners(ext, "messages", eventName, { primed: false });
+
+ await ext.unload();
+ return rv;
+}
+
+add_task(
+ {
+ skip_if: () => IS_NNTP,
+ },
+ async function test_update() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let account = createAccount();
+ let rootFolder = account.incomingServer.rootFolder;
+ let testFolder0 = await createSubfolder(rootFolder, "test0");
+ await createMessages(testFolder0, 1);
+ testFolder0.addKeywordsToMessages(
+ [[...testFolder0.messages][0]],
+ "testkeyword"
+ );
+
+ let files = {
+ "background.js": async () => {
+ async function capturePrimedEvent(eventName, callback) {
+ let eventPageExtensionReadyPromise = window.waitForMessage();
+ browser.test.sendMessage("capturePrimedEvent", eventName);
+ await eventPageExtensionReadyPromise;
+ let eventPageExtensionFinishedPromise = window.waitForMessage();
+ callback();
+ return eventPageExtensionFinishedPromise;
+ }
+
+ function newUpdatePromise(numberOfEventsToCollapse = 1) {
+ return new Promise(resolve => {
+ let seenEvents = {};
+ const listener = (msg, props) => {
+ if (!seenEvents.hasOwnProperty(msg.id)) {
+ seenEvents[msg.id] = {
+ counts: 0,
+ props: {},
+ };
+ }
+
+ seenEvents[msg.id].counts++;
+ for (let prop of Object.keys(props)) {
+ seenEvents[msg.id].props[prop] = props[prop];
+ }
+
+ if (seenEvents[msg.id].counts == numberOfEventsToCollapse) {
+ browser.messages.onUpdated.removeListener(listener);
+ resolve({ msg, props: seenEvents[msg.id].props });
+ }
+ };
+ browser.messages.onUpdated.addListener(listener);
+ });
+ }
+ let tags = await browser.messages.listTags();
+ let [data] = await window.sendMessage("getFolder");
+ let messageList = await browser.messages.list(data.folder);
+ browser.test.assertEq(1, messageList.messages.length);
+ let message = messageList.messages[0];
+ browser.test.assertFalse(message.flagged);
+ browser.test.assertFalse(message.read);
+ browser.test.assertFalse(message.junk);
+ browser.test.assertEq(0, message.junkScore);
+ browser.test.assertEq(0, message.tags.length);
+ browser.test.assertEq(data.size, message.size);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ // Test that setting flagged works.
+ let updatePromise = newUpdatePromise();
+ let primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { flagged: true })
+ );
+ let updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ flagged: true }, updateInfo.props);
+ await window.sendMessage("flagged");
+
+ // Test that setting read works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { read: true })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ read: true }, updateInfo.props);
+ await window.sendMessage("read");
+
+ // Test that setting junk works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { junk: true })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ junk: true }, updateInfo.props);
+ await window.sendMessage("junk");
+
+ // Test that setting one tag works.
+ updatePromise = newUpdatePromise();
+ primedUpdatedInfo = await capturePrimedEvent("onUpdated", () =>
+ browser.messages.update(message.id, { tags: [tags[0].key] })
+ );
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ [updateInfo.msg, updateInfo.props],
+ primedUpdatedInfo,
+ "The primed and non-primed onUpdated events should return the same values",
+ { strict: true }
+ );
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual({ tags: [tags[0].key] }, updateInfo.props);
+ await window.sendMessage("tags1");
+
+ // Test that setting two tags works. We get 3 events: one removing tags0,
+ // one adding tags1 and one adding tags2. updatePromise is waiting for
+ // the third one before resolving.
+ updatePromise = newUpdatePromise(3);
+ await browser.messages.update(message.id, {
+ tags: [tags[1].key, tags[2].key],
+ });
+ updateInfo = await updatePromise;
+ browser.test.assertEq(message.id, updateInfo.msg.id);
+ window.assertDeepEqual(
+ { tags: [tags[1].key, tags[2].key] },
+ updateInfo.props
+ );
+ await window.sendMessage("tags2");
+
+ // Test that unspecified properties aren't changed.
+ let listenerCalls = 0;
+ const listenerFunc = (msg, props) => {
+ listenerCalls++;
+ };
+ browser.messages.onUpdated.addListener(listenerFunc);
+ await browser.messages.update(message.id, {});
+ await window.sendMessage("empty");
+ // Check if the no-op update call triggered a listener.
+ await new Promise(resolve => setTimeout(resolve));
+ browser.messages.onUpdated.removeListener(listenerFunc);
+ browser.test.assertEq(
+ 0,
+ listenerCalls,
+ "Not expecting listener callbacks on no-op updates."
+ );
+
+ message = await browser.messages.get(message.id);
+ browser.test.assertTrue(message.flagged);
+ browser.test.assertTrue(message.read);
+ browser.test.assertTrue(message.junk);
+ browser.test.assertEq(100, message.junkScore);
+ browser.test.assertEq(2, message.tags.length);
+ browser.test.assertEq(tags[1].key, message.tags[0]);
+ browser.test.assertEq(tags[2].key, message.tags[1]);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ // Test that clearing properties works.
+ updatePromise = newUpdatePromise(5);
+ await browser.messages.update(message.id, {
+ flagged: false,
+ read: false,
+ junk: false,
+ tags: [],
+ });
+ updateInfo = await updatePromise;
+ window.assertDeepEqual(
+ {
+ flagged: false,
+ read: false,
+ junk: false,
+ tags: [],
+ },
+ updateInfo.props
+ );
+ await window.sendMessage("clear");
+
+ message = await browser.messages.get(message.id);
+ browser.test.assertFalse(message.flagged);
+ browser.test.assertFalse(message.read);
+ browser.test.assertFalse(message.external);
+ browser.test.assertFalse(message.junk);
+ browser.test.assertEq(0, message.junkScore);
+ browser.test.assertEq(0, message.tags.length);
+ browser.test.assertEq("0@made.up.invalid", message.headerMessageId);
+
+ browser.test.notifyPass("finished");
+ },
+ "utils.js": await getUtilsJS(),
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ files,
+ manifest: {
+ background: { scripts: ["utils.js", "background.js"] },
+ permissions: ["accountsRead", "messagesRead"],
+ browser_specific_settings: {
+ gecko: { id: "messages.update@mochi.test" },
+ },
+ },
+ });
+
+ let message = [...testFolder0.messages][0];
+ ok(!message.isFlagged);
+ ok(!message.isRead);
+ equal(message.getStringProperty("keywords"), "testkeyword");
+
+ extension.onMessage("capturePrimedEvent", async eventName => {
+ let primedEventData = await event_page_extension(eventName, () => {
+ // Resume execution in the main test, after the event page extension is
+ // ready to capture the event with deactivated background.
+ extension.sendMessage();
+ });
+ extension.sendMessage(...primedEventData);
+ });
+
+ extension.onMessage("flagged", async () => {
+ await TestUtils.waitForCondition(() => message.isFlagged);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("read", async () => {
+ await TestUtils.waitForCondition(() => message.isRead);
+ extension.sendMessage();
+ });
+
+ extension.onMessage("junk", async () => {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("junkscore") == 100
+ );
+ extension.sendMessage();
+ });
+
+ extension.onMessage("tags1", async () => {
+ if (IS_IMAP) {
+ // Only IMAP sets the junk/nonjunk keyword.
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") == "testkeyword junk $label1"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword $label1"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("tags2", async () => {
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword junk $label2 $label3"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword $label2 $label3"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("empty", async () => {
+ await TestUtils.waitForCondition(() => message.isFlagged);
+ await TestUtils.waitForCondition(() => message.isRead);
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword junk $label2 $label3"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () =>
+ message.getStringProperty("keywords") ==
+ "testkeyword $label2 $label3"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("clear", async () => {
+ await TestUtils.waitForCondition(() => !message.isFlagged);
+ await TestUtils.waitForCondition(() => !message.isRead);
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("junkscore") == 0
+ );
+ if (IS_IMAP) {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword nonjunk"
+ );
+ } else {
+ await TestUtils.waitForCondition(
+ () => message.getStringProperty("keywords") == "testkeyword"
+ );
+ }
+ extension.sendMessage();
+ });
+
+ extension.onMessage("getFolder", async () => {
+ extension.sendMessage({
+ folder: { accountId: account.key, path: "/test0" },
+ size: message.messageSize,
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("finished");
+ await extension.unload();
+
+ cleanUpAccount(account);
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini
new file mode 100644
index 0000000000..88659a20ad
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini
@@ -0,0 +1,7 @@
+[default]
+dupe-manifest = true
+head = head.js head-imap.js
+support-files = data/utils.js
+tags = imap webextensions
+
+[include:xpcshell.ini]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini
new file mode 100644
index 0000000000..19d50044cd
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini
@@ -0,0 +1,23 @@
+[default]
+dupe-manifest = true
+head = head.js
+support-files = data/utils.js
+tags = local webextensions
+
+[include:xpcshell.ini]
+[test_ext_accounts.js]
+[test_ext_accounts_mv3_event_pages.js]
+[test_ext_identities_mv3_event_pages.js]
+[test_ext_addressBook.js]
+support-files = images/**
+tags = addrbook
+[test_ext_addressBook_readonly.js]
+tags = addrbook
+[test_ext_addressBook_remote.js]
+tags = addrbook
+[test_ext_addressBook_provider.js]
+tags = addrbook
+[test_ext_addressBook_quickSearch.js]
+tags = addrbook
+[test_ext_alias.js]
+[test_ext_browserAction_unifiedtoolbar_restart.js]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini
new file mode 100644
index 0000000000..66e23e03bd
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini
@@ -0,0 +1,7 @@
+[default]
+dupe-manifest = true
+head = head.js head-nntp.js
+support-files = data/utils.js
+tags = nntp webextensions
+
+[include:xpcshell.ini]
diff --git a/comm/mail/components/extensions/test/xpcshell/xpcshell.ini b/comm/mail/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..666a67d5da
--- /dev/null
+++ b/comm/mail/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,17 @@
+[test_ext_experiments.js]
+tags = addrbook
+[test_ext_folders.js] # NNTP disabled (no support for folder operations).
+[test_ext_folders_mv3_event_pages.js] # NNTP disabled (no support for folder operations).
+[test_ext_messages.js] # NNTP disabled (no support for Trash folder).
+[test_ext_messages_attachments.js] # IMAP disabled (doesn't work with test server).
+support-files = messages/**
+[test_ext_messages_get.js] # NNTP disabled for PGP tests.
+support-files = messages/**
+[test_ext_messages_id.js] # NNTP disabled (message move not supported).
+[test_ext_messages_import.js]
+support-files = messages/**
+[test_ext_messages_move_copy_delete.js] # NNTP disabled (no support for Trash folder).
+[test_ext_messages_onNewMailReceived.js]
+[test_ext_messages_query.js]
+support-files = messages/alternative.eml
+[test_ext_messages_update.js] # NNTP disabled (no support for Trash folder).