diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/extensions/test/xpcshell | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/extensions/test/xpcshell')
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 Binary files differnew file mode 100644 index 0000000000..abda018027 --- /dev/null +++ b/comm/mail/components/extensions/test/xpcshell/images/redPixel.png diff --git a/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png Binary files differnew file mode 100644 index 0000000000..5514ad40e9 --- /dev/null +++ b/comm/mail/components/extensions/test/xpcshell/images/whitePixel.png 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). |