summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/composition
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/composition')
-rw-r--r--comm/mail/test/browser/composition/browser.ini127
-rw-r--r--comm/mail/test/browser/composition/browser_addressWidgets.js773
-rw-r--r--comm/mail/test/browser/composition/browser_attachment.js995
-rw-r--r--comm/mail/test/browser/composition/browser_attachmentCloudDraft.js577
-rw-r--r--comm/mail/test/browser/composition/browser_attachmentDragDrop.js689
-rw-r--r--comm/mail/test/browser/composition/browser_attachmentReminder.js892
-rw-r--r--comm/mail/test/browser/composition/browser_base64Display.js48
-rw-r--r--comm/mail/test/browser/composition/browser_blockedContent.js149
-rw-r--r--comm/mail/test/browser/composition/browser_charsetEdit.js231
-rw-r--r--comm/mail/test/browser/composition/browser_checkRecipientKeys.js87
-rw-r--r--comm/mail/test/browser/composition/browser_cp932Display.js37
-rw-r--r--comm/mail/test/browser/composition/browser_customHeaders.js92
-rw-r--r--comm/mail/test/browser/composition/browser_draftIdentity.js313
-rw-r--r--comm/mail/test/browser/composition/browser_drafts.js457
-rw-r--r--comm/mail/test/browser/composition/browser_emlActions.js194
-rw-r--r--comm/mail/test/browser/composition/browser_encryptedBccRecipients.js283
-rw-r--r--comm/mail/test/browser/composition/browser_expandLists.js151
-rw-r--r--comm/mail/test/browser/composition/browser_focus.js523
-rw-r--r--comm/mail/test/browser/composition/browser_font_color.js114
-rw-r--r--comm/mail/test/browser/composition/browser_font_family.js146
-rw-r--r--comm/mail/test/browser/composition/browser_font_size.js333
-rw-r--r--comm/mail/test/browser/composition/browser_forwardDefectiveCharset.js109
-rw-r--r--comm/mail/test/browser/composition/browser_forwardHeaders.js175
-rw-r--r--comm/mail/test/browser/composition/browser_forwardRFC822Attach.js71
-rw-r--r--comm/mail/test/browser/composition/browser_forwardUTF8.js149
-rw-r--r--comm/mail/test/browser/composition/browser_forwardedContent.js70
-rw-r--r--comm/mail/test/browser/composition/browser_forwardedEmlActions.js171
-rw-r--r--comm/mail/test/browser/composition/browser_imageDisplay.js172
-rw-r--r--comm/mail/test/browser/composition/browser_imageInsertionDialog.js164
-rw-r--r--comm/mail/test/browser/composition/browser_inlineImage.js112
-rw-r--r--comm/mail/test/browser/composition/browser_linkPreviews.js39
-rw-r--r--comm/mail/test/browser/composition/browser_messageBody.js109
-rw-r--r--comm/mail/test/browser/composition/browser_multipartRelated.js143
-rw-r--r--comm/mail/test/browser/composition/browser_newmsgComposeIdentity.js273
-rw-r--r--comm/mail/test/browser/composition/browser_paragraph_state.js888
-rw-r--r--comm/mail/test/browser/composition/browser_publicRecipientsWarning.js734
-rw-r--r--comm/mail/test/browser/composition/browser_quoteMessage.js91
-rw-r--r--comm/mail/test/browser/composition/browser_recipientPillsSelection.js264
-rw-r--r--comm/mail/test/browser/composition/browser_redirect.js212
-rw-r--r--comm/mail/test/browser/composition/browser_remove_text_styling.js136
-rw-r--r--comm/mail/test/browser/composition/browser_replyAddresses.js1143
-rw-r--r--comm/mail/test/browser/composition/browser_replyCatchAll.js271
-rw-r--r--comm/mail/test/browser/composition/browser_replyFormatFlowed.js90
-rw-r--r--comm/mail/test/browser/composition/browser_replyMultipartCharset.js149
-rw-r--r--comm/mail/test/browser/composition/browser_replySelection.js64
-rw-r--r--comm/mail/test/browser/composition/browser_replySignature.js117
-rw-r--r--comm/mail/test/browser/composition/browser_saveChangesOnQuit.js420
-rw-r--r--comm/mail/test/browser/composition/browser_sendButton.js363
-rw-r--r--comm/mail/test/browser/composition/browser_sendFormat.js565
-rw-r--r--comm/mail/test/browser/composition/browser_signatureInit.js50
-rw-r--r--comm/mail/test/browser/composition/browser_signatureUpdating.js276
-rw-r--r--comm/mail/test/browser/composition/browser_spelling.js311
-rw-r--r--comm/mail/test/browser/composition/browser_subjectWas.js65
-rw-r--r--comm/mail/test/browser/composition/browser_text_styling.js609
-rw-r--r--comm/mail/test/browser/composition/browser_xUnsent.js45
-rw-r--r--comm/mail/test/browser/composition/data/attachment.txt4
-rw-r--r--comm/mail/test/browser/composition/data/base64-bug1586890.eml25
-rw-r--r--comm/mail/test/browser/composition/data/base64-encoded-msg.eml11
-rw-r--r--comm/mail/test/browser/composition/data/base64-with-whitespace.eml46
-rw-r--r--comm/mail/test/browser/composition/data/body-greek.eml9
-rw-r--r--comm/mail/test/browser/composition/data/body-utf16.eml10
-rw-r--r--comm/mail/test/browser/composition/data/charset-cp932.eml11
-rw-r--r--comm/mail/test/browser/composition/data/content-utf8-alt-rel.eml46
-rw-r--r--comm/mail/test/browser/composition/data/content-utf8-alt-rel2.eml46
-rw-r--r--comm/mail/test/browser/composition/data/content-utf8-rel-alt.eml40
-rw-r--r--comm/mail/test/browser/composition/data/content-utf8-rel-only.eml32
-rw-r--r--comm/mail/test/browser/composition/data/defective-charset.eml28
-rw-r--r--comm/mail/test/browser/composition/data/en_NZ/en_NZ.aff0
-rw-r--r--comm/mail/test/browser/composition/data/en_NZ/en_NZ.dic23
-rw-r--r--comm/mail/test/browser/composition/data/evil-meta-msg.eml11
-rw-r--r--comm/mail/test/browser/composition/data/feed-message.eml26
-rw-r--r--comm/mail/test/browser/composition/data/format-flowed.eml12
-rw-r--r--comm/mail/test/browser/composition/data/format1-altering.eml21
-rw-r--r--comm/mail/test/browser/composition/data/format1-plain.eml22
-rw-r--r--comm/mail/test/browser/composition/data/format2-style-attr.eml37
-rw-r--r--comm/mail/test/browser/composition/data/format3-style-tag.eml31
-rw-r--r--comm/mail/test/browser/composition/data/iso-2022-jp.eml12
-rw-r--r--comm/mail/test/browser/composition/data/long-html-line.eml16
-rw-r--r--comm/mail/test/browser/composition/data/mime-encoded-subject.eml16
-rw-r--r--comm/mail/test/browser/composition/data/multipart-charset.eml24
-rw-r--r--comm/mail/test/browser/composition/data/nest.pngbin0 -> 293451 bytes
-rw-r--r--comm/mail/test/browser/composition/data/non-flowed-plain.eml15
-rw-r--r--comm/mail/test/browser/composition/data/tb-logo.pngbin0 -> 6462 bytes
-rw-r--r--comm/mail/test/browser/composition/data/testmsg.eml16
-rw-r--r--comm/mail/test/browser/composition/data/xunsent.eml14
-rw-r--r--comm/mail/test/browser/composition/head.js65
-rw-r--r--comm/mail/test/browser/composition/html/linkpreview.html14
87 files changed, 16514 insertions, 0 deletions
diff --git a/comm/mail/test/browser/composition/browser.ini b/comm/mail/test/browser/composition/browser.ini
new file mode 100644
index 0000000000..8391965d89
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser.ini
@@ -0,0 +1,127 @@
+[DEFAULT]
+head = head.js
+prefs =
+ ldap_2.servers.osx.dirType=-3
+ mail.account.account1.server=server1
+ mail.account.account2.identities=id1,id2
+ mail.account.account2.server=server2
+ mail.accountmanager.accounts=account1,account2
+ mail.accountmanager.defaultaccount=account2
+ mail.accountmanager.localfoldersserver=server1
+ mail.identity.id1.fullName=Tinderbox
+ mail.identity.id1.htmlSigFormat=false
+ mail.identity.id1.smtpServer=smtp1
+ mail.identity.id1.useremail=tinderbox@foo.invalid
+ mail.identity.id1.valid=true
+ mail.identity.id2.fullName=Tinderboxpushlog
+ mail.identity.id2.htmlSigFormat=true
+ mail.identity.id2.smtpServer=smtp1
+ mail.identity.id2.useremail=tinderboxpushlog@foo.invalid
+ mail.identity.id2.valid=true
+ mail.provider.suppress_dialog_on_startup=true
+ mail.server.server1.type=none
+ mail.server.server1.userName=nobody
+ mail.server.server2.check_new_mail=false
+ mail.server.server2.directory-rel=[ProfD]Mail/tinderbox
+ mail.server.server2.download_on_biff=true
+ mail.server.server2.hostname=tinderbox123
+ mail.server.server2.login_at_startup=false
+ mail.server.server2.name=tinderbox@foo.invalid
+ mail.server.server2.type=pop3
+ mail.server.server2.userName=tinderbox
+ mail.server.server2.whiteListAbURI=
+ mail.shell.checkDefaultClient=false
+ mail.smtp.defaultserver=smtp1
+ mail.smtpserver.smtp1.hostname=tinderbox123
+ mail.smtpserver.smtp1.username=tinderbox
+ mail.smtpservers=smtp1
+ mail.spotlight.firstRunDone=true
+ mail.startup.enabledMailCheckOnce=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+ datareporting.policy.dataSubmissionPolicyBypassNotification=true
+skip-if = os == 'linux' && bits == 32 && debug # Perma-fail
+subsuite = thunderbird
+support-files =
+ data/**
+ ../openpgp/data/**
+ html/linkpreview.html
+
+[browser_addressWidgets.js]
+[browser_attachment.js]
+[browser_attachmentCloudDraft.js]
+[browser_attachmentDragDrop.js]
+[browser_attachmentReminder.js]
+[browser_base64Display.js]
+[browser_blockedContent.js]
+skip-if = headless # clipboard doesn't work with headless
+[browser_charsetEdit.js]
+[browser_checkRecipientKeys.js]
+[browser_cp932Display.js]
+[browser_customHeaders.js]
+[browser_draftIdentity.js]
+skip-if = os == "mac"
+reason = See bug 1413851.
+[browser_drafts.js]
+[browser_emlActions.js]
+[browser_expandLists.js]
+[browser_encryptedBccRecipients.js]
+[browser_focus.js]
+[browser_font_color.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_font_family.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_font_size.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_forwardDefectiveCharset.js]
+[browser_forwardHeaders.js]
+[browser_forwardRFC822Attach.js]
+[browser_forwardUTF8.js]
+[browser_forwardedContent.js]
+[browser_forwardedEmlActions.js]
+[browser_imageDisplay.js]
+[browser_imageInsertionDialog.js]
+[browser_inlineImage.js]
+skip-if = headless # clipboard doesn't work with headless
+[browser_linkPreviews.js]
+[browser_messageBody.js]
+[browser_multipartRelated.js]
+[browser_newmsgComposeIdentity.js]
+[browser_paragraph_state.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_publicRecipientsWarning.js]
+[browser_quoteMessage.js]
+[browser_recipientPillsSelection.js]
+[browser_redirect.js]
+[browser_replyAddresses.js]
+skip-if = debug # Bug 1601591
+[browser_replyCatchAll.js]
+[browser_replyFormatFlowed.js]
+[browser_replyMultipartCharset.js]
+skip-if = debug # Bug 1606542
+[browser_replySelection.js]
+skip-if = true # Not working on 115 due to test changes elsewhere.
+[browser_replySignature.js]
+[browser_remove_text_styling.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_saveChangesOnQuit.js]
+[browser_sendButton.js]
+tags = addrbook
+skip-if = os == 'win' && bits == 64 && debug # Bug 1601520
+[browser_sendFormat.js]
+skip-if = os == "mac"
+reason = See bug 1763407
+[browser_signatureInit.js]
+[browser_signatureUpdating.js]
+[browser_spelling.js]
+[browser_text_styling.js]
+skip-if = os == "mac"
+reason = Cannot open the Format menu
+[browser_subjectWas.js]
+[browser_xUnsent.js]
diff --git a/comm/mail/test/browser/composition/browser_addressWidgets.js b/comm/mail/test/browser/composition/browser_addressWidgets.js
new file mode 100644
index 0000000000..4e5db02a72
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_addressWidgets.js
@@ -0,0 +1,773 @@
+/* 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/. */
+
+/**
+ * Tests proper enabling of addressing widgets.
+ */
+
+"use strict";
+
+var { click_menus_in_sequence } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { close_compose_window, open_compose_new_mail } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var { be_in_folder, FAKE_SERVER_HOSTNAME } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var cwc = null; // compose window controller
+var accountPOP3 = null;
+var accountNNTP = null;
+var originalAccountCount;
+
+add_setup(function () {
+ // Ensure we're in the tinderbox account as that has the right identities set
+ // up for this test.
+ let server = MailServices.accounts.findServer(
+ "tinderbox",
+ FAKE_SERVER_HOSTNAME,
+ "pop3"
+ );
+ accountPOP3 = MailServices.accounts.FindAccountForServer(server);
+
+ // There may be pre-existing accounts from other tests.
+ originalAccountCount = MailServices.accounts.allServers.length;
+});
+
+/**
+ * Check if the address type items are in the wished state.
+ *
+ * @param {Window} win - The window to search in.
+ * @param {string[]} itemsEnabled - List of item values that should be visible.
+ */
+function check_address_types_state(win, itemsEnabled) {
+ for (let item of win.document.querySelectorAll(
+ "#extraAddressRowsMenu > menuitem"
+ )) {
+ let buttonId = item.dataset.buttonId;
+ let showRowEl;
+ if (buttonId) {
+ let button = win.document.getElementById(buttonId);
+ if (item.dataset.preferButton == "true") {
+ showRowEl = button;
+ Assert.ok(item.hidden, `${item.id} menuitem should be hidden`);
+ } else {
+ showRowEl = item;
+ Assert.ok(button.hidden, `${button.id} button should be hidden`);
+ }
+ } else {
+ showRowEl = item;
+ }
+
+ let type = item.id.replace(/ShowAddressRowMenuItem$/, "");
+
+ let expectShown = itemsEnabled.includes(type);
+ let row = win.document.querySelector(
+ `.address-row[data-recipienttype="${type}"]`
+ );
+ if (expectShown) {
+ // Either the row or the element that shows it should be visible, but not
+ // both.
+ if (row.classList.contains("hidden")) {
+ Assert.ok(
+ !showRowEl.hidden,
+ `${showRowEl.id} should be visible when the row is hidden`
+ );
+ } else {
+ Assert.ok(
+ showRowEl.hidden,
+ `${showRowEl.id} should be hidden when the row is visible`
+ );
+ }
+ } else {
+ // Both the row and the element that shows it should be hidden.
+ Assert.ok(row.classList.contains("hidden"), `${row.id} should be hidden`);
+ Assert.ok(showRowEl.hidden, `${showRowEl.id} should be hidden`);
+ }
+ }
+}
+
+/**
+ * With only a POP3 account, no News related address types should be enabled.
+ */
+function check_mail_address_types(win) {
+ check_address_types_state(win, [
+ "addr_to",
+ "addr_cc",
+ "addr_reply",
+ "addr_bcc",
+ ]);
+}
+
+/**
+ * With a NNTP account, all address types should be enabled.
+ */
+function check_nntp_address_types(win) {
+ check_address_types_state(win, [
+ "addr_to",
+ "addr_cc",
+ "addr_reply",
+ "addr_bcc",
+ "addr_newsgroups",
+ "addr_followup",
+ ]);
+}
+
+/**
+ * With an NNTP account, the 'To' addressing row should be hidden.
+ */
+function check_collapsed_pop_recipient(cwc) {
+ Assert.ok(
+ cwc.window.document
+ .getElementById("addressRowTo")
+ .classList.contains("hidden")
+ );
+}
+
+function add_NNTP_account() {
+ // Create a NNTP server
+ let nntpServer = MailServices.accounts
+ .createIncomingServer(null, "example.nntp.invalid", "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = "tinderbox2@example.invalid";
+
+ accountNNTP = MailServices.accounts.createAccount();
+ accountNNTP.incomingServer = nntpServer;
+ accountNNTP.addIdentity(identity);
+ // Now there should be 1 more account.
+ Assert.equal(
+ MailServices.accounts.allServers.length,
+ originalAccountCount + 1
+ );
+}
+
+function remove_NNTP_account() {
+ // Remove our NNTP account to leave the profile clean.
+ MailServices.accounts.removeAccount(accountNNTP);
+ // There should be only the original accounts left.
+ Assert.equal(MailServices.accounts.allServers.length, originalAccountCount);
+}
+
+/**
+ * Bug 399446 & bug 922614
+ * Test that the allowed address types depend on the account type
+ * we are sending from.
+ */
+add_task(async function test_address_types() {
+ // Be sure there is no NNTP account yet.
+ for (let account of MailServices.accounts.accounts) {
+ Assert.notEqual(
+ account.incomingServer.type,
+ "nntp",
+ "There is a NNTP account existing unexpectedly"
+ );
+ }
+
+ // Open compose window on the existing POP3 account.
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ cwc = open_compose_new_mail();
+ check_mail_address_types(cwc.window);
+ close_compose_window(cwc);
+
+ add_NNTP_account();
+
+ // From now on, we should always get all possible address types offered,
+ // regardless of which account is used of composing (bug 922614).
+ await be_in_folder(accountNNTP.incomingServer.rootFolder);
+ cwc = open_compose_new_mail();
+ check_nntp_address_types(cwc.window);
+ check_collapsed_pop_recipient(cwc);
+ close_compose_window(cwc);
+
+ // Now try the same accounts but choosing them in the From dropdown
+ // inside compose window.
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ cwc = open_compose_new_mail();
+ check_nntp_address_types(cwc.window);
+
+ let NNTPidentity = accountNNTP.defaultIdentity.key;
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("msgIdentity"),
+ {},
+ cwc.window.document.getElementById("msgIdentity").ownerGlobal
+ );
+ await click_menus_in_sequence(
+ cwc.window.document.getElementById("msgIdentityPopup"),
+ [{ identitykey: NNTPidentity }]
+ );
+ check_nntp_address_types(cwc.window);
+
+ // Switch back to the POP3 account.
+ let POP3identity = accountPOP3.defaultIdentity.key;
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("msgIdentity"),
+ {},
+ cwc.window.document.getElementById("msgIdentity").ownerGlobal
+ );
+ await click_menus_in_sequence(
+ cwc.window.document.getElementById("msgIdentityPopup"),
+ [{ identitykey: POP3identity }]
+ );
+ check_nntp_address_types(cwc.window);
+
+ close_compose_window(cwc);
+
+ remove_NNTP_account();
+
+ // Now the NNTP account is lost, so we should be back to mail only addresses.
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ cwc = open_compose_new_mail();
+ check_mail_address_types(cwc.window);
+ close_compose_window(cwc);
+});
+
+add_task(async function test_address_suppress_leading_comma_space() {
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ let controller = open_compose_new_mail();
+
+ let addrInput = controller.window.document.getElementById("toAddrInput");
+ Assert.ok(addrInput);
+ Assert.equal(addrInput.value, "");
+
+ // Create a pill.
+ addrInput.value = "person@org";
+ // Comma triggers the pill creation.
+ // Note: the address input should already have focus.
+ EventUtils.synthesizeKey(",", {}, controller.window);
+
+ let addrPill = await TestUtils.waitForCondition(
+ () =>
+ controller.window.document.querySelector(
+ "#toAddrContainer > .address-pill"
+ ),
+ "Pill creation"
+ );
+ Assert.equal(addrInput.value, "");
+ let pillInput = addrPill.querySelector("input");
+ Assert.ok(pillInput);
+
+ // Asserts that the input has the correct exceptional behaviour for 'comma'
+ // and 'space'.
+ async function assertKeyInput(input) {
+ // Since we will be partially testing for a lack of response to the " " and
+ // "," key presses, we first run the tests with the "a" key press to assure
+ // us that the tests would otherwise capture the normal behaviour. This will
+ // also shows us that the comma and space behaviour is exceptional.
+ for (let key of ["a", " ", ","]) {
+ // Clear input.
+ input.value = "";
+ await TestUtils.waitForTick();
+
+ // Type the key in an empty input.
+ let eventPromise = BrowserTestUtils.waitForEvent(input, "keydown");
+ EventUtils.synthesizeKey(key, {}, controller.window);
+ await eventPromise;
+
+ if (key === " " || key === ",") {
+ // Key is suppressed, so the input remains empty.
+ Assert.equal(input.value, "");
+ } else {
+ // Normal behaviour: key is added to the input.
+ Assert.equal(input.value, key);
+ }
+
+ // If the input is not empty, we should still have the normal behaviour.
+ input.value = "z";
+ input.selectionStart = 1;
+ input.SelectionEnd = 1;
+ await TestUtils.waitForTick();
+
+ await BrowserTestUtils.synthesizeKey(
+ key,
+ {},
+ controller.window.browsingContext
+ );
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ Assert.equal(input.value, "z" + key);
+
+ // Test typing the key to replace all the trimmed input.
+ // Sample text with two spaces as start and end. Also includes a 2
+ // character emoji.
+ let someText = " some, text沒 ";
+ for (let selection of [
+ { start: 0, end: 0 },
+ { start: 1, end: 0 },
+ { start: 0, end: 1 },
+ { start: 2, end: 2 },
+ ]) {
+ input.value = someText;
+ input.selectionStart = selection.start;
+ input.selectionEnd = someText.length - selection.end;
+ await TestUtils.waitForTick();
+
+ // Type the key to replace the text.
+ await BrowserTestUtils.synthesizeKey(
+ key,
+ {},
+ controller.window.browsingContext
+ );
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ if (key === " " || key === ",") {
+ // Key is suppressed and input is empty.
+ Assert.equal(input.value, "");
+ } else {
+ // Normal behaviour: key replaces the selected text.
+ Assert.equal(
+ input.value,
+ someText.slice(0, selection.start) +
+ key +
+ someText.slice(someText.length - selection.end)
+ );
+ }
+ }
+
+ // If we do not replace all the trimmed input, we should still have
+ // normal behaviour.
+ input.value = " text ";
+ input.selectionStart = 1;
+ // Select up to 'x'.
+ input.selectionEnd = 5;
+ await TestUtils.waitForTick();
+
+ await BrowserTestUtils.synthesizeKey(
+ key,
+ {},
+ controller.window.browsingContext
+ );
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ Assert.equal(input.value, " " + key + "t ");
+ }
+ }
+
+ // Assert that the address input has the correct behaviour for key presses.
+ // Note: the address input should still have focus.
+ await assertKeyInput(addrInput);
+
+ // Now test the behaviour when editing a pill.
+ // First, we need to get into editing mode by clicking the pill twice.
+ EventUtils.synthesizeMouseAtCenter(
+ addrPill,
+ { clickCount: 1 },
+ controller.window
+ );
+ let clickPromise = BrowserTestUtils.waitForEvent(addrPill, "click");
+ // We do not want a double click, but two separate clicks.
+ EventUtils.synthesizeMouseAtCenter(
+ addrPill,
+ { clickCount: 1 },
+ controller.window
+ );
+ await clickPromise;
+
+ Assert.ok(!pillInput.hidden);
+
+ // Assert that editing a pill has the same behaviour as the address input.
+ await assertKeyInput(pillInput);
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_pill_creation_in_all_fields() {
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ let cwc = open_compose_new_mail();
+
+ let addresses = ["person@org", "foo@address.valid", "invalid", "foo@address"];
+ let subjectField = cwc.window.document.getElementById("msgSubject");
+
+ // Helper method to create multiple pills in a field.
+ async function assertPillsCreationInField(input) {
+ Assert.ok(input);
+ Assert.equal(input.value, "");
+
+ // Write an address in the field.
+ input.value = addresses[0];
+ // Enter triggers the pill creation.
+ EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window);
+ // Assert the pill was created.
+ await TestUtils.waitForCondition(
+ () =>
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill").length == 1,
+ "Pills created"
+ );
+ // Assert the pill has the correct address.
+ Assert.equal(
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill")[0].emailAddress,
+ addresses[0]
+ );
+
+ // Write another address in the field.
+ input.value = addresses[1];
+ // Tab triggers the pill creation.
+ EventUtils.synthesizeKey("VK_TAB", {}, cwc.window);
+ // Assert the pill was created.
+ await TestUtils.waitForCondition(
+ () =>
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill").length == 2,
+ "Pills created"
+ );
+ // Assert the pill has the correct address.
+ Assert.equal(
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill")[1].emailAddress,
+ addresses[1]
+ );
+
+ // Write an invalid email address in the To field.
+ input.value = addresses[2];
+ // Enter triggers the pill creation.
+ EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window);
+ // Assert that an invalid address pill was created.
+ await TestUtils.waitForCondition(
+ () =>
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill.invalid-address").length == 1,
+ "Invalid pill created"
+ );
+ // Assert the pill has the correct address.
+ Assert.equal(
+ input
+ .closest(".address-container")
+ .querySelector("mail-address-pill.invalid-address").emailAddress,
+ addresses[2]
+ );
+
+ // Write another address in the field.
+ input.value = addresses[3];
+ // Focusing on another element triggers the pill creation.
+ subjectField.focus();
+ // Assert the pill was created.
+ await TestUtils.waitForCondition(
+ () =>
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill").length == 4,
+ "Pills created"
+ );
+ // Assert the pill has the correct address.
+ Assert.equal(
+ input
+ .closest(".address-container")
+ .querySelectorAll("mail-address-pill")[3].emailAddress,
+ addresses[3]
+ );
+ }
+
+ // The To field is visible and focused by default when the compose window is
+ // first opened.
+ // Test pill creation for the To input field.
+ let toInput = cwc.window.document.getElementById("toAddrInput");
+ await assertPillsCreationInField(toInput);
+
+ // Click on the Cc recipient label.
+ let ccInput = cwc.window.document.getElementById("ccAddrInput");
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !ccInput.closest(".address-row").classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+ // Test pill creation for the Cc input field.
+ await assertPillsCreationInField(ccInput);
+
+ // Click on the Bcc recipient label.
+ let bccInput = cwc.window.document.getElementById("bccAddrInput");
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Bcc field should now be visible.
+ Assert.ok(
+ !bccInput.closest(".address-row").classList.contains("hidden"),
+ "The Bcc field is visible"
+ );
+ // Test pill creation for the Bcc input field.
+ await assertPillsCreationInField(bccInput);
+
+ // Focus on the Bcc field and hold press the Backspace key.
+ bccInput.focus();
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window);
+
+ // All pills should be deleted, but the focus should remain on the Bcc field.
+ Assert.equal(
+ bccInput.closest(".address-container").querySelectorAll("mail-address-pill")
+ .length,
+ 0,
+ "All pills in the Bcc field have been removed."
+ );
+ Assert.ok(
+ !bccInput.closest(".address-row").classList.contains("hidden"),
+ "The Bcc field is still visible"
+ );
+
+ // Press and hold Backspace again.
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window);
+
+ // Confirm the Bcc field is closed and the focus moved to the Cc field.
+ Assert.ok(
+ bccInput.closest(".address-row").classList.contains("hidden"),
+ "The Bcc field was closed"
+ );
+ Assert.equal(cwc.window.document.activeElement, ccInput);
+
+ // Now we're on the Cc field. Press and hold Backspace to delete all pills.
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window);
+
+ // All pills should be deleted, but the focus should remain on the Cc field.
+ Assert.equal(
+ ccInput.closest(".address-container").querySelectorAll("mail-address-pill")
+ .length,
+ 0,
+ "All pills in the Cc field have been removed."
+ );
+ Assert.ok(
+ !ccInput.closest(".address-row").classList.contains("hidden"),
+ "The Cc field is still visible"
+ );
+
+ // Press and hold Backspace again.
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window);
+
+ // Confirm the Cc field is closed and the focus moved to the To field.
+ Assert.ok(
+ ccInput.closest(".address-row").classList.contains("hidden"),
+ "The Cc field was closed"
+ );
+ Assert.equal(cwc.window.document.activeElement, toInput);
+
+ // Now we're on the To field. Press and hold Backspace to delete all pills.
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 5 }, cwc.window);
+
+ // All pills should be deleted, but the focus should remain on the To field.
+ Assert.equal(
+ toInput.closest(".address-container").querySelectorAll("mail-address-pill")
+ .length,
+ 0,
+ "All pills in the To field have been removed."
+ );
+ Assert.ok(
+ !toInput.closest(".address-row").classList.contains("hidden"),
+ "The To field is still visible"
+ );
+
+ // Press and hold Backspace again.
+ EventUtils.synthesizeKey("KEY_Backspace", { repeat: 2 }, cwc.window);
+
+ // Long backspace keypress on the To field shouldn't do anything if the field
+ // is empty. Confirm the To field is still visible and the focus stays on the
+ // To field.
+ Assert.ok(
+ !toInput.closest(".address-row").classList.contains("hidden"),
+ "The To field is still visible"
+ );
+ Assert.equal(cwc.window.document.activeElement, toInput);
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_addressing_fields_shortcuts() {
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ let cwc = open_compose_new_mail();
+
+ let addrToInput = cwc.window.document.getElementById("toAddrInput");
+ // The To input field should be empty.
+ Assert.equal(addrToInput.value, "");
+ // The To input field should be the currently focused element.
+ Assert.equal(cwc.window.document.activeElement, addrToInput);
+
+ const modifiers =
+ AppConstants.platform == "macosx"
+ ? { accelKey: true, shiftKey: true }
+ : { ctrlKey: true, shiftKey: true };
+
+ let addrCcInput = cwc.window.document.getElementById("ccAddrInput");
+ let ccRowShownPromise = BrowserTestUtils.waitForCondition(
+ () => !addrCcInput.closest(".address-row").classList.contains("hidden"),
+ "The Cc addressing row is not visible."
+ );
+ // Press the Ctrl/Cmd+Shift+C.
+ EventUtils.synthesizeKey("C", modifiers, cwc.window);
+ // The Cc addressing row should be visible.
+ await ccRowShownPromise;
+ // The Cc input field should be currently focused.
+ Assert.equal(cwc.window.document.activeElement, addrCcInput);
+
+ let addrBccInput = cwc.window.document.getElementById("bccAddrInput");
+ let bccRowShownPromise = BrowserTestUtils.waitForCondition(
+ () => !addrBccInput.closest(".address-row").classList.contains("hidden"),
+ "The Bcc addressing row is not visible."
+ );
+ // Press the Ctrl/Cmd+Shift+B.
+ EventUtils.synthesizeKey("B", modifiers, cwc.window);
+ await bccRowShownPromise;
+ // The Bcc input field should be currently focused.
+ Assert.equal(cwc.window.document.activeElement, addrBccInput);
+
+ // Press the Ctrl/Cmd+Shift+T.
+ EventUtils.synthesizeKey("T", modifiers, cwc.window);
+ // The To input field should be the currently focused element.
+ Assert.equal(cwc.window.document.activeElement, addrToInput);
+
+ // Press the Ctrl/Cmd+Shift+C.
+ EventUtils.synthesizeKey("C", modifiers, cwc.window);
+ // The Cc input field should be currently focused.
+ Assert.equal(cwc.window.document.activeElement, addrCcInput);
+
+ // Press the Ctrl/Cmd+Shift+B.
+ EventUtils.synthesizeKey("B", modifiers, cwc.window);
+ // The Bcc input field should be currently focused.
+ Assert.equal(cwc.window.document.activeElement, addrBccInput);
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_pill_deletion_and_focus() {
+ await be_in_folder(accountPOP3.incomingServer.rootFolder);
+ let cwc = open_compose_new_mail();
+
+ // When the compose window is opened, the focus should be on the To field.
+ let toInput = cwc.window.document.getElementById("toAddrInput");
+ Assert.equal(cwc.window.document.activeElement, toInput);
+
+ const modifiers =
+ AppConstants.platform == "macosx" ? { accelKey: true } : { ctrlKey: true };
+ const addresses = "person@org, foo@address.valid, invalid, foo@address";
+
+ // Test the To field.
+ test_deletion_and_focus_on_input(cwc, toInput, addresses, modifiers);
+
+ // Reveal and test the Cc field.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ test_deletion_and_focus_on_input(
+ cwc,
+ cwc.window.document.getElementById("ccAddrInput"),
+ addresses,
+ modifiers
+ );
+
+ // Reveal and test the Bcc field.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ test_deletion_and_focus_on_input(
+ cwc,
+ cwc.window.document.getElementById("bccAddrInput"),
+ addresses,
+ modifiers
+ );
+
+ close_compose_window(cwc);
+});
+
+function test_deletion_and_focus_on_input(cwc, input, addresses, modifiers) {
+ // Focus on the input before adding anything to be sure keyboard shortcut are
+ // triggered from the right element.
+ input.focus();
+
+ // Fill the input field with a long of string of comma separated addresses.
+ input.value = addresses;
+
+ // Enter triggers the pill creation.
+ EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window);
+
+ let container = input.closest(".address-container");
+ // We should now have 4 pills.
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill").length,
+ 4,
+ "All pills in the field have been created."
+ );
+
+ // One pill should be flagged as invalid.
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill.invalid-address").length,
+ 1,
+ "One created pill is invalid."
+ );
+
+ // After pills creation, the same field should be still focused.
+ Assert.equal(cwc.window.document.activeElement, input);
+
+ // Keypress left arrow should focus and select the last created pill.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill[selected]").length,
+ 1,
+ "One pill is currently selected."
+ );
+
+ // Pressing delete should delete the selected pill and move the focus back to
+ // the input.
+ EventUtils.synthesizeKey("KEY_Delete", {}, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill").length,
+ 3,
+ "One pill correctly deleted."
+ );
+ Assert.equal(cwc.window.document.activeElement, input);
+
+ // Keypress left arrow to select the last available pill.
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill[selected]").length,
+ 1,
+ "One pill is currently selected."
+ );
+
+ // BackSpace should delete the pill and focus on the previous adjacent pill.
+ EventUtils.synthesizeKey("KEY_Backspace", {}, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill").length,
+ 2,
+ "One pill correctly deleted."
+ );
+ let selectedPill = container.querySelector("mail-address-pill[selected]");
+ Assert.equal(cwc.window.document.activeElement, selectedPill);
+
+ // Pressing CTRL+A should select all pills.
+ EventUtils.synthesizeKey("a", modifiers, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill[selected]").length,
+ 2,
+ "All remaining 2 pills are currently selected."
+ );
+
+ // BackSpace should delete all pills and focus on empty inptu field.
+ EventUtils.synthesizeKey("KEY_Backspace", {}, cwc.window);
+ Assert.equal(
+ container.querySelectorAll("mail-address-pill").length,
+ 0,
+ "All pills have been deleted."
+ );
+ Assert.equal(cwc.window.document.activeElement, input);
+}
diff --git a/comm/mail/test/browser/composition/browser_attachment.js b/comm/mail/test/browser/composition/browser_attachment.js
new file mode 100644
index 0000000000..23f8e9a089
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_attachment.js
@@ -0,0 +1,995 @@
+/* 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/. */
+
+/**
+ * Tests attachment handling functionality of the message compose window.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var {
+ add_attachments,
+ close_compose_window,
+ delete_attachment,
+ open_compose_new_mail,
+ open_compose_with_forward,
+ open_compose_with_forward_as_attachments,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ select_click_row,
+ wait_for_popup_to_open,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { plan_for_modal_dialog, wait_for_modal_dialog } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var messenger;
+var folder;
+var epsilon;
+var filePrefix;
+
+var rawAttachment =
+ "Can't make the frug contest, Helen; stomach's upset. I'll fix you, " +
+ "Ubik! Ubik drops you back in the thick of things fast. Taken as " +
+ "directed, Ubik speeds relief to head and stomach. Remember: Ubik is " +
+ "only seconds away. Avoid prolonged use.";
+
+var b64Attachment =
+ "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" +
+ "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" +
+ "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" +
+ "SNQAAlmAY+71EgFoAAAAASUVORK5CYII=";
+var b64Size = 188;
+
+add_setup(async function () {
+ folder = await create_folder("ComposeAttachmentA");
+
+ messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ /* Today's gory details (thanks to Jonathan Protzenko): libmime somehow
+ * counts the trailing newline for an attachment MIME part. Most of the time,
+ * assuming attachment has N bytes (no matter what's inside, newlines or
+ * not), libmime will return N + 1 bytes. On Linux and Mac, this always
+ * holds. However, on Windows, if the attachment is not encoded (that is, is
+ * inline text), libmime will return N + 2 bytes. Since we're dealing with
+ * forwarded message data here, the bonus byte(s) appear twice.
+ */
+ epsilon = AppConstants.platform == "win" ? 4 : 2;
+ filePrefix = AppConstants.platform == "win" ? "file:///C:/" : "file:///";
+
+ // create some messages that have various types of attachments
+ let messages = [
+ // no attachment
+ {},
+ // raw attachment
+ {
+ attachments: [{ body: rawAttachment, filename: "ubik.txt", format: "" }],
+ },
+ // b64-encoded image attachment
+ {
+ attachments: [
+ {
+ body: b64Attachment,
+ contentType: "image/png",
+ filename: "lines.png",
+ encoding: "base64",
+ format: "",
+ },
+ ],
+ },
+ ];
+
+ for (let i = 0; i < messages.length; i++) {
+ await add_message_to_folder([folder], create_message(messages[i]));
+ }
+});
+
+/**
+ * Make sure that the attachment's size is what we expect
+ *
+ * @param controller the controller for the compose window
+ * @param index the attachment to examine, as an index into the listbox
+ * @param expectedSize the expected size of the attachment, in bytes
+ */
+function check_attachment_size(controller, index, expectedSize) {
+ let bucket = controller.window.document.getElementById("attachmentBucket");
+ let node = bucket.querySelectorAll("richlistitem.attachmentItem")[index];
+
+ // First, let's check that the attachment size is correct
+ let size = node.attachment.size;
+ if (Math.abs(size - expectedSize) > epsilon) {
+ throw new Error(
+ "Reported attachment size (" +
+ size +
+ ") not within epsilon " +
+ "of actual attachment size (" +
+ expectedSize +
+ ")"
+ );
+ }
+
+ // Next, make sure that the formatted size in the label is correct
+ let formattedSize = node.getAttribute("size");
+ let expectedFormattedSize = messenger.formatFileSize(size);
+ if (formattedSize != expectedFormattedSize) {
+ throw new Error(
+ "Formatted attachment size (" +
+ formattedSize +
+ ") does not " +
+ "match expected value (" +
+ expectedFormattedSize +
+ ")"
+ );
+ }
+}
+
+/**
+ * Make sure that the attachment's size is not displayed
+ *
+ * @param controller the controller for the compose window
+ * @param index the attachment to examine, as an index into the listbox
+ */
+function check_no_attachment_size(controller, index) {
+ let bucket = controller.window.document.getElementById("attachmentBucket");
+ let node = bucket.querySelectorAll("richlistitem.attachmentItem")[index];
+
+ if (node.attachment.size != -1) {
+ throw new Error("attachment.size attribute should be -1!");
+ }
+
+ // If there's no size, the size attribute is empty.
+ if (node.getAttribute("size") != "") {
+ throw new Error("Attachment size should not be displayed!");
+ }
+}
+
+/**
+ * Make sure that the total size of all attachments is what we expect.
+ *
+ * @param controller the controller for the compose window
+ * @param count the expected number of attachments
+ */
+function check_total_attachment_size(controller, count) {
+ let bucket = controller.window.document.getElementById("attachmentBucket");
+ let nodes = bucket.querySelectorAll("richlistitem.attachmentItem");
+ let sizeNode = controller.window.document.getElementById(
+ "attachmentBucketSize"
+ );
+
+ if (nodes.length != count) {
+ throw new Error(
+ "Saw " + nodes.length + " attachments, but expected " + count
+ );
+ }
+
+ let size = 0;
+ for (let i = 0; i < nodes.length; i++) {
+ let currSize = nodes[i].attachment.size;
+ if (currSize != -1) {
+ size += currSize;
+ }
+ }
+
+ // Next, make sure that the formatted size in the label is correct
+ let expectedFormattedSize = messenger.formatFileSize(size);
+ if (sizeNode.textContent != expectedFormattedSize) {
+ throw new Error(
+ "Formatted attachment size (" +
+ sizeNode.textContent +
+ ") does not " +
+ "match expected value (" +
+ expectedFormattedSize +
+ ")"
+ );
+ }
+}
+
+add_task(function test_file_attachment() {
+ let cwc = open_compose_new_mail();
+
+ let url = filePrefix + "some/file/here.txt";
+ let size = 1234;
+
+ add_attachments(cwc, url, size);
+ check_attachment_size(cwc, 0, size);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+add_task(function test_webpage_attachment() {
+ let cwc = open_compose_new_mail();
+
+ add_attachments(cwc, "https://www.mozilla.org/");
+ check_no_attachment_size(cwc, 0);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+add_task(function test_multiple_attachments() {
+ let cwc = open_compose_new_mail();
+
+ let files = [
+ { name: "foo.txt", size: 1234 },
+ { name: "bar.txt", size: 5678 },
+ { name: "baz.txt", size: 9012 },
+ ];
+ for (let i = 0; i < files.length; i++) {
+ add_attachments(cwc, filePrefix + files[i].name, files[i].size);
+ check_attachment_size(cwc, i, files[i].size);
+ }
+
+ check_total_attachment_size(cwc, files.length);
+ close_compose_window(cwc);
+});
+
+add_task(function test_delete_attachments() {
+ let cwc = open_compose_new_mail();
+
+ let files = [
+ { name: "foo.txt", size: 1234 },
+ { name: "bar.txt", size: 5678 },
+ { name: "baz.txt", size: 9012 },
+ ];
+ for (let i = 0; i < files.length; i++) {
+ add_attachments(cwc, filePrefix + files[i].name, files[i].size);
+ check_attachment_size(cwc, i, files[i].size);
+ }
+
+ delete_attachment(cwc, 0);
+ check_total_attachment_size(cwc, files.length - 1);
+
+ close_compose_window(cwc);
+});
+
+function subtest_rename_attachment(cwc) {
+ cwc.window.document.getElementById("loginTextbox").value = "renamed.txt";
+ cwc.window.document.querySelector("dialog").getButton("accept").doCommand();
+}
+
+add_task(function test_rename_attachment() {
+ let cwc = open_compose_new_mail();
+
+ let url = filePrefix + "some/file/here.txt";
+ let size = 1234;
+
+ add_attachments(cwc, url, size);
+
+ // Now, rename the attachment.
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ let node = bucket.querySelector("richlistitem.attachmentItem");
+ EventUtils.synthesizeMouseAtCenter(node, {}, node.ownerGlobal);
+ plan_for_modal_dialog("commonDialogWindow", subtest_rename_attachment);
+ cwc.window.RenameSelectedAttachment();
+ wait_for_modal_dialog("commonDialogWindow");
+
+ Assert.equal(node.getAttribute("name"), "renamed.txt");
+
+ check_attachment_size(cwc, 0, size);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+function subtest_open_attachment(cwc) {
+ cwc.window.document.querySelector("dialog").getButton("cancel").doCommand();
+}
+
+add_task(function test_open_attachment() {
+ let cwc = open_compose_new_mail();
+
+ // set up our external file for attaching
+ let file = new FileUtils.File(getTestFilePath("data/attachment.txt"));
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let url = fileHandler.getURLSpecFromActualFile(file);
+ let size = file.fileSize;
+
+ add_attachments(cwc, url, size);
+
+ // Now, open the attachment.
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ let node = bucket.querySelector("richlistitem.attachmentItem");
+ plan_for_modal_dialog("unknownContentTypeWindow", subtest_open_attachment);
+ EventUtils.synthesizeMouseAtCenter(node, { clickCount: 2 }, node.ownerGlobal);
+ wait_for_modal_dialog("unknownContentTypeWindow");
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_forward_raw_attachment() {
+ await be_in_folder(folder);
+ select_click_row(1);
+
+ let cwc = open_compose_with_forward();
+ check_attachment_size(cwc, 0, rawAttachment.length);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_forward_b64_attachment() {
+ await be_in_folder(folder);
+ select_click_row(2);
+
+ let cwc = open_compose_with_forward();
+ check_attachment_size(cwc, 0, b64Size);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_forward_message_as_attachment() {
+ await be_in_folder(folder);
+ let curMessage = select_click_row(0);
+
+ let cwc = open_compose_with_forward_as_attachments();
+ check_attachment_size(cwc, 0, curMessage.messageSize);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_forward_message_with_attachments_as_attachment() {
+ await be_in_folder(folder);
+ let curMessage = select_click_row(1);
+
+ let cwc = open_compose_with_forward_as_attachments();
+ check_attachment_size(cwc, 0, curMessage.messageSize);
+ check_total_attachment_size(cwc, 1);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Check that the compose window has the attachments we expect.
+ *
+ * @param aController The controller for the compose window
+ * @param aNames An array of attachment names that are expected
+ */
+function check_attachment_names(aController, aNames) {
+ let bucket = aController.window.document.getElementById("attachmentBucket");
+ Assert.equal(aNames.length, bucket.itemCount);
+ for (let i = 0; i < aNames.length; i++) {
+ Assert.equal(bucket.getItemAtIndex(i).getAttribute("name"), aNames[i]);
+ }
+}
+
+/**
+ * Execute a test of attachment reordering actions and check the resulting order.
+ *
+ * @param aCwc The controller for the compose window
+ * @param aInitialAttachmentNames An array of attachment names specifying the
+ * initial set of attachments to be created
+ * @param aReorder_actions An array of objects specifying a reordering action:
+ * { select: array of attachment item indexes to select,
+ * button: ID of button to click in the reordering menu,
+ * key: keycode of key to press instead of a click,
+ * key_modifiers: { accelKey: bool, ctrlKey: bool
+ * shiftKey: bool, altKey: bool, etc.},
+ * result: an array of attachment names in the new
+ * order that should result
+ * }
+ * @param openPanel {boolean} - Whether to open reorderAttachmentsPanel for the test
+ */
+async function subtest_reordering(
+ aCwc,
+ aInitialAttachmentNames,
+ aReorder_actions,
+ aOpenPanel = true
+) {
+ let bucket = aCwc.window.document.getElementById("attachmentBucket");
+ let panel;
+
+ // Create a set of attachments for the test.
+ const size = 1234;
+ for (let name of aInitialAttachmentNames) {
+ add_attachments(aCwc, filePrefix + name, size);
+ }
+ await new Promise(resolve => setTimeout(resolve));
+ Assert.equal(bucket.itemCount, aInitialAttachmentNames.length);
+ check_attachment_names(aCwc, aInitialAttachmentNames);
+
+ if (aOpenPanel) {
+ // Bring up the reordering panel.
+ aCwc.window.showReorderAttachmentsPanel();
+ await new Promise(resolve => setTimeout(resolve));
+ panel = aCwc.window.document.getElementById("reorderAttachmentsPanel");
+ await wait_for_popup_to_open(panel);
+ }
+
+ for (let action of aReorder_actions) {
+ // Ensure selection.
+ bucket.clearSelection();
+ for (let itemIndex of action.select) {
+ bucket.addItemToSelection(bucket.getItemAtIndex(itemIndex));
+ }
+ // Take action.
+ if ("button" in action) {
+ EventUtils.synthesizeMouseAtCenter(
+ aCwc.window.document.getElementById(action.button),
+ {},
+ aCwc.window.document.getElementById(action.button).ownerGlobal
+ );
+ } else if ("key" in action) {
+ EventUtils.synthesizeKey(action.key, action.key_modifiers, aCwc.window);
+ }
+ await new Promise(resolve => setTimeout(resolve));
+ // Check result.
+ check_attachment_names(aCwc, action.result);
+ }
+
+ if (aOpenPanel) {
+ // Close the panel.
+ panel.hidePopup();
+ utils.waitFor(
+ () => panel.state == "closed",
+ "Reordering panel didn't close"
+ );
+ }
+
+ // Clean up for a new set of attachments.
+ aCwc.window.RemoveAllAttachments();
+}
+
+/**
+ * Bug 663695, Bug 1417856, Bug 1426344, Bug 1425891, Bug 1427037.
+ * Check basic and advanced attachment reordering operations.
+ * This is the main function of this test.
+ */
+add_task(async function test_attachment_reordering() {
+ let cwc = open_compose_new_mail();
+ let editorEl = cwc.window.GetCurrentEditorElement();
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ let panel = cwc.window.document.getElementById("reorderAttachmentsPanel");
+ // const openReorderPanelModifiers =
+ // (AppConstants.platform == "macosx") ? { controlKey: true }
+ // : { altKey: true };
+
+ // First, some checks if the 'Reorder Attachments' panel
+ // opens and closes correctly.
+
+ // Create two attachments as otherwise the reordering panel won't open.
+ const size = 1234;
+ const initialAttachmentNames_0 = ["A1", "A2"];
+ for (let name of initialAttachmentNames_0) {
+ add_attachments(cwc, filePrefix + name, size);
+ await new Promise(resolve => setTimeout(resolve));
+ }
+ Assert.equal(bucket.itemCount, initialAttachmentNames_0.length);
+ check_attachment_names(cwc, initialAttachmentNames_0);
+
+ // Show 'Reorder Attachments' panel via mouse clicks.
+ let contextMenu = cwc.window.document.getElementById(
+ "msgComposeAttachmentItemContext"
+ );
+ let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ bucket.getItemAtIndex(1),
+ { type: "contextmenu" },
+ cwc.window
+ );
+ await shownPromise;
+ contextMenu.activateItem(
+ cwc.window.document.getElementById("composeAttachmentContext_reorderItem")
+ );
+ await wait_for_popup_to_open(panel);
+
+ // Click on the editor which should close the panel.
+ EventUtils.synthesizeMouseAtCenter(editorEl, {}, editorEl.ownerGlobal);
+ utils.waitFor(
+ () => panel.state == "closed",
+ "Reordering panel didn't close when editor was clicked."
+ );
+
+ // Clean up for a new set of attachments.
+ cwc.window.RemoveAllAttachments();
+
+ // Define checks for various moving operations.
+ // Check 1: basic, mouse-only.
+ const initialAttachmentNames_1 = ["a", "C", "B", "b", "bb", "x"];
+ const reorderActions_1 = [
+ {
+ select: [1, 2, 3],
+ button: "btn_sortAttachmentsToggle",
+ result: ["a", "b", "B", "C", "bb", "x"],
+ },
+ {
+ select: [4],
+ button: "btn_moveAttachmentLeft",
+ result: ["a", "b", "B", "bb", "C", "x"],
+ },
+ {
+ select: [5],
+ button: "btn_moveAttachmentFirst",
+ result: ["x", "a", "b", "B", "bb", "C"],
+ },
+ {
+ select: [0],
+ button: "btn_moveAttachmentRight",
+ result: ["a", "x", "b", "B", "bb", "C"],
+ },
+ {
+ select: [1],
+ button: "btn_moveAttachmentLast",
+ result: ["a", "b", "B", "bb", "C", "x"],
+ },
+ {
+ select: [1, 3],
+ button: "btn_moveAttachmentBundleUp",
+ result: ["a", "b", "bb", "B", "C", "x"],
+ },
+ // Bug 1417856
+ {
+ select: [2],
+ button: "btn_sortAttachmentsToggle",
+ result: ["a", "b", "B", "bb", "C", "x"],
+ },
+ ];
+
+ // Check 2: basic and advanced, mouse-only.
+ const initialAttachmentNames_2 = [
+ "a",
+ "x",
+ "C",
+ "y1",
+ "y2",
+ "B",
+ "b",
+ "z",
+ "bb",
+ ];
+ const reorderActions_2 = [
+ // For starters: moving a single attachment around in the list.
+ {
+ select: [1],
+ button: "btn_moveAttachmentLeft",
+ result: ["x", "a", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [0],
+ button: "btn_moveAttachmentLast",
+ result: ["a", "C", "y1", "y2", "B", "b", "z", "bb", "x"],
+ },
+ {
+ select: [8],
+ button: "btn_moveAttachmentFirst",
+ result: ["x", "a", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [0],
+ button: "btn_moveAttachmentRight",
+ result: ["a", "x", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+
+ // Moving multiple, disjunct selection with inner block up/down as-is.
+ // This feature can be useful for multiple disjunct selection patterns
+ // in an alternating list of attachments like
+ // {photo1.jpg, description1.txt, photo2.jpg, description2.txt},
+ // where the order of alternation should be inverted to become
+ // {description1.txt, photo1.jpg, description2.txt, photo2.txt}.
+ {
+ select: [1, 3, 4, 7],
+ button: "btn_moveAttachmentRight",
+ result: ["a", "C", "x", "B", "y1", "y2", "b", "bb", "z"],
+ },
+ {
+ select: [2, 4, 5, 8],
+ button: "btn_moveAttachmentLeft",
+ result: ["a", "x", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [1, 3, 4, 7],
+ button: "btn_moveAttachmentLeft",
+ result: ["x", "a", "y1", "y2", "C", "B", "z", "b", "bb"],
+ },
+
+ // Folding multiple, disjunct selection with inner block towards top/bottom.
+ {
+ select: [0, 2, 3, 6],
+ button: "btn_moveAttachmentLeft",
+ result: ["x", "y1", "y2", "a", "C", "z", "B", "b", "bb"],
+ },
+ {
+ select: [0, 1, 2, 5],
+ button: "btn_moveAttachmentLeft",
+ result: ["x", "y1", "y2", "a", "z", "C", "B", "b", "bb"],
+ },
+ {
+ select: [0, 1, 2, 4],
+ button: "btn_moveAttachmentLeft",
+ result: ["x", "y1", "y2", "z", "a", "C", "B", "b", "bb"],
+ },
+ {
+ select: [3, 5, 6, 8],
+ button: "btn_moveAttachmentRight",
+ result: ["x", "y1", "y2", "a", "z", "b", "C", "B", "bb"],
+ },
+ {
+ select: [4, 6, 7, 8],
+ button: "btn_moveAttachmentRight",
+ result: ["x", "y1", "y2", "a", "b", "z", "C", "B", "bb"],
+ },
+
+ // Prepare scenario for and test 'Group together' (upwards).
+ {
+ select: [1, 2],
+ button: "btn_moveAttachmentRight",
+ result: ["x", "a", "y1", "y2", "b", "z", "C", "B", "bb"],
+ },
+ {
+ select: [0, 2, 3, 5],
+ button: "btn_moveAttachmentRight",
+ result: ["a", "x", "b", "y1", "y2", "C", "z", "B", "bb"],
+ },
+ {
+ select: [1, 3, 4, 6],
+ button: "btn_moveAttachmentBundleUp",
+ result: ["a", "x", "y1", "y2", "z", "b", "C", "B", "bb"],
+ },
+ // 'Group together' (downwards) is not tested here because it is
+ // only available via keyboard shortcuts, e.g. Alt+Cursor Right.
+
+ // Sort selected attachments only.
+ // Unsorted multiple selection must be collapsed upwards first if disjunct,
+ // then sorted ascending.
+ {
+ select: [0, 5, 6, 8],
+ button: "btn_sortAttachmentsToggle",
+ result: ["a", "b", "bb", "C", "x", "y1", "y2", "z", "B"],
+ },
+ // Sorted multiple block selection must be sorted the other way round.
+ {
+ select: [0, 1, 2, 3],
+ button: "btn_sortAttachmentsToggle",
+ result: ["C", "bb", "b", "a", "x", "y1", "y2", "z", "B"],
+ },
+ // Sorted, multiple, disjunct selection must just be collapsed upwards.
+ {
+ select: [3, 8],
+ button: "btn_sortAttachmentsToggle",
+ result: ["C", "bb", "b", "a", "B", "x", "y1", "y2", "z"],
+ },
+ {
+ select: [0, 2, 3],
+ button: "btn_sortAttachmentsToggle",
+ result: ["C", "b", "a", "bb", "B", "x", "y1", "y2", "z"],
+ },
+
+ // Bug 1417856: Sort all attachments when 1 or no attachment selected.
+ {
+ select: [1],
+ button: "btn_sortAttachmentsToggle",
+ result: ["a", "b", "B", "bb", "C", "x", "y1", "y2", "z"],
+ },
+ {
+ select: [],
+ button: "btn_sortAttachmentsToggle",
+ result: ["z", "y2", "y1", "x", "C", "bb", "B", "b", "a"],
+ },
+
+ // Collapsing multiple, disjunct selection with inner block to top/bottom.
+ {
+ select: [3, 5, 6, 8],
+ button: "btn_moveAttachmentFirst",
+ result: ["x", "bb", "B", "a", "z", "y2", "y1", "C", "b"],
+ },
+ {
+ select: [0, 2, 3, 7],
+ button: "btn_moveAttachmentLast",
+ result: ["bb", "z", "y2", "y1", "b", "x", "B", "a", "C"],
+ },
+ ];
+
+ // Check 3: basic and advanced, keyboard-only.
+ const initialAttachmentNames_3 = [
+ "a",
+ "x",
+ "C",
+ "y1",
+ "y2",
+ "B",
+ "b",
+ "z",
+ "bb",
+ ];
+ const modAlt = { altKey: true };
+ const modifiers2 =
+ AppConstants.platform == "macosx"
+ ? { accelKey: true, altKey: true }
+ : { altKey: true };
+ const reorderActions_3 = [
+ // For starters: moving a single attachment around in the list.
+ {
+ select: [1],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["x", "a", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [0],
+ // key_moveAttachmentBottom
+ key: AppConstants.platform == "macosx" ? "VK_DOWN" : "VK_END",
+ key_modifiers: modifiers2,
+ result: ["a", "C", "y1", "y2", "B", "b", "z", "bb", "x"],
+ },
+ {
+ select: [8],
+ // key_moveAttachmentTop
+ key: AppConstants.platform == "macosx" ? "VK_UP" : "VK_HOME",
+ key_modifiers: modifiers2,
+ result: ["x", "a", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [0],
+ // key_moveAttachmentBottom2 (secondary shortcut on MAC, same as Win primary)
+ key: "VK_END",
+ key_modifiers: modAlt,
+ result: ["a", "C", "y1", "y2", "B", "b", "z", "bb", "x"],
+ },
+ {
+ select: [8],
+ // key_moveAttachmentTop2 (secondary shortcut on MAC, same as Win primary)
+ key: "VK_HOME",
+ key_modifiers: modAlt,
+ result: ["x", "a", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [0],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["a", "x", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+
+ // Moving multiple, disjunct selection with inner block up/down as-is.
+ // This feature can be useful for multiple disjunct selection patterns
+ // in an alternating list of attachments like
+ // {photo1.jpg, description1.txt, photo2.jpg, description2.txt},
+ // where the order of alternation should be inverted to become
+ // {description1.txt, photo1.jpg, description2.txt, photo2.txt}.
+ {
+ select: [1, 3, 4, 7],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["a", "C", "x", "B", "y1", "y2", "b", "bb", "z"],
+ },
+ {
+ select: [2, 4, 5, 8],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["a", "x", "C", "y1", "y2", "B", "b", "z", "bb"],
+ },
+ {
+ select: [1, 3, 4, 7],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["x", "a", "y1", "y2", "C", "B", "z", "b", "bb"],
+ },
+
+ // Folding multiple, disjunct selection with inner block towards top/bottom.
+ {
+ select: [0, 2, 3, 6],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "a", "C", "z", "B", "b", "bb"],
+ },
+ {
+ select: [0, 1, 2, 5],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "a", "z", "C", "B", "b", "bb"],
+ },
+ {
+ select: [0, 1, 2, 4],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "z", "a", "C", "B", "b", "bb"],
+ },
+ {
+ select: [3, 5, 6, 8],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "a", "z", "b", "C", "B", "bb"],
+ },
+ {
+ select: [4, 6, 7, 8],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "a", "b", "z", "C", "B", "bb"],
+ },
+
+ // Prepare scenario for and test 'Group together' (upwards/downwards).
+ {
+ select: [1, 2],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["x", "a", "y1", "y2", "b", "z", "C", "B", "bb"],
+ },
+ {
+ select: [0, 2, 3, 5],
+ // key_moveAttachmentRight
+ key: "VK_RIGHT",
+ key_modifiers: modAlt,
+ result: ["a", "x", "b", "y1", "y2", "C", "z", "B", "bb"],
+ },
+ {
+ select: [1, 3, 4, 6],
+ // key_moveAttachmentBundleUp
+ key: "VK_UP",
+ key_modifiers: modAlt,
+ result: ["a", "x", "y1", "y2", "z", "b", "C", "B", "bb"],
+ },
+ {
+ select: [5, 6],
+ // key_moveAttachmentLeft
+ key: "VK_LEFT",
+ key_modifiers: modAlt,
+ result: ["a", "x", "y1", "y2", "b", "C", "z", "B", "bb"],
+ },
+ {
+ select: [0, 4, 5, 7],
+ // key_moveAttachmentBundleDown
+ key: "VK_DOWN",
+ key_modifiers: modAlt,
+ result: ["x", "y1", "y2", "z", "a", "b", "C", "B", "bb"],
+ },
+
+ // Collapsing multiple, disjunct selection with inner block to top/bottom.
+ {
+ select: [0, 4, 5, 7],
+ // key_moveAttachmentTop
+ key: AppConstants.platform == "macosx" ? "VK_UP" : "VK_HOME",
+ key_modifiers: modifiers2,
+ result: ["x", "a", "b", "B", "y1", "y2", "z", "C", "bb"],
+ },
+ {
+ select: [0, 4, 5, 6],
+ // key_moveAttachmentBottom
+ key: AppConstants.platform == "macosx" ? "VK_DOWN" : "VK_END",
+ key_modifiers: modifiers2,
+ result: ["a", "b", "B", "C", "bb", "x", "y1", "y2", "z"],
+ },
+ {
+ select: [0, 1, 3, 4],
+ // key_moveAttachmentBottom2 (secondary shortcut on MAC, same as Win primary)
+ key: "VK_END",
+ key_modifiers: modAlt,
+ result: ["B", "x", "y1", "y2", "z", "a", "b", "C", "bb"],
+ },
+ {
+ select: [5, 6, 7, 8],
+ // key_moveAttachmentTop2 (secondary shortcut on MAC, same as Win primary)
+ key: "VK_HOME",
+ key_modifiers: modAlt,
+ result: ["a", "b", "C", "bb", "B", "x", "y1", "y2", "z"],
+ },
+ ];
+
+ // Check 4: Alt+Y keyboard shortcut for sorting (Bug 1425891).
+ const initialAttachmentNames_4 = [
+ "a",
+ "x",
+ "C",
+ "y1",
+ "y2",
+ "B",
+ "b",
+ "z",
+ "bb",
+ ];
+
+ const reorderActions_4 = [
+ {
+ select: [1],
+ // key_sortAttachmentsToggle
+ key: "y",
+ key_modifiers: modAlt,
+ result: ["a", "b", "B", "bb", "C", "x", "y1", "y2", "z"],
+ },
+ ];
+
+ // Execute the tests of reordering actions as defined above.
+ await subtest_reordering(cwc, initialAttachmentNames_1, reorderActions_1);
+ await subtest_reordering(cwc, initialAttachmentNames_2, reorderActions_2);
+ // Check 3 (keyboard-only) with panel open.
+ await subtest_reordering(cwc, initialAttachmentNames_3, reorderActions_3);
+ // Check 3 (keyboard-only) without panel.
+ await subtest_reordering(
+ cwc,
+ initialAttachmentNames_3,
+ reorderActions_3,
+ false
+ );
+ // Check 4 (Alt+Y keyboard shortcut for sorting) without panel.
+ await subtest_reordering(
+ cwc,
+ initialAttachmentNames_4,
+ reorderActions_4,
+ false
+ );
+ // Check 4 (Alt+Y keyboard shortcut for sorting) with panel open.
+ await subtest_reordering(cwc, initialAttachmentNames_4, reorderActions_4);
+ // XXX When the root problem of bug 1425891 has been found and fixed, we should
+ // test here if the panel stays open as it should, esp. on Windows.
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_restore_attachment_bucket_height() {
+ let cwc = open_compose_new_mail();
+
+ let attachmentArea = cwc.window.document.getElementById("attachmentArea");
+ let attachmentBucket = cwc.window.document.getElementById("attachmentBucket");
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(attachmentArea),
+ "Attachment area should be hidden initially with no attachments"
+ );
+
+ // Add 9 attachments to open a pane least 2 rows height.
+ let files = [
+ { name: "foo.txt", size: 1234 },
+ { name: "bar.txt", size: 5678 },
+ { name: "baz.txt", size: 9012 },
+ { name: "foo2.txt", size: 1234 },
+ { name: "bar2.txt", size: 5678 },
+ { name: "baz2.txt", size: 9012 },
+ { name: "foo3.txt", size: 1234 },
+ { name: "bar3.txt", size: 5678 },
+ { name: "baz3.txt", size: 9012 },
+ ];
+ for (let i = 0; i < files.length; i++) {
+ add_attachments(cwc, filePrefix + files[i].name, files[i].size);
+ }
+
+ // Store the height of the attachment bucket.
+ let heightBefore = attachmentBucket.getBoundingClientRect().height;
+
+ let modifiers =
+ AppConstants.platform == "macosx"
+ ? { accelKey: true, shiftKey: true }
+ : { ctrlKey: true, shiftKey: true };
+
+ let collapsedPromise = BrowserTestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(attachmentArea) && !attachmentArea.open,
+ "The attachment area should be visible but closed."
+ );
+
+ // Press Ctrl/Cmd+Shift+M to collapse the attachment pane.
+ EventUtils.synthesizeKey("M", modifiers, cwc.window);
+ await collapsedPromise;
+
+ let visiblePromise = BrowserTestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(attachmentArea) && attachmentArea.open,
+ "The attachment area should be visible and open."
+ );
+ // Press Ctrl/Cmd+Shift+M again.
+ EventUtils.synthesizeKey("M", modifiers, cwc.window);
+ await visiblePromise;
+
+ // The height of these elements should have been properly restored.
+ Assert.equal(attachmentBucket.getBoundingClientRect().height, heightBefore);
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_attachmentCloudDraft.js b/comm/mail/test/browser/composition/browser_attachmentCloudDraft.js
new file mode 100644
index 0000000000..5054bd9fed
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_attachmentCloudDraft.js
@@ -0,0 +1,577 @@
+/* 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/. */
+
+/**
+ * Tests that cloudFile attachments are properly restored in re-opened drafts.
+ */
+
+"use strict";
+
+var { gMockFilePicker, gMockFilePickReg } = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+var {
+ close_compose_window,
+ open_compose_new_mail,
+ save_compose_message,
+ setup_msg_contents,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { CloudFileTestProvider } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { get_notification, wait_for_notification_to_show } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var { plan_for_modal_dialog, plan_for_new_window, wait_for_modal_dialog } =
+ ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDrafts;
+var gOutbox;
+var gCloudFileProvider;
+const kFiles = ["./data/attachment.txt"];
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+ gOutbox = await get_special_folder(Ci.nsMsgFolderFlags.Queue);
+ gMockFilePickReg.register();
+ // Register an extension based cloudFile provider.
+ gCloudFileProvider = new CloudFileTestProvider("testProvider");
+ await gCloudFileProvider.register(this);
+});
+
+registerCleanupFunction(async function () {
+ gMockFilePickReg.unregister();
+ await gCloudFileProvider.unregister();
+});
+
+/**
+ * Test reopening a draft with a cloudFile attachment.
+ *
+ * It must be possible to rename the restored cloudFile attachment.
+ * It must be possible to convert the restored cloudFile to a local attachment.
+ */
+add_task(async function test_draft_with_cloudFile_attachment() {
+ // Prepare the mock file picker.
+ let files = collectFiles(kFiles);
+ gMockFilePicker.returnFiles = files;
+
+ let cloudFileAccount = await gCloudFileProvider.createAccount("validAccount");
+ let draft = await createAndCloseDraftWithCloudAttachment(cloudFileAccount);
+ let expectedUpload = { ...draft.upload };
+
+ let cwc = openDraft();
+
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments."
+ );
+ let itemFromDraft = [...bucket.children].find(
+ e => e.attachment.name == "attachment.txt"
+ );
+ Assert.ok(itemFromDraft, "Should have found the attachment item");
+ Assert.ok(itemFromDraft.attachment.sendViaCloud, "Should be a cloudFile.");
+ Assert.equal(
+ draft.url,
+ itemFromDraft.attachment.url,
+ "Should have restored the url of the original attachment, pointing to the local file."
+ );
+ Assert.ok(
+ !itemFromDraft.attachment.temporary,
+ "The attachments local file should not be temporary."
+ );
+ Assert.equal(
+ cloudFileAccount.accountKey,
+ itemFromDraft.attachment.cloudFileAccountKey,
+ "Should have restored the correct account key."
+ );
+
+ expectedUpload.immutable = true;
+ Assert.deepEqual(
+ expectedUpload,
+ itemFromDraft.cloudFileUpload,
+ "Should have found the existing upload."
+ );
+ Assert.equal(
+ draft.itemIcon,
+ itemFromDraft.querySelector("img.attachmentcell-icon").src,
+ "CloudFile icon of draft should match CloudFile icon of original email."
+ );
+ Assert.equal(
+ draft.itemSize,
+ itemFromDraft.querySelector(".attachmentcell-size").textContent,
+ "Attachment size of draft should match attachment size of original email."
+ );
+ Assert.equal(
+ draft.totalSize,
+ cwc.window.document.getElementById("attachmentBucketSize").textContent,
+ "Total size of draft should match total size of original email."
+ );
+
+ // Rename attachment.
+ await cwc.window.UpdateAttachment(itemFromDraft, { name: "renamed.txt" });
+ Assert.equal(
+ "renamed.txt",
+ itemFromDraft.attachment.name,
+ "Renaming a restored cloudFile attachment should succeed."
+ );
+
+ // Convert to regular attachment.
+ await cwc.window.UpdateAttachment(itemFromDraft, { cloudFileAccount: null });
+ Assert.ok(
+ !itemFromDraft.attachment.sendViaCloud,
+ "Converting a restored cloudFile attachment to a regular attachment should succeed."
+ );
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+
+ // Cleanup cloudFile account.
+ await gCloudFileProvider.removeAccount(cloudFileAccount);
+});
+
+/**
+ * Test reopening a draft with a cloudFile attachment, which is not know to the
+ * current session.
+ *
+ * It must be possible to rename the restored cloudFile attachment.
+ * It must be possible to convert the restored cloudFile to a local attachment.
+ */
+add_task(async function test_draft_with_unknown_cloudFile_attachment() {
+ // Prepare the mock file picker.
+ let files = collectFiles(kFiles);
+ gMockFilePicker.returnFiles = files;
+
+ let cloudFileAccount = await gCloudFileProvider.createAccount(
+ "validAccountUnknownUpload"
+ );
+ let draft = await createAndCloseDraftWithCloudAttachment(cloudFileAccount);
+ let expectedUpload = { ...draft.upload };
+
+ // Change the known upload, so the draft comes back as unknown.
+ let id1 = cloudFileAccount._uploads.get(1);
+ id1.serviceName = "wrongService";
+ cloudFileAccount._uploads.set(1, id1);
+
+ let cwc = openDraft();
+
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments."
+ );
+ let itemFromDraft = [...bucket.children].find(
+ e => e.attachment.name == "attachment.txt"
+ );
+ Assert.ok(itemFromDraft, "Should have found the attachment item");
+ Assert.ok(itemFromDraft.attachment.sendViaCloud, "Should be a cloudFile.");
+ Assert.equal(
+ draft.url,
+ itemFromDraft.attachment.url,
+ "Should have restored the url of the original attachment, pointing to the local file."
+ );
+ Assert.ok(
+ !itemFromDraft.attachment.temporary,
+ "The attachments local file should not be temporary."
+ );
+ Assert.equal(
+ cloudFileAccount.accountKey,
+ itemFromDraft.attachment.cloudFileAccountKey,
+ "Should have restored the correct account key."
+ );
+
+ expectedUpload.id = 2;
+ expectedUpload.immutable = true;
+ Assert.deepEqual(
+ expectedUpload,
+ itemFromDraft.cloudFileUpload,
+ "Should have created a new upload with id = 2."
+ );
+ Assert.equal(
+ draft.itemIcon,
+ itemFromDraft.querySelector("img.attachmentcell-icon").src,
+ "CloudFile icon of draft should match CloudFile icon of original email."
+ );
+ Assert.equal(
+ draft.itemSize,
+ itemFromDraft.querySelector(".attachmentcell-size").textContent,
+ "Attachment size of draft should match attachment size of original email."
+ );
+ Assert.equal(
+ draft.totalSize,
+ cwc.window.document.getElementById("attachmentBucketSize").textContent,
+ "Total size of draft should match total size of original email."
+ );
+
+ // Rename attachment.
+ await cwc.window.UpdateAttachment(itemFromDraft, { name: "renamed.txt" });
+ Assert.equal(
+ "renamed.txt",
+ itemFromDraft.attachment.name,
+ "Renaming an unknown cloudFile attachment should succeed."
+ );
+
+ // Convert to regular attachment.
+ await cwc.window.UpdateAttachment(itemFromDraft, { cloudFileAccount: null });
+ Assert.ok(
+ !itemFromDraft.attachment.sendViaCloud,
+ "Converting an unknown cloudFile attachment to a regular attachment should succeed."
+ );
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+
+ // Cleanup cloudFile account.
+ await gCloudFileProvider.removeAccount(cloudFileAccount);
+});
+
+/**
+ * Test reopening a draft with a cloudFile attachment, whose account has been
+ * deleted.
+ *
+ * It must NOT be possible to rename the restored cloudFile attachment.
+ * It must be possible to convert the restored cloudFile to a local attachment.
+ */
+add_task(async function test_draft_with_cloudFile_attachment_no_account() {
+ // Prepare the mock file picker.
+ let files = collectFiles(kFiles);
+ gMockFilePicker.returnFiles = files;
+
+ let cloudFileAccount = await gCloudFileProvider.createAccount(
+ "invalidAccount"
+ );
+ let draft = await createAndCloseDraftWithCloudAttachment(cloudFileAccount);
+ let expectedUpload = { ...draft.upload };
+
+ // Remove account.
+ await gCloudFileProvider.removeAccount(cloudFileAccount);
+
+ let cwc = openDraft();
+
+ // Check that the draft has a cloudFile attachment.
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments."
+ );
+ let itemFromDraft = [...bucket.children].find(
+ e => e.attachment.name == "attachment.txt"
+ );
+ Assert.ok(itemFromDraft, "Should have found the attachment item");
+ Assert.ok(itemFromDraft.attachment.sendViaCloud, "Should be a cloudFile.");
+ Assert.equal(
+ draft.url,
+ itemFromDraft.attachment.url,
+ "Should have restored the url of the original attachment, pointing to the local file."
+ );
+ Assert.ok(
+ !itemFromDraft.attachment.temporary,
+ "The attachments local file should not be temporary."
+ );
+ Assert.equal(
+ cloudFileAccount.accountKey,
+ itemFromDraft.attachment.cloudFileAccountKey,
+ "Should have restored the correct account key."
+ );
+
+ delete expectedUpload.id;
+ expectedUpload.immutable = false;
+ Assert.deepEqual(
+ expectedUpload,
+ itemFromDraft.cloudFileUpload,
+ "Should have restored the upload from the draft without an id and immutable = false."
+ );
+ Assert.equal(
+ draft.itemIcon,
+ itemFromDraft.querySelector("img.attachmentcell-icon").src,
+ "CloudFile icon of draft should match CloudFile icon of original email."
+ );
+ Assert.equal(
+ draft.itemSize,
+ itemFromDraft.querySelector(".attachmentcell-size").textContent,
+ "Attachment size of draft should match attachment size of original email."
+ );
+ Assert.equal(
+ draft.totalSize,
+ cwc.window.document.getElementById("attachmentBucketSize").textContent,
+ "Total size of draft should match total size of original email."
+ );
+
+ // Rename attachment.
+ await Assert.rejects(
+ cwc.window.UpdateAttachment(itemFromDraft, { name: "renamed.txt" }),
+ /CloudFile Error: Account not found: undefined/,
+ "Renaming a restored cloudFile attachment (without account) should not succeed."
+ );
+
+ // Convert to regular attachment.
+ await cwc.window.UpdateAttachment(itemFromDraft, { cloudFileAccount: null });
+ Assert.ok(
+ !itemFromDraft.attachment.sendViaCloud,
+ "Converting a restored cloudFile attachment (without account) to a regular attachment should succeed."
+ );
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+});
+
+/**
+ * Test reopening a draft with a cloudFile attachment, whose local file has been
+ * deleted.
+ *
+ * It must NOT be possible to rename the restored cloudFile attachment.
+ * It must NOT be possible to convert the restored cloudFile to a local attachment.
+ */
+add_task(async function test_draft_with_cloudFile_attachment_no_file() {
+ // Prepare the mock file picker.
+ let tempFile = await createAttachmentFile(
+ "attachment.txt",
+ "This is a sample text."
+ );
+ gMockFilePicker.returnFiles = [tempFile.file];
+
+ let cloudFileAccount = await gCloudFileProvider.createAccount(
+ "validAccountNoFile"
+ );
+ let draft = await createAndCloseDraftWithCloudAttachment(cloudFileAccount);
+ let expectedUpload = { ...draft.upload };
+
+ // Remove local file of cloudFile attachment.
+ await IOUtils.remove(tempFile.path);
+
+ let cwc = openDraft();
+
+ // Check that the draft has a cloudFile attachment.
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments."
+ );
+ let itemFromDraft = [...bucket.children].find(
+ e => e.attachment.name == "attachment.txt"
+ );
+ Assert.ok(itemFromDraft, "Should have found the attachment item");
+ Assert.ok(itemFromDraft.attachment.sendViaCloud, "Should be a cloudFile.");
+ Assert.notEqual(
+ draft.url,
+ itemFromDraft.attachment.url,
+ "Should NOT have restored the url of the original attachment."
+ );
+ Assert.ok(
+ itemFromDraft.attachment.url.endsWith(".html"),
+ "The attachments url should still point to the html placeholder file."
+ );
+ Assert.ok(
+ itemFromDraft.attachment.temporary,
+ "The attachments html placeholder file should be temporary."
+ );
+
+ Assert.equal(
+ cloudFileAccount.accountKey,
+ itemFromDraft.attachment.cloudFileAccountKey,
+ "Should have restored the correct account key."
+ );
+
+ expectedUpload.immutable = true;
+ Assert.deepEqual(
+ expectedUpload,
+ itemFromDraft.cloudFileUpload,
+ "Should have restored the correct upload."
+ );
+ Assert.equal(
+ draft.itemIcon,
+ itemFromDraft.querySelector("img.attachmentcell-icon").src,
+ "CloudFile icon of draft should match CloudFile icon of original email."
+ );
+ Assert.equal(
+ draft.itemSize,
+ itemFromDraft.querySelector(".attachmentcell-size").textContent,
+ "Attachment size of draft should match attachment size of original email."
+ );
+ Assert.equal(
+ draft.totalSize,
+ cwc.window.document.getElementById("attachmentBucketSize").textContent,
+ "Total size of draft should match total size of original email."
+ );
+
+ // Rename attachment.
+ await Assert.rejects(
+ cwc.window.UpdateAttachment(itemFromDraft, { name: "renamed.txt" }),
+ e => {
+ return (
+ e.message.startsWith("CloudFile Error: Attachment file not found: ") &&
+ e.message.endsWith("attachment.txt")
+ );
+ },
+ "Renaming a restored cloudFile attachment (without local file) should not succeed."
+ );
+
+ // Rename attachment.
+ await Assert.rejects(
+ cwc.window.UpdateAttachment(itemFromDraft, { name: "renamed.txt" }),
+ e => {
+ return (
+ e.message.startsWith("CloudFile Error: Attachment file not found: ") &&
+ e.message.endsWith("attachment.txt")
+ );
+ },
+ "Renaming a restored cloudFile attachment (without local file) should not succeed."
+ );
+
+ // Convert to regular attachment.
+ await Assert.rejects(
+ cwc.window.UpdateAttachment(itemFromDraft, { cloudFileAccount: null }),
+ e => {
+ return (
+ e.message.startsWith("CloudFile Error: Attachment file not found: ") &&
+ e.message.endsWith("attachment.txt")
+ );
+ },
+ "Converting a restored cloudFile attachment (without local file) to a regular attachment should not succeed."
+ );
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+
+ // Cleanup cloudFile account.
+ await gCloudFileProvider.removeAccount(cloudFileAccount);
+});
+
+async function createAndCloseDraftWithCloudAttachment(cloudFileAccount) {
+ // Open a sample message.
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ `Testing drafts with cloudFiles for provider ${cloudFileAccount.displayName}!`,
+ "Some body..."
+ );
+
+ await cwc.window.attachToCloudNew(cloudFileAccount);
+
+ let bucket = cwc.window.document.getElementById("attachmentBucket");
+ Assert.equal(
+ bucket.itemCount,
+ kFiles.length,
+ "Should find correct number of attachments."
+ );
+ let item = [...bucket.children].find(
+ e => e.attachment.name == "attachment.txt"
+ );
+ Assert.ok(item, "Should have found the attachment item");
+ Assert.ok(item.attachment.sendViaCloud, "Should be a cloudFile.");
+ Assert.equal(
+ cloudFileAccount.accountKey,
+ item.attachment.cloudFileAccountKey,
+ "Should have the correct account key."
+ );
+ Assert.deepEqual(
+ cloudFileAccount,
+ item.cloudFileAccount,
+ "Should have the correct cloudFileAccount."
+ );
+
+ let url = item.attachment.url;
+ let upload = item.cloudFileUpload;
+ let itemIcon = item.querySelector("img.attachmentcell-icon").src;
+ let itemSize = item.querySelector(".attachmentcell-size").textContent;
+ let totalSize = cwc.window.document.getElementById(
+ "attachmentBucketSize"
+ ).textContent;
+
+ Assert.equal(
+ itemIcon,
+ "chrome://messenger/content/extension.svg",
+ "CloudFile icon should be correct."
+ );
+
+ // Now close the message with saving it as draft.
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ // The draft message was saved into Local Folders/Drafts.
+ await be_in_folder(gDrafts);
+
+ return { upload, url, itemIcon, itemSize, totalSize };
+}
+
+function openDraft() {
+ select_click_row(0);
+ let aboutMessage = get_about_message();
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // Edit the draft again...
+ plan_for_new_window("msgcompose");
+ let box = get_notification(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // ... by clicking Edit in the draft message notification bar.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ return wait_for_compose_window();
+}
+
+/**
+ * Click Save in the Save message dialog.
+ */
+function click_save_message(controller) {
+ if (controller.window.document.title != "Save Message") {
+ throw new Error(
+ "Not a Save message dialog; title=" + controller.window.document.title
+ );
+ }
+ controller.window.document
+ .querySelector("dialog")
+ .getButton("accept")
+ .doCommand();
+}
+
+function collectFiles(files) {
+ return files.map(filename => new FileUtils.File(getTestFilePath(filename)));
+}
+
+async function createAttachmentFile(filename, content) {
+ let tempPath = PathUtils.join(PathUtils.tempDir, filename);
+ await IOUtils.writeUTF8(tempPath, content);
+ return {
+ path: tempPath,
+ file: new FileUtils.File(tempPath),
+ };
+}
diff --git a/comm/mail/test/browser/composition/browser_attachmentDragDrop.js b/comm/mail/test/browser/composition/browser_attachmentDragDrop.js
new file mode 100644
index 0000000000..bc826574c7
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_attachmentDragDrop.js
@@ -0,0 +1,689 @@
+/* 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/. */
+
+/**
+ * Tests the Drag and Drop functionalities of the attachment bucket in the
+ * message compose window.
+ */
+
+"use strict";
+
+var { CloudFileTestProvider } = ChromeUtils.import(
+ "resource://testing-common/mozmill/CloudfileHelpers.jsm"
+);
+var { gMockFilePicker, gMockFilePickReg } = ChromeUtils.import(
+ "resource://testing-common/mozmill/AttachmentHelpers.jsm"
+);
+
+var { open_compose_new_mail, close_compose_window, add_attachments } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ be_in_folder,
+ create_folder,
+ create_message,
+ FAKE_SERVER_HOSTNAME,
+ get_about_message,
+ inboxFolder,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+);
+
+var gCloudFileProvider;
+var gCloudFileAccount;
+const kFiles = [
+ "./data/attachment.txt",
+ "./data/base64-bug1586890.eml",
+ "./data/base64-encoded-msg.eml",
+ "./data/base64-with-whitespace.eml",
+ "./data/body-greek.eml",
+ "./data/body-utf16.eml",
+];
+
+add_setup(async function () {
+ // Prepare the mock file picker.
+ gMockFilePickReg.register();
+ gMockFilePicker.returnFiles = collectFiles(kFiles);
+
+ // Register an extension based cloudFile provider.
+ gCloudFileProvider = new CloudFileTestProvider("testProvider");
+ await gCloudFileProvider.register(this);
+ gCloudFileAccount = await gCloudFileProvider.createAccount("testAccount");
+});
+
+registerCleanupFunction(async function () {
+ gMockFilePickReg.unregister();
+ // Remove the cloudFile account and unregister the provider.
+ await gCloudFileProvider.removeAccount(gCloudFileAccount);
+ await gCloudFileProvider.unregister();
+});
+
+function getDragOverTarget(win) {
+ return win.document.getElementById("messageArea");
+}
+
+function getDropTarget(win) {
+ return win.document.getElementById("dropAttachmentOverlay");
+}
+
+function initDragSession({ dragData, dropEffect }) {
+ let dropAction;
+ switch (dropEffect) {
+ case null:
+ case undefined:
+ case "move":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
+ break;
+ case "copy":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_COPY;
+ break;
+ case "link":
+ dropAction = Ci.nsIDragService.DRAGDROP_ACTION_LINK;
+ break;
+ default:
+ throw new Error(`${dropEffect} is an invalid drop effect value`);
+ }
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.dropEffect = dropEffect;
+
+ for (let i = 0; i < dragData.length; i++) {
+ const item = dragData[i];
+ for (let j = 0; j < item.length; j++) {
+ dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+ }
+ }
+
+ dragService.startDragSessionForTests(dropAction);
+ const session = dragService.getCurrentSession();
+ session.dataTransfer = dataTransfer;
+
+ return session;
+}
+
+/**
+ * Helper method to simulate a drag and drop action above the window.
+ */
+async function simulateDragAndDrop(win, dragData, type) {
+ let dropTarget = getDropTarget(win);
+ let dragOverTarget = getDragOverTarget(win);
+ let dropEffect = "move";
+
+ let session = initDragSession({ dragData, dropEffect });
+
+ info("Simulate drag over and wait for the drop target to be visible");
+
+ EventUtils.synthesizeDragOver(
+ dragOverTarget,
+ dragOverTarget,
+ dragData,
+ dropEffect,
+ win
+ );
+
+ // This make sure that the fake dataTransfer has still
+ // the expected drop effect after the synthesizeDragOver call.
+ session.dataTransfer.dropEffect = "move";
+
+ await BrowserTestUtils.waitForCondition(
+ () => dropTarget.classList.contains("show"),
+ "Wait for the drop target element to be visible"
+ );
+
+ // If the dragged file is an image, the attach inline container should be
+ // visible.
+ if (type == "image" || type == "inline" || type == "link") {
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ !win.document.getElementById("addInline").classList.contains("hidden"),
+ "Wait for the addInline element to be visible"
+ );
+ } else {
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ win.document.getElementById("addInline").classList.contains("hidden"),
+ "Wait for the addInline element to be hidden"
+ );
+ }
+
+ if (type == "inline") {
+ // Change the drop target to the #addInline container.
+ dropTarget = win.document.getElementById("addInline");
+ }
+
+ info("Simulate drop dragData on drop target");
+
+ EventUtils.synthesizeDropAfterDragOver(
+ null,
+ session.dataTransfer,
+ dropTarget,
+ win,
+ { _domDispatchOnly: true }
+ );
+
+ if (type == "inline") {
+ let editor = win.GetCurrentEditor();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ editor.selectAll();
+ return editor.getSelectedElement("img");
+ }, "Confirm the image was added to the message body");
+
+ Assert.equal(
+ win.document.getElementById("attachmentBucket").itemCount,
+ 0,
+ "Confirm the file hasn't been attached"
+ );
+ } else {
+ // The dropped files should have been attached.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ win.document.getElementById("attachmentBucket").itemCount ==
+ dragData.length,
+ "Wait for the file to be attached"
+ );
+ }
+
+ dragService.endDragSession(true);
+}
+
+/**
+ * Test how the attachment overlay reacts to an image file being dragged above
+ * the message compose window.
+ */
+add_task(async function test_image_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/tb-logo.png"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "image"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test how the attachment overlay reacts to an image file being dragged above
+ * the message compose window and dropped above the inline container.
+ */
+add_task(async function test_image_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/tb-logo.png"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "inline"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test how the attachment overlay reacts to a text file being dragged above
+ * the message compose window.
+ */
+add_task(async function test_text_file_drag() {
+ let file = new FileUtils.File(getTestFilePath("data/attachment.txt"));
+ let cwc = open_compose_new_mail();
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [[{ type: "application/x-moz-file", data: file }]],
+ "text"
+ );
+
+ close_compose_window(cwc);
+});
+
+add_task(async function test_message_drag() {
+ let folder = await create_folder("dragondrop");
+ let subject = "Dragons don't drop from the sky";
+ let body = "Dragons can fly after all.";
+ await be_in_folder(folder);
+ await add_message_to_folder(
+ [folder],
+ create_message({ subject, body: { body } })
+ );
+ select_click_row(0);
+
+ let msgStr = get_about_message().gMessageURI;
+ let msgUrl = MailServices.messageServiceFromURI(msgStr).getUrlForUri(msgStr);
+
+ let cwc = open_compose_new_mail();
+ let attachmentBucket = cwc.window.document.getElementById("attachmentBucket");
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ { type: "text/x-moz-message", data: msgStr },
+ { type: "text/x-moz-url", data: msgUrl.spec },
+ {
+ type: "application/x-moz-file-promise-url",
+ data: msgUrl.spec + "?fileName=" + encodeURIComponent("message.eml"),
+ },
+ {
+ type: "application/x-moz-file-promise",
+ data: new window.messageFlavorDataProvider(),
+ },
+ ],
+ ],
+ "message"
+ );
+
+ let attachment = attachmentBucket.childNodes[0].attachment;
+ Assert.equal(
+ attachment.name,
+ "Dragons don't drop from the sky.eml",
+ "attachment should have expected file name"
+ );
+ Assert.equal(
+ attachment.contentType,
+ "message/rfc822",
+ "attachment should say it's a message"
+ );
+ Assert.notEqual(attachment, 0, "attachment should not be 0 bytes");
+
+ // Clear the added attachment.
+ await cwc.window.RemoveAttachments([attachmentBucket.childNodes[0]]);
+
+ // Try the same with mail.forward_add_extension false.
+ Services.prefs.setBoolPref("mail.forward_add_extension", false);
+
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ { type: "text/x-moz-message", data: msgStr },
+ { type: "text/x-moz-url", data: msgUrl.spec },
+ {
+ type: "application/x-moz-file-promise-url",
+ data: msgUrl.spec + "?fileName=" + encodeURIComponent("message.eml"),
+ },
+ {
+ type: "application/x-moz-file-promise",
+ data: new window.messageFlavorDataProvider(),
+ },
+ ],
+ ],
+ "message"
+ );
+
+ let attachment2 = attachmentBucket.childNodes[0].attachment;
+ Assert.equal(
+ attachment2.name,
+ "Dragons don't drop from the sky",
+ "attachment2 should have expected file name"
+ );
+ Assert.equal(
+ attachment2.contentType,
+ "message/rfc822",
+ "attachment2 should say it's a message"
+ );
+ Assert.notEqual(attachment2, 0, "attachment2 should not be 0 bytes");
+
+ Services.prefs.clearUserPref("mail.forward_add_extension");
+
+ close_compose_window(cwc);
+ await be_in_folder(inboxFolder);
+ folder.deleteSelf(null);
+});
+
+add_task(async function test_link_drag() {
+ let cwc = open_compose_new_mail();
+ await simulateDragAndDrop(
+ cwc.window,
+ [
+ [
+ {
+ type: "text/uri-list",
+ data: "https://example.com",
+ },
+ {
+ type: "text/x-moz-url",
+ data: "https://example.com\nExample website",
+ },
+ { type: "application/x-moz-file", data: "" },
+ ],
+ ],
+ "link"
+ );
+
+ let attachment =
+ cwc.window.document.getElementById("attachmentBucket").childNodes[0]
+ .attachment;
+ Assert.equal(
+ attachment.name,
+ "Example website",
+ "Attached link has expected name"
+ );
+ Assert.equal(
+ attachment.url,
+ "https://example.com",
+ "Attached link has correct URL"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Get the attachment item for the given url.
+ *
+ * @param {Element} bucket - The element to search in for the attachment item.
+ * @param {string} url - The url of the attachment to find.
+ *
+ * @returns {Element?} - The item with the given attachment url, or null if none
+ * was found.
+ */
+function getAttachmentItem(bucket, url) {
+ for (let child of bucket.childNodes) {
+ if (child.attachment.url == url) {
+ return child;
+ }
+ }
+ return null;
+}
+
+/**
+ * Assert that the given bucket has the given selected items.
+ *
+ * @param {Element} bucket - The bucket to check.
+ * @param {Element[]} selectedItems - The expected selected items in the bucket.
+ */
+function assertSelection(bucket, selectedItems) {
+ for (let child of bucket.childNodes) {
+ if (selectedItems.includes(child)) {
+ Assert.ok(
+ child.selected,
+ `${child.attachment.url} item should be selected`
+ );
+ } else {
+ Assert.ok(
+ !child.selected,
+ `${child.attachment.url} item should not be selected`
+ );
+ }
+ }
+}
+
+/**
+ * Select the given attachment items in the bucket.
+ *
+ * @param {Element} bucket - The attachment bucket to select from.
+ * @param {Element[]} itemSet - The set of attachment items to select. This must
+ * contain at least one item.
+ */
+function selectAttachments(bucket, itemSet) {
+ let win = bucket.ownerGlobal;
+ let first = true;
+ for (let item of itemSet) {
+ item.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(item, { ctrlKey: !first }, win);
+ first = false;
+ }
+ assertSelection(bucket, itemSet);
+}
+
+/**
+ * Perform a single drag operation between attachment buckets.
+ *
+ * @param {Element} dragSrc - The attachment item to start dragging from.
+ * @param {Element} destBucket - The attachment bucket to drag to.
+ * @param {string[]} expectUrls - The expected list of all the attachment urls
+ * in the destBucket after the drop. Note this should include both the current
+ * attachments as well as the expected gained attachments.
+ */
+async function moveAttachments(dragSrc, destBucket, expectUrls) {
+ let srcWindow = dragSrc.ownerGlobal;
+ let destWindow = destBucket.ownerGlobal;
+ let dragOverTarget = getDragOverTarget(destWindow);
+ let dropTarget = getDropTarget(destWindow);
+
+ let [dragOverResult, dataTransfer] = EventUtils.synthesizeDragOver(
+ dragSrc,
+ dragOverTarget,
+ null,
+ null,
+ srcWindow,
+ destWindow
+ );
+
+ EventUtils.synthesizeDropAfterDragOver(
+ dragOverResult,
+ dataTransfer,
+ dropTarget,
+ destWindow
+ );
+
+ await TestUtils.waitForCondition(
+ () => destBucket.itemCount == expectUrls.length,
+ `Destination bucket has ${expectUrls.length} attachments`
+ );
+ let items = Array.from(destBucket.childNodes);
+ for (let i = 0; i < items.length; i++) {
+ Assert.ok(
+ items[i].attachment.url.startsWith("file://") &&
+ items[i].attachment.url.includes(expectUrls[i].split(".")[0]),
+ `Attachment url ${items[i].attachment.url} should be the correct file:// url`
+ );
+ }
+}
+
+/**
+ * Perform a series of drag and drop of attachments from the given source bucket
+ * to the given destination bucket.
+ *
+ * The dragged attachment will be saved as a local temporary file. This test
+ * extracts the filename from the url and checks if the url of the attachment
+ * in the destBucket is a file:// url and has the correct file name.
+ * The original url is a mailbox:// url:
+ * mailbox:///something?number=1&part=1.4&filename=file2.txt
+ *
+ * @param {Element} srcBucket - The bucket to drag from. It must contain 6
+ * attachment items and be open.
+ * @param {Element} destBucket - The bucket to drag to. It must be empty.
+ */
+async function drag_between_buckets(srcBucket, destBucket) {
+ Assert.equal(srcBucket.itemCount, 6, "Src bucket starts with 6 attachments");
+ Assert.equal(
+ destBucket.itemCount,
+ 0,
+ "Dest bucket starts with no attachments"
+ );
+
+ let attachmentSet = Array.from(srcBucket.childNodes, item => {
+ return { url: item.attachment.url, srcItem: item };
+ });
+
+ let dragSession = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+ dragSession.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_MOVE);
+
+ // NOTE: Attachment #4 is never dragged from the source to the destination
+ // bucket as part of this test.
+
+ let destUrls = [];
+
+ // Select attachment #2, and drag it.
+ selectAttachments(srcBucket, [attachmentSet[2].srcItem]);
+ destUrls.push(attachmentSet[2].url.split("=").pop());
+ await moveAttachments(attachmentSet[2].srcItem, destBucket, destUrls);
+
+ // Start with attachment #3 selected, but drag attachment #1.
+ // The drag operation should at first change the selection to attachment #1,
+ // such that it becomes the transferred file.
+ selectAttachments(srcBucket, [attachmentSet[3].srcItem]);
+ destUrls.push(attachmentSet[1].url.split("=").pop());
+ await moveAttachments(attachmentSet[1].srcItem, destBucket, destUrls);
+ // Confirm that attachment #1 was selected.
+ assertSelection(srcBucket, [attachmentSet[1].srcItem]);
+
+ // Select two attachments. And then start a drag on one of them.
+ // We expect both the selected attachments to move.
+ selectAttachments(srcBucket, [
+ attachmentSet[0].srcItem,
+ attachmentSet[3].srcItem,
+ ]);
+ destUrls.push(
+ attachmentSet[0].url.split("=").pop(),
+ attachmentSet[3].url.split("=").pop()
+ );
+ await moveAttachments(attachmentSet[3].srcItem, destBucket, destUrls);
+
+ // Select three attachments, two of which are already added.
+ // Expect the new one to be added.
+ selectAttachments(srcBucket, [
+ attachmentSet[1].srcItem,
+ attachmentSet[5].srcItem,
+ attachmentSet[2].srcItem,
+ ]);
+ destUrls.push(attachmentSet[5].url.split("=").pop());
+ await moveAttachments(attachmentSet[1].srcItem, destBucket, destUrls);
+ dragService.endDragSession(true);
+}
+
+/**
+ * Test dragging regular attachments from one composition window to another.
+ */
+add_task(async function test_drag_and_drop_between_composition_windows() {
+ let ctrlSrc = open_compose_new_mail();
+ let ctrlDest = open_compose_new_mail();
+
+ // Add attachments (via mocked file picker).
+ await ctrlSrc.window.AttachFile();
+
+ let srcAttachmentArea =
+ ctrlSrc.window.document.getElementById("attachmentArea");
+
+ // Wait for attachment area to be visible and open in response.
+ await TestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(srcAttachmentArea) && srcAttachmentArea.open,
+ "Attachment area is visible and open"
+ );
+
+ let srcBucket = ctrlSrc.window.document.getElementById("attachmentBucket");
+ let dstBucket = ctrlDest.window.document.getElementById("attachmentBucket");
+ await drag_between_buckets(srcBucket, dstBucket);
+
+ // Make sure a dragged attachment can be converted to a cloudFile attachment.
+ try {
+ await ctrlSrc.window.UpdateAttachment(dstBucket.childNodes[0], {
+ cloudFileAccount: gCloudFileAccount,
+ });
+ Assert.ok(
+ dstBucket.childNodes[0].attachment.sendViaCloud,
+ "Regular attachment should have been converted to a cloudFile attachment."
+ );
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Converting a drag'n'dropped regular attachment to a cloudFile attachment should succeed: ${ex.message}`
+ );
+ }
+
+ close_compose_window(ctrlSrc);
+ close_compose_window(ctrlDest);
+});
+
+/**
+ * Test dragging cloudFile attachments from one composition window to another.
+ */
+add_task(async function test_cloud_drag_and_drop_between_composition_windows() {
+ let ctrlSrc = open_compose_new_mail();
+ let ctrlDest = open_compose_new_mail();
+
+ // Add cloudFile attachments (via mocked file picker).
+ await ctrlSrc.window.attachToCloudNew(gCloudFileAccount);
+
+ let srcAttachmentArea =
+ ctrlSrc.window.document.getElementById("attachmentArea");
+
+ // Wait for attachment area to be visible and open in response.
+ await TestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(srcAttachmentArea) && srcAttachmentArea.open,
+ "Attachment area is visible and open"
+ );
+
+ let srcBucket = ctrlSrc.window.document.getElementById("attachmentBucket");
+ let dstBucket = ctrlDest.window.document.getElementById("attachmentBucket");
+ await drag_between_buckets(srcBucket, dstBucket);
+
+ // Make sure a dragged cloudFile attachment can be converted to a regular
+ // attachment.
+ try {
+ await ctrlSrc.window.UpdateAttachment(dstBucket.childNodes[0], {
+ cloudFileAccount: null,
+ });
+ Assert.ok(
+ !dstBucket.childNodes[0].attachment.sendViaCloud,
+ "CloudFile Attachment should have been converted to a regular attachment."
+ );
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Converting a drag'n'dropped cloudFile attachment to a regular attachment should succeed: ${ex.message}`
+ );
+ }
+
+ close_compose_window(ctrlSrc);
+ close_compose_window(ctrlDest);
+});
+
+/**
+ * Test dragging attachments from a message into a composition window.
+ */
+add_task(async function test_drag_and_drop_between_composition_windows() {
+ let ctrlDest = open_compose_new_mail();
+
+ let folder = await create_folder("AttachmentsForComposition");
+ await add_message_to_folder(
+ [folder],
+ create_message({
+ attachments: [0, 1, 2, 3, 4, 5].map(num => {
+ return {
+ body: "Some Text",
+ filename: `file${num}.txt`,
+ format: "text/plain",
+ };
+ }),
+ })
+ );
+ await be_in_folder(folder);
+ select_click_row(0);
+ let aboutMessage = get_about_message();
+ let srcAttachmentArea =
+ aboutMessage.document.getElementById("attachmentView");
+ Assert.ok(!srcAttachmentArea.collapsed, "Attachment area is visible");
+
+ let srcBucket = aboutMessage.document.getElementById("attachmentList");
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentBar"),
+ {},
+ aboutMessage
+ );
+ Assert.ok(!srcBucket.collapsed, "Attachment list is visible");
+
+ await drag_between_buckets(
+ srcBucket,
+ ctrlDest.window.document.getElementById("attachmentBucket")
+ );
+
+ close_compose_window(ctrlDest);
+});
+
+function collectFiles(files) {
+ return files.map(filename => new FileUtils.File(getTestFilePath(filename)));
+}
diff --git a/comm/mail/test/browser/composition/browser_attachmentReminder.js b/comm/mail/test/browser/composition/browser_attachmentReminder.js
new file mode 100644
index 0000000000..8566f9b4cf
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_attachmentReminder.js
@@ -0,0 +1,892 @@
+/* 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/. */
+
+/**
+ * Tests that the attachment reminder works properly.
+ */
+
+"use strict";
+
+var {
+ add_attachments,
+ close_compose_window,
+ open_compose_new_mail,
+ save_compose_message,
+ setup_msg_contents,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { delete_all_existing } = ChromeUtils.import(
+ "resource://testing-common/mozmill/KeyboardHelpers.jsm"
+);
+var {
+ assert_notification_displayed,
+ check_notification_displayed,
+ get_notification_button,
+ get_notification,
+ wait_for_notification_to_show,
+ wait_for_notification_to_stop,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var {
+ click_menus_in_sequence,
+ plan_for_modal_dialog,
+ plan_for_new_window,
+ plan_for_window_close,
+ wait_for_modal_dialog,
+ wait_for_window_close,
+ wait_for_window_focused,
+} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+let aboutMessage = get_about_message();
+
+var kBoxId = "compose-notification-bottom";
+var kNotificationId = "attachmentReminder";
+var kReminderPref = "mail.compose.attachment_reminder";
+var gDrafts;
+var gOutbox;
+
+add_setup(async function () {
+ requestLongerTimeout(4);
+
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+ gOutbox = await get_special_folder(Ci.nsMsgFolderFlags.Queue);
+
+ Assert.ok(Services.prefs.getBoolPref(kReminderPref));
+});
+
+/**
+ * Check if the attachment reminder bar is in the wished state.
+ *
+ * @param aCwc A compose window controller.
+ * @param aShown True for expecting the bar to be shown, false otherwise.
+ *
+ * @returns If the bar is shown, return the notification object.
+ */
+function assert_automatic_reminder_state(aCwc, aShown) {
+ return assert_notification_displayed(
+ aCwc.window,
+ kBoxId,
+ kNotificationId,
+ aShown
+ );
+}
+
+/**
+ * Waits for the attachment reminder bar to change into the wished state.
+ *
+ * @param aCwc A compose window controller.
+ * @param aShown True for waiting for the bar to be shown,
+ * false for waiting for it to be hidden.
+ * @param aDelay Set to true to sleep a while to give the notification time
+ * to change. This is used if the state is already what we want
+ * but we expect it could change in a short while.
+ */
+async function wait_for_reminder_state(aCwc, aShown, aDelay = false) {
+ const notificationSlackTime = 5000;
+
+ if (aShown) {
+ if (aDelay) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, notificationSlackTime));
+ }
+ // This waits up to 30 seconds for the notification to appear.
+ wait_for_notification_to_show(aCwc.window, kBoxId, kNotificationId);
+ } else if (
+ check_notification_displayed(aCwc.window, kBoxId, kNotificationId)
+ ) {
+ // This waits up to 30 seconds for the notification to disappear.
+ wait_for_notification_to_stop(aCwc.window, kBoxId, kNotificationId);
+ } else {
+ // This waits 5 seconds during which the notification must not appear.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, notificationSlackTime));
+ assert_automatic_reminder_state(aCwc, false);
+ }
+}
+
+/**
+ * Check whether the manual reminder is in the proper state.
+ *
+ * @param aCwc A compose window controller.
+ * @param aChecked Whether the reminder should be enabled.
+ */
+function assert_manual_reminder_state(aCwc, aChecked) {
+ const remindCommand = "cmd_remindLater";
+ Assert.equal(
+ aCwc.window.document
+ .getElementById("button-attachPopup_remindLaterItem")
+ .getAttribute("command"),
+ remindCommand
+ );
+
+ let checkedValue = aChecked ? "true" : "false";
+ Assert.equal(
+ aCwc.window.document.getElementById(remindCommand).getAttribute("checked"),
+ checkedValue
+ );
+}
+
+/**
+ * Returns the keywords string currently shown in the notification message.
+ *
+ * @param {MozMillController} cwc - The compose window controller.
+ */
+function get_reminder_keywords(cwc) {
+ assert_automatic_reminder_state(cwc, true);
+ let box = get_notification(cwc.window, kBoxId, kNotificationId);
+ return box.messageText.querySelector("#attachmentKeywords").textContent;
+}
+
+/**
+ * Test that the attachment reminder works, in general.
+ */
+add_task(async function test_attachment_reminder_appears_properly() {
+ let cwc = open_compose_new_mail();
+
+ // There should be no notification yet.
+ assert_automatic_reminder_state(cwc, false);
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "Testing automatic reminder!",
+ "Hello! "
+ );
+
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Seen this cool attachment?", cwc.window);
+
+ // Give the notification time to appear. It should now.
+ await wait_for_reminder_state(cwc, true);
+
+ // The manual reminder should be disabled yet.
+ assert_manual_reminder_state(cwc, false);
+
+ let box = get_notification(cwc.window, kBoxId, kNotificationId);
+ // Click ok to be notified on send if no attachments are attached.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.lastElementChild,
+ {},
+ cwc.window
+ );
+ await wait_for_reminder_state(cwc, false);
+
+ // The manual reminder should be enabled now.
+ assert_manual_reminder_state(cwc, true);
+
+ // Now try to send, make sure we get the alert.
+ // Click the "Oh, I Did!" button in the attachment reminder dialog.
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ let buttonSend = cwc.window.document.getElementById("button-send");
+ EventUtils.synthesizeMouseAtCenter(buttonSend, {}, buttonSend.ownerGlobal);
+ await dialogPromise;
+ await new Promise(resolve => setTimeout(resolve));
+
+ // After confirming the reminder the menuitem should get disabled.
+ assert_manual_reminder_state(cwc, false);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that the alert appears normally, but not after closing the
+ * notification.
+ */
+add_task(async function test_attachment_reminder_dismissal() {
+ let cwc = open_compose_new_mail();
+
+ // There should be no notification yet.
+ assert_automatic_reminder_state(cwc, false);
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "popping up, eh?",
+ "Hi there, remember the attachment! " +
+ "Yes, there is a file test.doc attached! " +
+ "Do check it, test.doc is a nice attachment."
+ );
+
+ // Give the notification time to appear.
+ await wait_for_reminder_state(cwc, true);
+
+ Assert.equal(get_reminder_keywords(cwc), "test.doc, attachment, attached");
+
+ // We didn't click the "Remind Me Later" - the alert should pop up
+ // on send anyway.
+ // Click the "Oh, I Did!" button in the attachment reminder dialog.
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ let buttonSend = cwc.window.document.getElementById("button-send");
+ EventUtils.synthesizeMouseAtCenter(buttonSend, {}, buttonSend.ownerGlobal);
+ await dialogPromise;
+ await new Promise(resolve => setTimeout(resolve));
+
+ let notification = assert_automatic_reminder_state(cwc, true);
+
+ notification.close();
+ assert_automatic_reminder_state(cwc, false);
+
+ click_send_and_handle_send_error(cwc);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 938829
+ * Check that adding an attachment actually hides the notification.
+ */
+add_task(async function test_attachment_reminder_with_attachment() {
+ let cwc = open_compose_new_mail();
+
+ // There should be no notification yet.
+ assert_automatic_reminder_state(cwc, false);
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "Testing automatic reminder!",
+ "Hello! We will have a real attachment here."
+ );
+
+ // Give the notification time to appear. It should.
+ await wait_for_reminder_state(cwc, true);
+
+ // Add an attachment.
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("prefs.js");
+ Assert.ok(
+ file.exists(),
+ "The required file prefs.js was not found in the profile."
+ );
+ let attachments = [cwc.window.FileToAttachment(file)];
+ cwc.window.AddAttachments(attachments);
+
+ // The notification should hide.
+ await wait_for_reminder_state(cwc, false);
+
+ // Add some more text with keyword so the automatic notification
+ // could potentially show up.
+ setup_msg_contents(cwc, "", "", " Yes, there is a file attached!");
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ cwc.window.RemoveAllAttachments();
+
+ // After removing the attachment, notification should come back
+ // with all the keywords, even those input while having an attachment.
+ await wait_for_reminder_state(cwc, true);
+ Assert.equal(get_reminder_keywords(cwc), "attachment, attached");
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that the mail.compose.attachment_reminder_aggressive pref works.
+ */
+add_task(async function test_attachment_reminder_aggressive_pref() {
+ const kPref = "mail.compose.attachment_reminder_aggressive";
+ Services.prefs.setBoolPref(kPref, false);
+
+ let cwc = open_compose_new_mail();
+
+ // There should be no notification yet.
+ assert_automatic_reminder_state(cwc, false);
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "aggressive?",
+ "Check this attachment!"
+ );
+
+ await wait_for_reminder_state(cwc, true);
+ click_send_and_handle_send_error(cwc);
+
+ close_compose_window(cwc);
+
+ // Now reset the pref back to original value.
+ if (Services.prefs.prefHasUserValue(kPref)) {
+ Services.prefs.clearUserPref(kPref);
+ }
+});
+
+/**
+ * Test that clicking "No, Send Now" in the attachment reminder alert
+ * works.
+ */
+add_task(async function test_no_send_now_sends() {
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "will the 'No, Send Now' button work?",
+ "Hello, I got your attachment!"
+ );
+
+ await wait_for_reminder_state(cwc, true);
+
+ // Click the send button again, this time choose "No, Send Now".
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ let buttonSend = cwc.window.document.getElementById("button-send");
+ EventUtils.synthesizeMouseAtCenter(buttonSend, {}, buttonSend.ownerGlobal);
+ await dialogPromise;
+ await new Promise(resolve => setTimeout(resolve));
+
+ // After clicking "Send Now" sending is proceeding, just handle the error.
+ click_send_and_handle_send_error(cwc, true);
+
+ // We're now back in the compose window, let's close it then.
+ close_compose_window(cwc);
+});
+
+/**
+ * Click the manual reminder in the menu.
+ *
+ * @param aCwc A compose window controller.
+ * @param aExpectedState A boolean specifying what is the expected state
+ * of the reminder menuitem after the click.
+ */
+async function click_manual_reminder(aCwc, aExpectedState) {
+ wait_for_window_focused(aCwc.window);
+ let button = aCwc.window.document.getElementById("button-attach");
+
+ let popup = aCwc.window.document.getElementById("button-attachPopup");
+ let shownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ button.querySelector(".toolbarbutton-menubutton-dropmarker"),
+ {},
+ aCwc.window
+ );
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.activateItem(
+ aCwc.window.document.getElementById("button-attachPopup_remindLaterItem")
+ );
+ await hiddenPromise;
+ wait_for_window_focused(aCwc.window);
+ assert_manual_reminder_state(aCwc, aExpectedState);
+}
+
+/**
+ * Bug 521128
+ * Test proper behaviour of the manual reminder.
+ */
+add_task(async function test_manual_attachment_reminder() {
+ // Open a sample message with no attachment keywords.
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing manual reminder!",
+ "Some body..."
+ );
+
+ // Enable the manual reminder.
+ await click_manual_reminder(cwc, true);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Now close the message with saving it as draft.
+ plan_for_modal_dialog("commonDialogWindow", click_save_message);
+ cwc.window.goDoCommand("cmd_close");
+ wait_for_modal_dialog("commonDialogWindow");
+
+ // Open another blank compose window.
+ cwc = open_compose_new_mail();
+ // This one should have the reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ close_compose_window(cwc);
+
+ // The draft message was saved into Local Folders/Drafts.
+ await be_in_folder(gDrafts);
+
+ select_click_row(0);
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // Edit the draft again...
+ plan_for_new_window("msgcompose");
+ let box = get_notification(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // ... by clicking Edit in the draft message notification bar.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ cwc = wait_for_compose_window();
+
+ // Check the reminder enablement was preserved in the message.
+ assert_manual_reminder_state(cwc, true);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Now try to send, make sure we get the alert.
+ // Click the "Oh, I Did!" button in the attachment reminder dialog.
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ let buttonSend = cwc.window.document.getElementById("button-send");
+ EventUtils.synthesizeMouseAtCenter(buttonSend, {}, buttonSend.ownerGlobal);
+ await dialogPromise;
+ await new Promise(resolve => setTimeout(resolve));
+
+ // We were alerted once and the manual reminder is automatically turned off.
+ assert_manual_reminder_state(cwc, false);
+
+ // Enable the manual reminder and disable it again to see if it toggles right.
+ await click_manual_reminder(cwc, true);
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ await click_manual_reminder(cwc, false);
+
+ // Now try to send again, there should be no more alert.
+ click_send_and_handle_send_error(cwc);
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+}).__skipMe = AppConstants.platform == "linux"; // See bug 1535292.
+
+/**
+ * Bug 938759
+ * Test hiding of the automatic notification if the manual reminder is set.
+ */
+add_task(
+ async function test_manual_automatic_attachment_reminder_interaction() {
+ // Open a blank message compose
+ let cwc = open_compose_new_mail();
+ // This one should have the reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add some attachment keywords.
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing manual reminder!",
+ "Expect an attachment here..."
+ );
+
+ // The automatic attachment notification should pop up.
+ await wait_for_reminder_state(cwc, true);
+
+ // Now enable the manual reminder.
+ await click_manual_reminder(cwc, true);
+ // The attachment notification should disappear.
+ await wait_for_reminder_state(cwc, false);
+
+ // Add some more text so the automatic notification
+ // could potentially show up.
+ setup_msg_contents(cwc, "", "", " and look for your attachment!");
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ // Now disable the manual reminder.
+ await click_manual_reminder(cwc, false);
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ // Add some more text without keywords.
+ setup_msg_contents(cwc, "", "", " No keywords here.");
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ // Add some more text with a new keyword.
+ setup_msg_contents(cwc, "", "", " Do you find it attached?");
+ // Give the notification time to appear. It should now.
+ await wait_for_reminder_state(cwc, true);
+ Assert.equal(get_reminder_keywords(cwc), "attachment, attached");
+
+ close_compose_window(cwc);
+ }
+);
+
+/**
+ * Assert if there is any notification in the compose window.
+ *
+ * @param aCwc Compose Window Controller
+ * @param aValue True if notification should exist.
+ * False otherwise.
+ */
+function assert_any_notification(aCwc, aValue) {
+ let notification =
+ aCwc.window.document.getElementById(kBoxId).currentNotification;
+ if ((notification == null) == aValue) {
+ throw new Error("Notification in wrong state");
+ }
+}
+
+/**
+ * Bug 989653
+ * Send filelink attachment should not trigger the attachment reminder.
+ */
+add_task(function test_attachment_vs_filelink_reminder() {
+ // Open a blank message compose
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing Filelink notification",
+ "There is no body. I hope you don't mind!"
+ );
+
+ // There should be no notification yet.
+ assert_any_notification(cwc, false);
+
+ // Bring up the FileLink notification.
+ let kOfferThreshold = "mail.compose.big_attachments.threshold_kb";
+ let maxSize = Services.prefs.getIntPref(kOfferThreshold, 0) * 1024;
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("prefs.js");
+ add_attachments(cwc, Services.io.newFileURI(file).spec, maxSize);
+
+ // The filelink attachment proposal should be up but not the attachment
+ // reminder and it should also not interfere with the sending of the message.
+ wait_for_notification_to_show(cwc.window, kBoxId, "bigAttachment");
+ assert_automatic_reminder_state(cwc, false);
+
+ click_send_and_handle_send_error(cwc);
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 944643
+ * Test the attachment reminder coming up when keyword is in subject line.
+ */
+add_task(async function test_attachment_reminder_in_subject() {
+ // Open a blank message compose
+ let cwc = open_compose_new_mail();
+ // This one should have the reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add some attachment keyword in subject.
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing attachment reminder!",
+ "There is no keyword in this body..."
+ );
+
+ // The automatic attachment notification should pop up.
+ await wait_for_reminder_state(cwc, true);
+ Assert.equal(get_reminder_keywords(cwc), "attachment");
+
+ // Now clear the subject
+ delete_all_existing(cwc, cwc.window.document.getElementById("msgSubject"));
+
+ // Give the notification time to disappear.
+ await wait_for_reminder_state(cwc, false);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 944643
+ * Test the attachment reminder coming up when keyword is in subject line
+ * and also body.
+ */
+add_task(async function test_attachment_reminder_in_subject_and_body() {
+ // Open a blank message compose
+ let cwc = open_compose_new_mail();
+ // This one should have the reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add some attachment keyword in subject.
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing attachment reminder!",
+ "There should be an attached file in this body..."
+ );
+
+ // The automatic attachment notification should pop up.
+ await wait_for_reminder_state(cwc, true);
+ Assert.equal(get_reminder_keywords(cwc), "attachment, attached");
+
+ // Now clear only the subject
+ delete_all_existing(cwc, cwc.window.document.getElementById("msgSubject"));
+
+ // Give the notification some time. It should not disappear,
+ // just reduce the keywords list.
+ await wait_for_reminder_state(cwc, true, true);
+ Assert.equal(get_reminder_keywords(cwc), "attached");
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 1099866
+ * Test proper behaviour of attachment reminder when keyword reminding
+ * is turned off.
+ */
+add_task(async function test_disabled_attachment_reminder() {
+ Services.prefs.setBoolPref(kReminderPref, false);
+
+ // Open a sample message with no attachment keywords.
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing disabled keyword reminder!",
+ "Some body..."
+ );
+
+ // This one should have the manual reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add some keyword so the automatic notification
+ // could potentially show up.
+ setup_msg_contents(cwc, "", "", " and look for your attachment!");
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ // Enable the manual reminder.
+ await click_manual_reminder(cwc, true);
+ assert_automatic_reminder_state(cwc, false);
+
+ // Disable the manual reminder and the notification should still be hidden
+ // even when there are still keywords in the body.
+ await click_manual_reminder(cwc, false);
+ assert_automatic_reminder_state(cwc, false);
+
+ // There should be no attachment message upon send.
+ click_send_and_handle_send_error(cwc);
+
+ close_compose_window(cwc);
+
+ Services.prefs.setBoolPref(kReminderPref, true);
+});
+
+/**
+ * Bug 833909
+ * Test reminder comes up when a draft with keywords is opened.
+ */
+add_task(async function test_reminder_in_draft() {
+ // Open a sample message with no attachment keywords.
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing draft reminder!",
+ "Some body..."
+ );
+
+ // This one should have the manual reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be no attachment notification.
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add some keyword so the automatic notification
+ // could potentially show up.
+ setup_msg_contents(cwc, "", "", " and look for your attachment!");
+
+ // Give the notification time to appear.
+ await wait_for_reminder_state(cwc, true);
+
+ // Now close the message with saving it as draft.
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ // The draft message was saved into Local Folders/Drafts.
+ await be_in_folder(gDrafts);
+
+ select_click_row(0);
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // Edit the draft again...
+ plan_for_new_window("msgcompose");
+ let box = get_notification(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // ... by clicking Edit in the draft message notification bar.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ cwc = wait_for_compose_window();
+
+ // Give the notification time to appear.
+ await wait_for_reminder_state(cwc, true);
+
+ close_compose_window(cwc);
+
+ // Delete the leftover draft message.
+ press_delete();
+});
+
+/**
+ * Bug 942436
+ * Test that the reminder can be turned off for the current message.
+ */
+add_task(async function test_disabling_attachment_reminder() {
+ // Open a sample message with attachment keywords.
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing turning off the reminder",
+ "Some attachment keywords here..."
+ );
+
+ // This one should have the manual reminder disabled.
+ assert_manual_reminder_state(cwc, false);
+ // There should be an attachment reminder.
+ await wait_for_reminder_state(cwc, true);
+
+ // Disable the reminder (not just dismiss) using the menuitem
+ // in the notification bar menu-button.
+ let disableButton = get_notification_button(
+ cwc.window,
+ kBoxId,
+ kNotificationId,
+ {
+ popup: "reminderBarPopup",
+ }
+ );
+ let dropmarker = disableButton.querySelector("dropmarker");
+ EventUtils.synthesizeMouseAtCenter(dropmarker, {}, dropmarker.ownerGlobal);
+ await click_menus_in_sequence(
+ disableButton.closest("toolbarbutton").querySelector("menupopup"),
+ [{ id: "disableReminder" }]
+ );
+
+ await wait_for_reminder_state(cwc, false);
+
+ // Add more keywords.
+ setup_msg_contents(cwc, "", "", "... and another file attached.");
+ // Give the notification time to appear. It shouldn't.
+ await wait_for_reminder_state(cwc, false);
+
+ // Enable the manual reminder.
+ // This overrides the previous explicit disabling of any reminder.
+ await click_manual_reminder(cwc, true);
+ assert_automatic_reminder_state(cwc, false);
+
+ // Disable the manual reminder and the notification should still be hidden
+ // even when there are still keywords in the body.
+ await click_manual_reminder(cwc, false);
+ assert_automatic_reminder_state(cwc, false);
+
+ // Add more keywords to trigger automatic reminder.
+ setup_msg_contents(cwc, "", "", "I enclosed another file.");
+ // Give the notification time to appear. It should now.
+ await wait_for_reminder_state(cwc, true);
+
+ // Disable the reminder again.
+ disableButton = get_notification_button(cwc.window, kBoxId, kNotificationId, {
+ popup: "reminderBarPopup",
+ });
+ dropmarker = disableButton.querySelector("dropmarker");
+ EventUtils.synthesizeMouseAtCenter(dropmarker, {}, dropmarker.ownerGlobal);
+ await click_menus_in_sequence(
+ disableButton.closest("toolbarbutton").querySelector("menupopup"),
+ [{ id: "disableReminder" }]
+ );
+ await wait_for_reminder_state(cwc, false);
+
+ // Now send the message.
+ plan_for_window_close(cwc);
+ cwc.window.goDoCommand("cmd_sendLater");
+ wait_for_window_close();
+
+ // There should be no alert so it is saved in Outbox.
+ await be_in_folder(gOutbox);
+
+ select_click_row(0);
+ // Delete the leftover outgoing message.
+ press_delete();
+
+ // Get back to the mail account for other tests.
+ let mail = MailServices.accounts.defaultAccount.incomingServer.rootFolder;
+ await be_in_folder(mail);
+});
+
+/**
+ * Click the send button and handle the send error dialog popping up.
+ * It will return us back to the compose window.
+ *
+ * @param aController
+ * @param aAlreadySending Set this to true if sending was already triggered
+ * by other means.
+ */
+function click_send_and_handle_send_error(aController, aAlreadySending) {
+ plan_for_modal_dialog("commonDialogWindow", click_ok_on_send_error);
+ if (!aAlreadySending) {
+ let buttonSend = aController.window.document.getElementById("button-send");
+ EventUtils.synthesizeMouseAtCenter(buttonSend, {}, buttonSend.ownerGlobal);
+ }
+ wait_for_modal_dialog("commonDialogWindow");
+}
+
+/**
+ * Click Ok in the Send Message Error dialog.
+ */
+function click_ok_on_send_error(controller) {
+ if (controller.window.document.title != "Send Message Error") {
+ throw new Error(
+ "Not a send error dialog; title=" + controller.window.document.title
+ );
+ }
+ controller.window.document
+ .querySelector("dialog")
+ .getButton("accept")
+ .doCommand();
+}
+
+/**
+ * Click Save in the Save message dialog.
+ */
+function click_save_message(controller) {
+ if (controller.window.document.title != "Save Message") {
+ throw new Error(
+ "Not a Save message dialog; title=" + controller.window.document.title
+ );
+ }
+ controller.window.document
+ .querySelector("dialog")
+ .getButton("accept")
+ .doCommand();
+}
diff --git a/comm/mail/test/browser/composition/browser_base64Display.js b/comm/mail/test/browser/composition/browser_base64Display.js
new file mode 100644
index 0000000000..8b2dc1edaf
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_base64Display.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+/**
+ * Tests that messages with "broken" base64 are correctly displayed.
+ */
+
+"use strict";
+
+var { get_about_message, open_message_from_file } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+add_task(async function test_base64_display() {
+ let file = new FileUtils.File(
+ getTestFilePath("data/base64-with-whitespace.eml")
+ );
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+ let bodyText = aboutMessage.document
+ .getElementById("messagepane")
+ .contentDocument.querySelector("body").textContent;
+ close_window(msgc);
+
+ Assert.ok(
+ bodyText.includes("abcdefghijklmnopqrstuvwxyz"),
+ "Decode base64 body from message."
+ );
+});
+
+add_task(async function test_base64_display2() {
+ let file = new FileUtils.File(getTestFilePath("data/base64-bug1586890.eml"));
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+ let bodyText = aboutMessage.document
+ .getElementById("messagepane")
+ .contentDocument.querySelector("body").textContent;
+ close_window(msgc);
+
+ Assert.ok(
+ bodyText.includes("abcdefghijklm"),
+ "Decode base64 body from UTF-16 message with broken charset."
+ );
+});
diff --git a/comm/mail/test/browser/composition/browser_blockedContent.js b/comm/mail/test/browser/composition/browser_blockedContent.js
new file mode 100644
index 0000000000..997c760af0
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_blockedContent.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+/**
+ * Tests that we do the right thing wrt. blocked resources during composition.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var { get_msg_source, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder, get_special_folder, press_delete, select_click_row } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+ );
+var { wait_for_notification_to_show } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var { plan_for_window_close, wait_for_window_close } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gOutboxFolder;
+
+var kBoxId = "compose-notification-bottom";
+var kNotificationId = "blockedContent";
+
+add_setup(async function () {
+ gOutboxFolder = await get_special_folder(Ci.nsMsgFolderFlags.Queue);
+});
+
+function putHTMLOnClipboard(html) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+
+ // Register supported data flavors
+ trans.init(null);
+ trans.addDataFlavor("text/html");
+
+ let wapper = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wapper.data = html;
+ trans.setTransferData("text/html", wapper);
+
+ Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+}
+
+/**
+ * Test that accessing file: URLs will block when appropriate, and load
+ * the content when appropriate.
+ */
+add_task(async function test_paste_file_urls() {
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "someone@example.com",
+ "testing html paste",
+ "See these images- one broken one not\n"
+ );
+
+ const fname = "data/tb-logo.png";
+ let file = new FileUtils.File(getTestFilePath(fname));
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+
+ let dest = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ file.leafName
+ );
+ let tmpFile;
+ let tmpFileURL;
+ IOUtils.remove(dest, { ignoreAbsent: true })
+ .then(function () {
+ return IOUtils.copy(file.path, dest);
+ })
+ .then(function () {
+ return IOUtils.setModificationTime(dest);
+ })
+ .then(function () {
+ tmpFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ tmpFile.initWithPath(dest);
+ Assert.ok(tmpFile.exists(), "tmpFile's not there at " + dest);
+
+ tmpFileURL = fileHandler.getURLSpecFromActualFile(tmpFile);
+ putHTMLOnClipboard(
+ "<img id='bad-img' src='file://foo/non-existent' alt='bad' /> and " +
+ "<img id='tmp-img' src='" +
+ tmpFileURL +
+ "' alt='tmp' />"
+ );
+
+ cwc.window.document.getElementById("messageEditor").focus();
+ // Ctrl+V = Paste
+ EventUtils.synthesizeKey(
+ "v",
+ { shiftKey: false, accelKey: true },
+ cwc.window
+ );
+ })
+ .catch(function (err) {
+ throw new Error("Setting up img file FAILED: " + err);
+ });
+
+ // Now wait for the paste, and for the file: based image to get converted
+ // to data:.
+ utils.waitFor(function () {
+ let img = cwc.window.document
+ .getElementById("messageEditor")
+ .contentDocument.getElementById("tmp-img");
+ return img && img.naturalHeight == 84 && img.src.startsWith("data:");
+ }, "Timeout waiting for pasted tmp image to be loaded ok");
+
+ // For the non-existent (non-accessible!) image we should get a notification.
+ wait_for_notification_to_show(cwc.window, kBoxId, kNotificationId);
+
+ plan_for_window_close(cwc);
+ cwc.window.goDoCommand("cmd_sendLater");
+ wait_for_window_close();
+
+ await be_in_folder(gOutboxFolder);
+ let outMsg = select_click_row(0);
+ let outMsgContent = await get_msg_source(outMsg);
+
+ Assert.ok(
+ outMsgContent.includes("file://foo/non-existent"),
+ "non-existent file not in content=" + outMsgContent
+ );
+
+ Assert.ok(
+ !outMsgContent.includes(tmpFileURL),
+ "tmp file url still in content=" + outMsgContent
+ );
+
+ Assert.ok(
+ outMsgContent.includes('id="tmp-img" src="cid:'),
+ "tmp-img should be cid after send; content=" + outMsgContent
+ );
+
+ press_delete(); // Delete the msg from Outbox.
+});
diff --git a/comm/mail/test/browser/composition/browser_charsetEdit.js b/comm/mail/test/browser/composition/browser_charsetEdit.js
new file mode 100644
index 0000000000..61b4a756b8
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_charsetEdit.js
@@ -0,0 +1,231 @@
+/* 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/. */
+
+/**
+ * Tests that we do the right thing wrt. message encoding when editing or
+ * replying to messages.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+
+var {
+ close_compose_window,
+ open_compose_with_reply,
+ save_compose_message,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ get_special_folder,
+ get_about_message,
+ make_display_unthreaded,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { SyntheticPartLeaf } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { wait_for_notification_to_show, get_notification } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var { plan_for_new_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+let aboutMessage = get_about_message();
+
+var gDrafts;
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Helper to get the full message content.
+ *
+ * @param aMsgHdr: nsIMsgDBHdr object whose text body will be read
+ * @param aGetText: if true, return header objects. if false, return body data.
+ * @returns Map(partnum -> message headers)
+ */
+function getMsgHeaders(aMsgHdr, aGetText = false) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ let handler = {
+ _done: false,
+ _data: new Map(),
+ _text: new Map(),
+ endMessage() {
+ this._done = true;
+ },
+ deliverPartData(num, text) {
+ this._text.set(num, this._text.get(num) + text);
+ },
+ startPart(num, headers) {
+ this._data.set(num, headers);
+ this._text.set(num, "");
+ },
+ };
+ let streamListener = MimeParser.makeStreamListenerParser(handler, {
+ strformat: "unicode",
+ });
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ utils.waitFor(() => handler._done);
+ return aGetText ? handler._text : handler._data;
+}
+
+/**
+ * Test that if we reply to a message in an invalid charset, we don't try to compose
+ * in that charset. Instead, we should be using UTF-8.
+ */
+add_task(async function test_wrong_reply_charset() {
+ let folder = gDrafts;
+ let msg0 = create_message({
+ bodyPart: new SyntheticPartLeaf("Some text", {
+ charset: "invalid-charset",
+ }),
+ });
+ await add_message_to_folder([folder], msg0);
+ await be_in_folder(folder);
+ // Make the folder unthreaded for easier message selection.
+ make_display_unthreaded();
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+ Assert.equal(getMsgHeaders(msg).get("").charset, "invalid-charset");
+
+ let rwc = open_compose_with_reply();
+ await save_compose_message(rwc.window);
+ await TestUtils.waitForCondition(
+ () => folder.getTotalMessages(false) == 2,
+ "message saved to drafts folder"
+ );
+ close_compose_window(rwc);
+
+ let draftMsg = select_click_row(1);
+ Assert.equal(getMsgHeaders(draftMsg).get("").charset, "UTF-8");
+ press_delete(mc); // Delete message
+
+ // Edit the original message. Charset should be UTF-8 now.
+ msg = select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+
+ plan_for_new_window("msgcompose");
+
+ let box = get_notification(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // Click on the "Edit" button in the draft notification.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ rwc = wait_for_compose_window();
+ await save_compose_message(rwc.window);
+ close_compose_window(rwc);
+ msg = select_click_row(0);
+ await TestUtils.waitForCondition(
+ () => getMsgHeaders(msg).get("").charset == "UTF-8",
+ "The charset matches"
+ );
+ press_delete(mc); // Delete message
+});
+
+/**
+ * Test that replying to bad charsets don't screw up the existing text.
+ */
+add_task(async function test_no_mojibake() {
+ let folder = gDrafts;
+ let nonASCII = "繧ア繝繧。繝ォ繧ウ繧「繝医Ν";
+ let UTF7 = "+MLEwxDChMOswszCiMMgw6w-";
+ let msg0 = create_message({
+ bodyPart: new SyntheticPartLeaf(UTF7, { charset: "utf-7" }),
+ });
+ await add_message_to_folder([folder], msg0);
+ await be_in_folder(folder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+ await TestUtils.waitForCondition(
+ () => getMsgHeaders(msg).get("").charset == "utf-7",
+ "message charset correctly set"
+ );
+ Assert.equal(getMsgHeaders(msg, true).get("").trim(), nonASCII);
+
+ let rwc = open_compose_with_reply();
+ await save_compose_message(rwc.window);
+ await TestUtils.waitForCondition(
+ () => folder.getTotalMessages(false) == 2,
+ "message saved to drafts folder"
+ );
+ close_compose_window(rwc);
+
+ let draftMsg = select_click_row(1);
+ Assert.equal(getMsgHeaders(draftMsg).get("").charset.toUpperCase(), "UTF-8");
+ let text = getMsgHeaders(draftMsg, true).get("");
+ // Delete message first before throwing so subsequent tests are not affected.
+ press_delete(mc);
+ if (!text.includes(nonASCII)) {
+ throw new Error("Expected to find " + nonASCII + " in " + text);
+ }
+
+ // Edit the original message. Charset should be UTF-8 now.
+ msg = select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+
+ plan_for_new_window("msgcompose");
+ let box = get_notification(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ // Click on the "Edit" button in the draft notification.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ rwc = wait_for_compose_window();
+ await save_compose_message(rwc.window);
+ close_compose_window(rwc);
+ msg = select_click_row(0);
+ Assert.equal(getMsgHeaders(msg).get("").charset.toUpperCase(), "UTF-8");
+ Assert.equal(getMsgHeaders(msg, true).get("").trim(), nonASCII);
+ press_delete(mc); // Delete message
+});
diff --git a/comm/mail/test/browser/composition/browser_checkRecipientKeys.js b/comm/mail/test/browser/composition/browser_checkRecipientKeys.js
new file mode 100644
index 0000000000..64754c0ac0
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_checkRecipientKeys.js
@@ -0,0 +1,87 @@
+/* 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 { plan_for_new_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+var {
+ close_compose_window,
+ open_compose_new_mail,
+ setup_msg_contents,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+add_setup(() => {
+ Services.prefs.setBoolPref("mail.smime.remind_encryption_possible", true);
+ Services.prefs.setBoolPref("mail.openpgp.remind_encryption_possible", true);
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("mail.smime.remind_encryption_possible");
+ Services.prefs.clearUserPref("mail.openpgp.remind_encryption_possible");
+});
+
+/**
+ * Test that checkEncryptionState should not affect gMsgCompose.compFields.
+ */
+add_task(async function test_checkEncryptionState() {
+ let [id] = await OpenPGPTestUtils.importPrivateKey(
+ window,
+ new FileUtils.File(
+ getTestFilePath(
+ "../openpgp/data/keys/bob@openpgp.example-0xfbfcc82a015e7330-secret.asc"
+ )
+ )
+ );
+
+ // Set up the identity to cover the remindOpenPGP/remindSMime branches in
+ // checkEncryptionState.
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = "test@local";
+ identity.setUnicharAttribute("encryption_cert_name", "smime-cert");
+ identity.setUnicharAttribute("openpgp_key_id", id.split("0x").join(""));
+ let account = MailServices.accounts.createAccount();
+ account.addIdentity(identity);
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "test",
+ "openpgp.example",
+ "imap"
+ );
+ await be_in_folder(account.incomingServer.rootFolder);
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ // Set up the compose fields used to init the compose window.
+ let fields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ fields.to = "to@local";
+ fields.cc = "cc1@local,cc2@local";
+ fields.bcc = "bcc1@local,bcc2@local";
+ let params = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ params.identity = identity;
+ params.composeFields = fields;
+
+ // Open a compose window.
+ plan_for_new_window("msgcompose");
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ let cwc = wait_for_compose_window();
+
+ // Test gMsgCompose.compFields is intact.
+ let compFields = cwc.window.gMsgCompose.compFields;
+ Assert.equal(compFields.to, "to@local");
+ Assert.equal(compFields.cc, "cc1@local, cc2@local");
+ Assert.equal(compFields.bcc, "bcc1@local, bcc2@local");
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_cp932Display.js b/comm/mail/test/browser/composition/browser_cp932Display.js
new file mode 100644
index 0000000000..4c3aef3f44
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_cp932Display.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+/**
+ * Tests that messages in cp932, Thunderbirds alias for Shift_JIS, are correctly displayed.
+ */
+
+"use strict";
+
+var { get_about_message, open_message_from_file } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+add_task(async function test_cp932_display() {
+ let file = new FileUtils.File(getTestFilePath("data/charset-cp932.eml"));
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+ let subjectText =
+ aboutMessage.document.getElementById("expandedsubjectBox").textContent;
+ let bodyText = aboutMessage.document
+ .getElementById("messagepane")
+ .contentDocument.querySelector("body").textContent;
+ Assert.ok(
+ subjectText.includes("縺薙%縺ォ譛ャ譁縺後″縺セ縺吶"),
+ "Decoded cp932 text not found in message subject. subjectText=" +
+ subjectText
+ );
+ Assert.ok(
+ bodyText.includes("縺薙%縺ォ譛ャ譁縺後″縺セ縺吶"),
+ "Decoded cp932 text not found in message body."
+ );
+ close_window(msgc);
+});
diff --git a/comm/mail/test/browser/composition/browser_customHeaders.js b/comm/mail/test/browser/composition/browser_customHeaders.js
new file mode 100644
index 0000000000..308c800c04
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_customHeaders.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+/**
+ * Test mail.compose.other.header is rendered and handled correctly.
+ */
+var {
+ close_compose_window,
+ get_msg_source,
+ open_compose_new_mail,
+ save_compose_message,
+ open_compose_from_draft,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder, select_click_row, get_special_folder } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { wait_for_window_focused } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+/**
+ * Test custom headers are set and encoded correctly.
+ */
+add_task(async function test_customHeaders() {
+ let draftsFolder = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+
+ // Set other.header so that they will be rendered in compose window.
+ let otherHeaders = Services.prefs.getCharPref("mail.compose.other.header");
+ Services.prefs.setCharPref(
+ "mail.compose.other.header",
+ "X-Header1, X-Header2, Approved ,Supersedes"
+ );
+
+ // Set values to custom headers.
+ let cwc = open_compose_new_mail();
+ let inputs = cwc.window.document.querySelectorAll(".address-row-raw input");
+ inputs[0].value = "Test テ、テカテシ";
+ inputs[1].value = "Test 沽";
+ inputs[2].value = "moderator@tinderbox.com";
+ inputs[3].value = "<message-id-1234@tinderbox.com>";
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ await be_in_folder(draftsFolder);
+ let draftMsg = select_click_row(0);
+ let draftMsgLines = (await get_msg_source(draftMsg)).split("\n");
+
+ // Check header values are set and encoded correctly.
+ Assert.ok(
+ draftMsgLines.some(
+ line => line.trim() == "X-Header1: =?UTF-8?B?VGVzdCDDpMO2w7w=?="
+ ),
+ "Correct X-Header1 found"
+ );
+ Assert.ok(
+ draftMsgLines.some(
+ line => line.trim() == "X-Header2: =?UTF-8?B?VGVzdCDwn5iD?="
+ ),
+ "Correct X-Header2 found"
+ );
+ Assert.ok(
+ draftMsgLines.some(
+ line => line.trim() == "Approved: moderator@tinderbox.com"
+ ),
+ "Correct Approved found"
+ );
+ Assert.ok(
+ draftMsgLines.some(
+ line => line.trim() == "Supersedes: <message-id-1234@tinderbox.com>"
+ ),
+ "Correct Supersedes found"
+ );
+
+ cwc = open_compose_from_draft();
+ let inputs2 = cwc.window.document.querySelectorAll(".address-row-raw input");
+
+ Assert.equal(inputs2[0].value, "Test テ、テカテシ");
+ Assert.equal(inputs2[1].value, "Test 沽");
+ Assert.equal(inputs2[2].value, "moderator@tinderbox.com");
+ Assert.equal(inputs2[3].value, "<message-id-1234@tinderbox.com>");
+
+ close_compose_window(cwc);
+
+ // Reset other.header.
+ Services.prefs.setCharPref("mail.compose.other.header", otherHeaders);
+});
diff --git a/comm/mail/test/browser/composition/browser_draftIdentity.js b/comm/mail/test/browser/composition/browser_draftIdentity.js
new file mode 100644
index 0000000000..554b3f594e
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_draftIdentity.js
@@ -0,0 +1,313 @@
+/* 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/. */
+
+/**
+ * Tests that compose new message chooses the correct initial identity when
+ * called from the context of an open composer.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_from_draft } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { assert_notification_displayed, wait_for_notification_to_show } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+ );
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+let aboutMessage = get_about_message();
+
+var gDrafts;
+var gAccount;
+
+// The first identity should become the default in the account.
+var gIdentities = [
+ { email: "x@example.invalid" },
+ { email: "y@example.invalid", fullname: "User Y" },
+ { email: "y@example.invalid", fullname: "User YY", label: "YY" },
+ { email: "y+y@example.invalid", fullname: "User Y" },
+ { email: "z@example.invalid", fullname: "User Z", label: "Label Z" },
+ { email: "a+b@example.invalid", fullname: "User A" },
+];
+
+add_setup(async function () {
+ // Now set up an account with some identities.
+ gAccount = MailServices.accounts.createAccount();
+ gAccount.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "Draft Identity Testing",
+ "pop3"
+ );
+
+ for (let id of gIdentities) {
+ let identity = MailServices.accounts.createIdentity();
+ if ("email" in id) {
+ identity.email = id.email;
+ }
+ if ("fullname" in id) {
+ identity.fullName = id.fullname;
+ }
+ if ("label" in id) {
+ identity.label = id.label;
+ }
+ gAccount.addIdentity(identity);
+ id.key = identity.key;
+ id.name = identity.identityName;
+ }
+
+ MailServices.accounts.defaultAccount = gAccount;
+
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Create a new templated draft message in the drafts folder.
+ *
+ * @returns {integer} The index (position) of the created message in the drafts folder.
+ */
+function create_draft(aFrom, aIdKey) {
+ let msgCount = gDrafts.getTotalMessages(false);
+ let source =
+ "From - Wed Mar 01 01:02:03 2017\n" +
+ "X-Mozilla-Status: 0000\n" +
+ "X-Mozilla-Status2: 00000000\n" +
+ "X-Mozilla-Keys:\n" +
+ "FCC: mailbox://nobody@Local%20Folders/Sent\n" +
+ (aIdKey
+ ? // prettier-ignore
+ `X-Identity-Key: ${aIdKey}\n` +
+ `X-Account-Key: ${gAccount.key}\n`
+ : "") +
+ `From: ${aFrom}\n` +
+ "To: nobody@example.invalid\n" +
+ "Subject: test!\n" +
+ `Message-ID: <${msgCount}@example.invalid>\n` +
+ "Date: Wed, 1 Mar 2017 01:02:03 +0100\n" +
+ "X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;\n" +
+ " attachmentreminder=0; deliveryformat=4\n" +
+ "MIME-Version: 1.0\n" +
+ "Content-Type: text/plain; charset=utf-8\n" +
+ "Content-Transfer-Encoding: 8bit\n" +
+ "\n" +
+ "Testing draft identity.\n";
+
+ gDrafts.QueryInterface(Ci.nsIMsgLocalMailFolder).addMessage(source);
+ let msgCountNew = gDrafts.getTotalMessages(false);
+
+ Assert.equal(msgCountNew, msgCount + 1);
+ return msgCountNew - 1;
+}
+
+/**
+ * Helper to check that a suitable From identity was set up in the given
+ * composer window.
+ *
+ * @param cwc Compose window controller.
+ * @param aIdentityKey The key of the expected identity.
+ * @param aFrom The expected displayed From address.
+ */
+function checkCompIdentity(cwc, aIdentityKey, aFrom) {
+ Assert.equal(
+ cwc.window.getCurrentAccountKey(),
+ gAccount.key,
+ "The From account is not correctly selected"
+ );
+ Assert.equal(
+ cwc.window.getCurrentIdentityKey(),
+ aIdentityKey,
+ "The From identity is not correctly selected"
+ );
+ Assert.equal(
+ cwc.window.document.getElementById("msgIdentity").value,
+ aFrom,
+ "The From value was initialized to an unexpected value"
+ );
+}
+
+/**
+ * Bug 394216
+ * Test that starting a new message from a draft with various combinations
+ * of From and X-Identity-Key gets the expected initial identity selected.
+ */
+add_task(async function test_draft_identity_selection() {
+ let tests = [
+ // X-Identity-Key header exists:
+ // 1. From header matches X-Identity-Key identity exactly
+ {
+ idIndex: 1,
+ warning: false,
+ draftIdKey: gIdentities[1].key,
+ draftFrom: gIdentities[1].name,
+ },
+ // 2. From header similar to X-Identity-Key identity with +suffix appended
+ {
+ idIndex: 1,
+ warning: false,
+ draftIdKey: gIdentities[1].key,
+ draftFrom: gIdentities[1].name.replace("y@", "y+x@"),
+ },
+ // 3. X-Identity-Key identity similar to From header with +suffix appended
+ {
+ idIndex: 5,
+ warning: false,
+ draftIdKey: gIdentities[5].key,
+ draftFrom: gIdentities[5].name.replace("a+b@", "a@"),
+ },
+
+ // From header not similar to existing X-Identity-Key identity:
+ // 4. From header not even containing an email address
+ {
+ idIndex: 5,
+ warning: false,
+ draftIdKey: gIdentities[5].key,
+ draftFrom: "User",
+ from: "User <>",
+ },
+ // 5. no matching identity exists
+ {
+ idIndex: 1,
+ warning: true,
+ draftIdKey: gIdentities[1].key,
+ draftFrom: "New User <modified@sender.invalid>",
+ },
+ // 6. 1 matching identity exists
+ {
+ idIndex: 4,
+ warning: false,
+ draftIdKey: gIdentities[4].key,
+ draftFrom: "New User <" + gIdentities[4].email + ">",
+ },
+ // 7. 2 or more matching identities exist
+ {
+ idIndex: 1,
+ warning: true,
+ draftIdKey: gIdentities[0].key,
+ draftFrom: gIdentities[1].name.replace("User Y", "User YZ"),
+ },
+
+ // No X-Identity-Key header:
+ // 8. no matching identity exists
+ // This is a 'foreign draft' in which case we won't preserve the From value
+ // and set it from the default identity.
+ {
+ idIndex: 0,
+ warning: true,
+ draftFrom: "Unknown <unknown@nowhere.invalid>",
+ from: gIdentities[0].name,
+ },
+ // 9. From header matches default identity
+ { idIndex: 0, warning: false, draftFrom: gIdentities[0].name },
+ // 10. From header matches some other identity
+ { idIndex: 5, warning: false, draftFrom: gIdentities[5].name },
+ // 11. From header matches identity with suffix
+ { idIndex: 3, warning: false, draftFrom: gIdentities[3].name },
+ // 12. From header matches 2 identities
+ {
+ idIndex: 1,
+ warning: true,
+ draftFrom: gIdentities[1].email,
+ from: gIdentities[1].name,
+ },
+ ];
+
+ for (let test of tests) {
+ test.draftIndex = create_draft(test.draftFrom, test.draftIdKey);
+ }
+
+ for (let test of tests) {
+ dump("Running draft identity test" + tests.indexOf(test) + "\n");
+ await be_in_folder(gDrafts);
+ select_click_row(test.draftIndex);
+ assert_selected_and_displayed(test.draftIndex);
+ wait_for_notification_to_show(
+ aboutMessage,
+ "mail-notification-top",
+ "draftMsgContent"
+ );
+ let cwc = open_compose_from_draft();
+ checkCompIdentity(
+ cwc,
+ gIdentities[test.idIndex].key,
+ test.from ? test.from : test.draftFrom
+ );
+ if (test.warning) {
+ wait_for_notification_to_show(
+ cwc.window,
+ "compose-notification-bottom",
+ "identityWarning"
+ );
+ } else {
+ assert_notification_displayed(
+ cwc.window,
+ "compose-notification-bottom",
+ "identityWarning",
+ false
+ );
+ }
+
+ close_compose_window(cwc, false);
+ }
+ /*
+ // TODO: fix this in bug 1238264, the identity selector does not properly close.
+ // Open a draft again that shows the notification.
+ await be_in_folder(gDrafts);
+ select_click_row(tests[tests.length-1].draftIndex);
+ let cwc = open_compose_from_draft();
+ wait_for_notification_to_show(cwc, "compose-notification-bottom",
+ "identityWarning");
+ // Notification should go away when another identity is chosen.
+ EventUtils.synthesizeMouseAtCenter(cwc.e("msgIdentity"), { }, cwc.window.document.getElementById("msgIdentity").ownerGlobal)
+ await click_menus_in_sequence(cwc.window.document.getElementById("msgIdentityPopup"),
+ [ { identitykey: gIdentities[0].key } ]);
+
+ wait_for_notification_to_stop(cwc, "compose-notification-bottom",
+ "identityWarning");
+ close_compose_window(cwc, false);
+*/
+});
+
+registerCleanupFunction(async function () {
+ for (let id = 1; id < gIdentities.length; id++) {
+ gAccount.removeIdentity(
+ MailServices.accounts.getIdentity(gIdentities[id].key)
+ );
+ }
+
+ // The last identity of an account can't be removed so clear all its prefs
+ // which effectively destroys it.
+ MailServices.accounts.getIdentity(gIdentities[0].key).clearAllValues();
+ MailServices.accounts.removeAccount(gAccount);
+ gAccount = null;
+
+ // Clear our drafts.
+ await be_in_folder(gDrafts);
+ while (gDrafts.getTotalMessages(false) > 0) {
+ press_delete();
+ }
+
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+ // Focus an element in the main window, then blur it again to avoid it
+ // hijacking keypresses.
+ let mainWindowElement = document.getElementById("button-appmenu");
+ mainWindowElement.focus();
+ mainWindowElement.blur();
+});
diff --git a/comm/mail/test/browser/composition/browser_drafts.js b/comm/mail/test/browser/composition/browser_drafts.js
new file mode 100644
index 0000000000..a5d6bed0cc
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_drafts.js
@@ -0,0 +1,457 @@
+/* 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/. */
+
+/**
+ * Tests draft related functionality:
+ * - that we don't allow opening multiple copies of a draft.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ get_compose_body,
+ get_msg_source,
+ open_compose_new_mail,
+ save_compose_message,
+ setup_msg_contents,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ make_message_sets_in_folders,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { wait_for_notification_to_show, get_notification } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+
+var {
+ click_menus_in_sequence,
+ close_popup_sequence,
+ plan_for_new_window,
+ wait_for_window_focused,
+} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+let aboutMessage = get_about_message();
+
+var kBoxId = "mail-notification-top";
+var draftsFolder;
+
+add_setup(async function () {
+ requestLongerTimeout(2);
+ draftsFolder = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Bug 349547.
+ * Tests that we only open one compose window for one instance of a draft.
+ */
+add_task(async function test_open_draft_again() {
+ await make_message_sets_in_folders([draftsFolder], [{ count: 1 }]);
+ await be_in_folder(draftsFolder);
+ select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+ let box = get_notification(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ // Click on the "Edit" button in the draft notification.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ let cwc = wait_for_compose_window();
+
+ let cwins = [...Services.wm.getEnumerator("msgcompose")].length;
+
+ // click edit in main win again
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+
+ // Wait a sec to see if it caused a new window.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ Assert.ok(
+ Services.ww.activeWindow == cwc.window,
+ "the original draft composition window should have got focus (again)"
+ );
+
+ let cwins2 = [...Services.wm.getEnumerator("msgcompose")].length;
+
+ Assert.ok(cwins2 > 0, "No compose window open!");
+ Assert.equal(cwins, cwins2, "The number of compose windows changed!");
+
+ // Type something and save, then check that we only have one draft.
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Hello!", cwc.window);
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+ Assert.equal(draftsFolder.getTotalMessages(false), 1);
+
+ select_click_row(0);
+ press_delete(mc); // clean up after ourselves
+});
+
+/**
+ * Bug 1202165
+ * Test that the user set delivery format is preserved in a draft message.
+ */
+async function internal_check_delivery_format(editDraft) {
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing storing of the composition properties in the draft!",
+ "Hello!"
+ );
+
+ // Select our wanted format.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("optionsMenu"),
+ {},
+ cwc.window.document.getElementById("optionsMenu").ownerGlobal
+ );
+ await click_menus_in_sequence(
+ cwc.window.document.getElementById("optionsMenuPopup"),
+ [{ id: "outputFormatMenu" }, { id: "format_both" }]
+ );
+
+ /**
+ * Check if the right format is selected in the menu.
+ *
+ * @param aMenuItemId The id of the menuitem expected to be selected.
+ * @param aValue A value of nsIMsgCompSendFormat constants of the expected selected format.
+ */
+ async function assert_format_value(aMenuItemId, aValue) {
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("optionsMenu"),
+ {},
+ cwc.window.document.getElementById("optionsMenu").ownerGlobal
+ );
+ let formatMenu = await click_menus_in_sequence(
+ cwc.window.document.getElementById("optionsMenuPopup"),
+ [{ id: "outputFormatMenu" }],
+ true
+ );
+ let formatItem = cwc.window.document
+ .getElementById("outputFormatMenuPopup")
+ .querySelector("[name=output_format][checked=true]");
+ Assert.equal(formatItem.id, aMenuItemId);
+ close_popup_sequence(formatMenu);
+ }
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ // Open a new composition see if the menu is again at default value, not the one
+ // chosen above.
+ cwc = open_compose_new_mail();
+
+ await assert_format_value("format_auto", Ci.nsIMsgCompSendFormat.Auto);
+
+ close_compose_window(cwc);
+
+ await be_in_folder(draftsFolder);
+ select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+ let box = get_notification(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ if (editDraft) {
+ // Trigger "edit draft".
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ } else {
+ // Trigger "edit as new" resulting in template processing.
+ EventUtils.synthesizeKey(
+ "e",
+ { shiftKey: false, accelKey: true },
+ mc.window
+ );
+ }
+ cwc = wait_for_compose_window();
+
+ // Check if format value was restored.
+ await assert_format_value("format_both", Ci.nsIMsgCompSendFormat.Both);
+
+ close_compose_window(cwc);
+
+ press_delete(mc); // clean up the created draft
+}
+
+add_task(async function test_save_delivery_format_with_edit_draft() {
+ await internal_check_delivery_format(true);
+}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac.
+
+add_task(async function test_save_delivery_format_with_edit_template() {
+ await internal_check_delivery_format(false);
+}).__skipMe = AppConstants.platform == "macosx"; // Can't click menu bar on Mac.
+
+/**
+ * Tests that 'Edit as New' leaves the original message in drafts folder.
+ */
+add_task(async function test_edit_as_new_in_draft() {
+ await make_message_sets_in_folders([draftsFolder], [{ count: 1 }]);
+ await be_in_folder(draftsFolder);
+
+ Assert.equal(draftsFolder.getTotalMessages(false), 1);
+
+ select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ EventUtils.synthesizeKey("e", { shiftKey: false, accelKey: true });
+ let cwc = wait_for_compose_window();
+
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Hello!", cwc.window);
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) == 2,
+ "message saved to drafts folder"
+ );
+
+ // Clean up the created drafts and count again.
+ press_delete(mc);
+ select_click_row(0);
+ press_delete(mc);
+ Assert.equal(draftsFolder.getTotalMessages(false), 0);
+});
+
+/**
+ * Tests that editing a draft works as it should also when the identity
+ * name has properties that require mime encoding when sent out.
+ */
+add_task(async function test_edit_draft_mime_from() {
+ const identity = MailServices.accounts.createIdentity();
+ identity.email = "skinner@example.com";
+ identity.fullName = "SKINNER, Seymore";
+ const accounts = MailServices.accounts.accounts.at(-1); // Local Folders
+ accounts.addIdentity(identity);
+ registerCleanupFunction(() => {
+ accounts.removeIdentity(identity);
+ });
+
+ draftsFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .addMessage(
+ "From - Sun Oct 01 01:02:03 2023\n" +
+ "X-Mozilla-Status: 0000\n" +
+ "X-Mozilla-Status2: 00000000\n" +
+ "X-Mozilla-Keys:\n" +
+ `X-Account-Key: ${accounts.key}\n` +
+ `From: "SKINNER, Seymore <skinner@example.com>\n` +
+ "To: nobody@example.invalid\n" +
+ "Subject: test_edit_draft_mime_from!\n" +
+ `Message-ID: <${Date.now()}@example.invalid>\n` +
+ "Date: Sun, 1 Oct 2017 01:02:03 +0100\n" +
+ "X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;\n" +
+ " attachmentreminder=0; deliveryformat=4\n" +
+ "MIME-Version: 1.0\n" +
+ "Content-Type: text/plain; charset=utf-8\n" +
+ "Content-Transfer-Encoding: 8bit\n" +
+ "\n" +
+ "Identitiy names should not show quotes!.\n"
+ );
+ await be_in_folder(draftsFolder);
+
+ Assert.equal(
+ draftsFolder.getTotalMessages(false),
+ 1,
+ "should have one draft"
+ );
+
+ select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ EventUtils.synthesizeKey("e", { shiftKey: false, accelKey: true });
+ let cwc = wait_for_compose_window();
+
+ const msgIdentity = cwc.window.document.getElementById("msgIdentity");
+ // Should show no quotes in the address.
+ Assert.equal(
+ msgIdentity.value,
+ "SKINNER, Seymore <skinner@example.com>",
+ "should show human readable version of identity"
+ );
+ // Should not be editable - which it would be if no identity matched.
+ Assert.equal(
+ msgIdentity.getAttribute("editable"),
+ "",
+ "msgIdentity should not be editable since a draft identity email matches"
+ );
+
+ close_compose_window(cwc);
+ // Clean up the created draft and count again.
+ press_delete(mc);
+ Assert.equal(
+ draftsFolder.getTotalMessages(false),
+ 0,
+ "should have no drafts after deleting"
+ );
+});
+
+/**
+ * Tests Content-Language header.
+ */
+add_task(async function test_content_language_header() {
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing Content-Language header",
+ "Hello, we speak en-US"
+ );
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ await be_in_folder(draftsFolder);
+ let draftMsg = select_click_row(0);
+ let draftMsgContent = await get_msg_source(draftMsg);
+
+ // Check for a single line that contains our header.
+ if (
+ !draftMsgContent
+ .split("\n")
+ .some(line => line.trim() == "Content-Language: en-US")
+ ) {
+ Assert.ok(false, "Failed to find Content-Language: en-US");
+ }
+
+ // Clean up the created draft.
+ press_delete(mc);
+});
+
+/**
+ * Tests Content-Language header suppression.
+ */
+add_task(async function test_content_language_header_suppression() {
+ let statusQuo = Services.prefs.getBoolPref("mail.suppress_content_language");
+ Services.prefs.setBoolPref("mail.suppress_content_language", true);
+
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing Content-Language header suppression",
+ "Hello, we speak blank"
+ );
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ await be_in_folder(draftsFolder);
+ let draftMsg = select_click_row(0);
+ let draftMsgContent = await get_msg_source(draftMsg);
+
+ // Check no line contains our Content-Language.
+ Assert.ok(
+ !draftMsgContent.split("\n").some(line => /^Content-Language:/.test(line)),
+ "Didn't find Content-Language header in draft content"
+ );
+
+ // Clean up the created draft.
+ press_delete(mc);
+
+ Services.prefs.setBoolPref("mail.suppress_content_language", statusQuo);
+});
+
+/**
+ * Tests space stuffing of plaintext message.
+ */
+add_task(async function test_remove_space_stuffing_format_flowed() {
+ // Prepare for plaintext email.
+ let oldHtmlPref = Services.prefs.getBoolPref(
+ "mail.identity.default.compose_html"
+ );
+ Services.prefs.setBoolPref("mail.identity.default.compose_html", false);
+
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(
+ cwc,
+ "test@example.invalid",
+ "Testing space stuffing in plain text email",
+ "NoSpace\n OneSpace\n TwoSpaces"
+ );
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ await be_in_folder(draftsFolder);
+
+ select_click_row(0);
+
+ // Wait for the notification with the Edit button.
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+ let box = get_notification(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ // Click on the "Edit" button in the draft notification.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ cwc = wait_for_compose_window();
+
+ let bodyText = get_compose_body(cwc).innerHTML;
+ if (!bodyText.includes("NoSpace<br> OneSpace<br> TwoSpaces")) {
+ Assert.ok(false, "Something went wrong with space stuffing");
+ }
+ close_compose_window(cwc);
+
+ // Clean up the created draft.
+ press_delete(mc);
+
+ Services.prefs.setBoolPref("mail.identity.default.compose_html", oldHtmlPref);
+});
diff --git a/comm/mail/test/browser/composition/browser_emlActions.js b/comm/mail/test/browser/composition/browser_emlActions.js
new file mode 100644
index 0000000000..cf24be0b23
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_emlActions.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+/**
+ * Tests that actions such as replying to an .eml works properly.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ get_compose_body,
+ open_compose_with_forward,
+ open_compose_with_reply,
+ save_compose_message,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ open_message_from_file,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDrafts;
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Test that replying to an opened .eml message works, and that the reply can
+ * be saved as a draft.
+ */
+add_task(async function test_reply_to_eml_save_as_draft() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/testmsg.eml"));
+ let msgc = await open_message_from_file(file);
+
+ let replyWin = open_compose_with_reply(msgc);
+
+ // Ctrl+S saves as draft.
+ await save_compose_message(replyWin.window);
+ close_compose_window(replyWin);
+
+ await TestUtils.waitForCondition(
+ () => gDrafts.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ // Drafts folder should exist now.
+ await be_in_folder(gDrafts);
+ let draftMsg = select_click_row(0);
+ if (!draftMsg) {
+ throw new Error("No draft saved!");
+ }
+ press_delete(); // Delete the draft.
+
+ close_window(msgc); // close base .eml message
+});
+
+/**
+ * Test that forwarding an opened .eml message works, and that the forward can
+ * be saved as a draft.
+ */
+add_task(async function test_forward_eml_save_as_draft() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/testmsg.eml"));
+ let msgc = await open_message_from_file(file);
+
+ let replyWin = open_compose_with_forward(msgc);
+
+ await save_compose_message(replyWin.window);
+ close_compose_window(replyWin);
+
+ await TestUtils.waitForCondition(
+ () => gDrafts.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ // Drafts folder should exist now.
+ await be_in_folder(gDrafts);
+ let draftMsg = select_click_row(0);
+ if (!draftMsg) {
+ throw new Error("No draft saved!");
+ }
+ press_delete(); // Delete the draft.
+
+ close_window(msgc); // close base .eml message
+});
+
+/**
+ * Test that MIME encoded subject is decoded when replying to an opened .eml.
+ */
+add_task(async function test_reply_eml_subject() {
+ // Open an .eml file whose subject is encoded.
+ let file = new FileUtils.File(
+ getTestFilePath("data/mime-encoded-subject.eml")
+ );
+ let msgc = await open_message_from_file(file);
+
+ let replyWin = open_compose_with_reply(msgc);
+
+ Assert.equal(
+ replyWin.window.document.getElementById("msgSubject").value,
+ "Re: \u2200a\u220aA"
+ );
+ close_compose_window(replyWin); // close compose window
+ close_window(msgc); // close base .eml message
+});
+
+/**
+ * Test that replying to a base64 encoded .eml works.
+ */
+add_task(async function test_reply_to_base64_eml() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/base64-encoded-msg.eml"));
+ let msgc = await open_message_from_file(file);
+ let compWin = open_compose_with_reply(msgc);
+ let bodyText = get_compose_body(compWin).textContent;
+ const TXT = "You have decoded this text from base64.";
+ Assert.ok(bodyText.includes(TXT), "body should contain the decoded text");
+ close_compose_window(compWin);
+ close_window(msgc);
+});
+
+/**
+ * Test that forwarding a base64 encoded .eml works.
+ */
+add_task(async function test_forward_base64_eml() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/base64-encoded-msg.eml"));
+ let msgc = await open_message_from_file(file);
+ let compWin = open_compose_with_forward(msgc);
+ let bodyText = get_compose_body(compWin).textContent;
+ const TXT = "You have decoded this text from base64.";
+ Assert.ok(bodyText.includes(TXT), "body should contain the decoded text");
+ close_compose_window(compWin);
+ close_window(msgc);
+});
+
+/**
+ * Test that replying and forwarding an evil meta msg works.
+ */
+add_task(async function test_reply_fwd_to_evil_meta() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/evil-meta-msg.eml"));
+ let msgc = await open_message_from_file(file);
+
+ const TXT = "KABOOM!";
+
+ let reWin = open_compose_with_reply(msgc);
+ let reText = get_compose_body(reWin).textContent;
+ Assert.ok(reText.includes(TXT), "re body should contain the text");
+ close_compose_window(reWin);
+
+ let fwdWin = open_compose_with_forward(msgc);
+ let fwdText = get_compose_body(fwdWin).textContent;
+ Assert.ok(fwdText.includes(TXT), "fwd body should contain the text");
+ close_compose_window(fwdWin);
+
+ close_window(msgc);
+});
+
+/**
+ * Test that forwarding an opened .eml message works with catchAll enabled.
+ */
+add_task(async function test_forward_eml_catchall() {
+ // Open an .eml file.
+ let file = new FileUtils.File(getTestFilePath("data/testmsg.eml"));
+ let msgc = await open_message_from_file(file);
+
+ MailServices.accounts.defaultAccount.defaultIdentity.catchAll = true;
+
+ let replyWin = open_compose_with_forward(msgc);
+ let bodyText = get_compose_body(replyWin).textContent;
+ const message = "Because they're stupid, that's why";
+ Assert.ok(bodyText.includes(message), "Correct message body");
+
+ MailServices.accounts.defaultAccount.defaultIdentity.catchAll = false;
+
+ close_compose_window(replyWin); // close compose window
+ close_window(msgc); // close base .eml message
+});
diff --git a/comm/mail/test/browser/composition/browser_encryptedBccRecipients.js b/comm/mail/test/browser/composition/browser_encryptedBccRecipients.js
new file mode 100644
index 0000000000..65d8542b69
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_encryptedBccRecipients.js
@@ -0,0 +1,283 @@
+/* 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/. */
+
+/**
+ * Tests for the notification displayed when Bcc recipients are used while
+ * encryption is enabled.
+ */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+var { be_in_folder } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_compose_window, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+let bobAcct;
+
+async function waitCheckEncryptionStateDone(win) {
+ return BrowserTestUtils.waitForEvent(
+ win.document,
+ "encryption-state-checked"
+ );
+}
+
+/**
+ * Setup an account with OpenPGP for testing.
+ */
+add_setup(async function () {
+ bobAcct = MailServices.accounts.createAccount();
+ bobAcct.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob",
+ "openpgp.example",
+ "imap"
+ );
+
+ let bobIdentity = MailServices.accounts.createIdentity();
+ bobIdentity.email = "bob@openpgp.example";
+ bobAcct.addIdentity(bobIdentity);
+
+ let [id] = await OpenPGPTestUtils.importPrivateKey(
+ window,
+ new FileUtils.File(
+ getTestFilePath(
+ "../openpgp/data/keys/bob@openpgp.example-0xfbfcc82a015e7330-secret.asc"
+ )
+ )
+ );
+
+ Assert.ok(id, "private key id received");
+ bobIdentity.setUnicharAttribute("openpgp_key_id", id.split("0x").join(""));
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeIncomingServer(bobAcct.incomingServer, true);
+ MailServices.accounts.removeAccount(bobAcct, true);
+ });
+});
+
+/**
+ * Test the warning is shown when encryption is enabled.
+ */
+add_task(async function testWarningShowsWhenEncryptionEnabled() {
+ await be_in_folder(bobAcct.incomingServer.rootFolder);
+
+ let cwc = open_compose_new_mail();
+
+ Assert.ok(!cwc.window.gSendEncrypted);
+
+ // This toggle will trigger checkEncryptionState(), request that
+ // an event will be sent after the next call to checkEncryptionState
+ // has completed.
+ let checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ await OpenPGPTestUtils.toggleMessageEncryption(cwc.window);
+ await checkDonePromise;
+
+ Assert.ok(cwc.window.gSendEncrypted);
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+
+ // setup_msg_contents will trigger checkEncryptionState.
+ checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "Encryption Enabled ",
+ "",
+ "bccAddrInput"
+ );
+ await checkDonePromise;
+
+ // Warning should show when encryption enabled
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "Timeout waiting for warnEncryptedBccRecipients notification"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test dismissing the warning works.
+ */
+add_task(async function testNotificationDismissal() {
+ await be_in_folder(bobAcct.incomingServer.rootFolder);
+
+ let cwc = open_compose_new_mail();
+
+ Assert.ok(!cwc.window.gSendEncrypted);
+
+ // This toggle will trigger checkEncryptionState(), request that
+ // an event will be sent after the next call to checkEncryptionState
+ // has completed.
+ let checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ await OpenPGPTestUtils.toggleMessageEncryption(cwc.window);
+ await checkDonePromise;
+
+ Assert.ok(cwc.window.gSendEncrypted);
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+
+ // setup_msg_contents will trigger checkEncryptionState.
+ checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "Warning Dismissal",
+ "",
+ "bccAddrInput"
+ );
+ await checkDonePromise;
+
+ // Warning should show when encryption enabled
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "Timeout waiting for warnEncryptedBccRecipients notification"
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "notification was not removed in time"
+ );
+
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.lastElementChild,
+ {},
+ cwc.window
+ );
+ await notificationHidden;
+
+ Assert.ok(
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "notification should be removed"
+ );
+
+ // setup_msg_contents will trigger checkEncryptionState.
+ checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ setup_msg_contents(cwc, "test2@example.org", "", "", "bccAddrInput");
+ await checkDonePromise;
+
+ // Give the notification some time to incorrectly appear.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ Assert.ok(
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "notification should not reappear after dismissal"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning does not show when encryption is not enabled.
+ */
+add_task(async function testNoWarningWhenEncryptionDisabled() {
+ await be_in_folder(bobAcct.incomingServer.rootFolder);
+
+ let cwc = open_compose_new_mail();
+
+ Assert.ok(!window.gSendEncrypted);
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+
+ // setup_msg_contents will trigger checkEncryptionState.
+ let checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ setup_msg_contents(
+ cwc,
+ "test@example.org",
+ "No Warning ",
+ "",
+ "bccAddrInput"
+ );
+ await checkDonePromise;
+
+ // Give the notification some time to incorrectly appear.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ Assert.ok(
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "warning should not show when encryption disabled"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning does not show when the Bcc recipient is the sender.
+ */
+add_task(async function testNoWarningWhenBccRecipientIsSender() {
+ await be_in_folder(bobAcct.incomingServer.rootFolder);
+
+ let cwc = open_compose_new_mail();
+
+ Assert.ok(!window.gSendEncrypted);
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_bccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+
+ // setup_msg_contents will trigger checkEncryptionState.
+ let checkDonePromise = waitCheckEncryptionStateDone(cwc.window);
+ setup_msg_contents(
+ cwc,
+ "bob@openpgp.example",
+ "Bcc Self",
+ "",
+ "bccAddrInput"
+ );
+ await checkDonePromise;
+
+ // Give the notification some time to incorrectly appear.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ Assert.ok(
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnEncryptedBccRecipients"
+ ),
+ "warning should not show when Bcc recipient is the sender"
+ );
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_expandLists.js b/comm/mail/test/browser/composition/browser_expandLists.js
new file mode 100644
index 0000000000..03041400cb
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_expandLists.js
@@ -0,0 +1,151 @@
+/* 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/. */
+
+/**
+ * Tests for the "Expand List" mail pill context menu.
+ */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var { close_compose_window, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+/**
+ * Tests mailing list expansion works via the mail pill context menu.
+ *
+ * @param {Window} win The compose window.
+ * @param {string} target The id of the mail pill container to test expansion on.
+ * @param {string} addresses A comma separated string of addresses to put in
+ * the target field. Instances of "Test List" will be replaced to test that the
+ * expansion was successful.
+ */
+async function testListExpansion(win, target, addresses) {
+ let menu = win.document.getElementById("emailAddressPillPopup");
+ let menuItem = win.document.getElementById("expandList");
+ let shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ let container = win.document.getElementById(target);
+ let listPill = Array.from(
+ container.querySelectorAll("mail-address-pill")
+ ).find(pill => pill.isMailList);
+
+ EventUtils.synthesizeMouseAtCenter(listPill, { type: "contextmenu" }, win);
+ await shownPromise;
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.activateItem(menuItem);
+ await hiddenPromise;
+
+ let expected = [];
+ for (let addr of addresses.split(",")) {
+ if (addr == "Test List") {
+ expected.push("Member 0 <member0@example>");
+ expected.push("Member 1 <member1@example>");
+ expected.push("Member 2 <member2@example>");
+ } else {
+ expected.push(addr);
+ }
+ }
+
+ let allPills = [];
+ await TestUtils.waitForCondition(() => {
+ allPills = Array.from(container.querySelectorAll("mail-address-pill"));
+ return allPills.length == expected.length;
+ }, "expanded list pills did not appear in time");
+
+ Assert.equal(
+ allPills.map(pill => pill.fullAddress).join(","),
+ expected.join(","),
+ "mail list pills were expanded correctly"
+ );
+}
+
+/**
+ * Creates the mailing list used during the tests.
+ */
+add_setup(async function () {
+ let book = MailServices.ab.directories[0];
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "Test List";
+ list = book.addMailList(list);
+
+ for (let i = 0; i < 3; i++) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.primaryEmail = `member${i}@example`;
+ card.displayName = `Member ${i}`;
+ list.addCard(card);
+ }
+ list.editMailListToDatabase(null);
+});
+
+/**
+ * Tests the "Expand List" menu option works with the "To" list.
+ */
+add_task(async function testExpandListsOnTo() {
+ let cwc = open_compose_new_mail();
+ let addresses = "start@example,Test List,end@example";
+
+ setup_msg_contents(cwc, addresses, "Expand To Test", "");
+ await testListExpansion(cwc.window, "toAddrContainer", addresses);
+ close_compose_window(cwc);
+});
+
+/**
+ * Tests the "Expand List" menu option works with the "To" list,
+ * with invalid pills involved.
+ */
+add_task(async function testExpandListsInvalidPill() {
+ let cwc = open_compose_new_mail();
+ // We add one invalid pill in the middle so see that parsing out the
+ // addresses still works correctly for that case.
+ let addresses =
+ "start@example,invalidpill,Test List,end@example,invalidpill2";
+
+ setup_msg_contents(cwc, addresses, "Expand To Test Invalid Pill", "");
+ await testListExpansion(cwc.window, "toAddrContainer", addresses);
+ close_compose_window(cwc);
+});
+
+/**
+ * Tests the "Expand List" menu option works with the "Cc" list.
+ */
+add_task(async function testExpandListsOnCc() {
+ let cwc = open_compose_new_mail();
+ let button = cwc.window.document.getElementById(
+ "addr_ccShowAddressRowButton"
+ );
+ let addresses = "start@example,Test List,end@example";
+
+ button.click();
+ setup_msg_contents(cwc, addresses, "Expand Cc Test", "", "ccAddrInput");
+ await testListExpansion(cwc.window, "ccAddrContainer", addresses);
+ close_compose_window(cwc);
+});
+
+/**
+ * Tests the "Expand List" menu option works with the "Bcc" list.
+ */
+add_task(async function testExpandListsOnBcc() {
+ let cwc = open_compose_new_mail();
+ let button = cwc.window.document.getElementById(
+ "addr_bccShowAddressRowButton"
+ );
+ let addresses = "start@example,Test List,end@example";
+
+ button.click();
+ setup_msg_contents(cwc, addresses, "Expand Bcc Test", "", "bccAddrInput");
+ await testListExpansion(cwc.window, "bccAddrContainer", addresses);
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_focus.js b/comm/mail/test/browser/composition/browser_focus.js
new file mode 100644
index 0000000000..852f2e99cc
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_focus.js
@@ -0,0 +1,523 @@
+/* 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/. */
+
+/*
+ * Test that cycling through the focus of the 3pane's panes works correctly.
+ */
+
+"use strict";
+
+var { add_attachments, close_compose_window, open_compose_new_mail } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { mc } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+requestLongerTimeout(3);
+
+/**
+ * Test the cycling of focus in the composition window through (Shift+)F6.
+ *
+ * @param {MozMillController} controller - Controller for the compose window.
+ * @param {object} options - Options to set for the test.
+ * @param {boolean} options.useTab - Whether to use Ctrl+Tab instead of F6.
+ * @param {boolean} options.attachment - Whether to add an attachment.
+ * @param {boolean} options.notifications - Whether to show notifications.
+ * @param {boolean} options.languageButton - Whether to show the language
+ * menu button.
+ * @param {boolean} options.contacts - Whether to show the contacts side pane.
+ * @param {string} otherHeader - The name of the custom header to show.
+ */
+async function checkFocusCycling(controller, options) {
+ let win = controller.window;
+ let doc = win.document;
+ let contactDoc;
+ let contactsInput;
+ let identityElement = doc.getElementById("msgIdentity");
+ let bccButton = doc.getElementById("addr_bccShowAddressRowButton");
+ let toInput = doc.getElementById("toAddrInput");
+ let bccInput = doc.getElementById("bccAddrInput");
+ let subjectInput = doc.getElementById("msgSubject");
+ let editorElement = doc.getElementById("messageEditor");
+ let attachmentElement = doc.getElementById("attachmentBucket");
+ let extraMenuButton = doc.getElementById("extraAddressRowsMenuButton");
+ let languageButton = doc.getElementById("languageStatusButton");
+ let firstNotification;
+ let secondNotification;
+
+ if (Services.ww.activeWindow != win) {
+ // Wait for the window to be in focus before beginning.
+ await BrowserTestUtils.waitForEvent(win, "activate");
+ }
+
+ let key = options.useTab ? "VK_TAB" : "VK_F6";
+ let goForward = () =>
+ EventUtils.synthesizeKey(key, { ctrlKey: options.useTab }, win);
+ let goBackward = () =>
+ EventUtils.synthesizeKey(
+ key,
+ { ctrlKey: options.useTab, shiftKey: true },
+ win
+ );
+
+ if (options.attachment) {
+ add_attachments(controller, "https://www.mozilla.org/");
+ }
+
+ if (options.contacts) {
+ // Open the contacts sidebar.
+ EventUtils.synthesizeKey("VK_F9", {}, win);
+ contactsInput = await TestUtils.waitForCondition(() => {
+ contactDoc = doc.getElementById("contactsBrowser").contentDocument;
+ return contactDoc.getElementById("peopleSearchInput");
+ }, "Waiting for the contacts pane to load");
+ }
+
+ if (options.languageButton) {
+ // languageButton only shows if we have more than one dictionary, but we
+ // will show it anyway.
+ languageButton.hidden = false;
+ }
+
+ // Show the bcc row by clicking the button.
+ EventUtils.synthesizeMouseAtCenter(bccButton, {}, win);
+
+ // Show the custom row.
+ let otherRow = doc.querySelector(
+ `.address-row[data-recipienttype="${options.otherHeader}"]`
+ );
+ // Show the input.
+ let menu = doc.getElementById("extraAddressRowsMenu");
+ let promise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(extraMenuButton, {}, win);
+ await promise;
+ promise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.activateItem(doc.getElementById(otherRow.dataset.showSelfMenuitem));
+ await promise;
+ let otherHeaderInput = otherRow.querySelector(".address-row-input");
+
+ // Move the initial focus back to the To input.
+ toInput.focus();
+
+ if (options.notifications) {
+ // Exceed the recipient threshold.
+ Assert.equal(
+ win.gComposeNotification.allNotifications.length,
+ 0,
+ "Should be no initial notifications"
+ );
+ let notificationPromise = TestUtils.waitForCondition(
+ () => win.gComposeNotification.allNotifications[0],
+ "First notification shown"
+ );
+ EventUtils.sendString("a@b.org,c@d.org", win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ firstNotification = await notificationPromise;
+ }
+
+ // We start on the addressing widget and go from there.
+
+ // From To to Subject.
+ goForward();
+ Assert.ok(bccInput.matches(":focus"), "forward to bcc row");
+ goForward();
+ Assert.ok(otherHeaderInput.matches(":focus"), "forward to other row");
+ goForward();
+ Assert.ok(subjectInput.matches(":focus"), "forward to subject");
+
+ if (options.notifications && !options.attachment) {
+ // Include an attachment key word in the subject.
+ let notificationPromise = TestUtils.waitForCondition(() => {
+ let notifications = win.gComposeNotification.allNotifications;
+ if (notifications.length != 2) {
+ return null;
+ }
+ return notifications[1];
+ }, "Second notification shown");
+ EventUtils.sendString("My attached file", win);
+ secondNotification = await notificationPromise;
+ Assert.notEqual(
+ firstNotification,
+ secondNotification,
+ "New notification shown second"
+ );
+ }
+
+ // From Subject to Message Body.
+ goForward();
+ // The editor's body will not match ":focus", even when it has focus, instead,
+ // we use the parent window's activeElement.
+ Assert.equal(editorElement, doc.activeElement, "forward to message body");
+
+ // From Message Body to Attachment bucket if visible.
+ goForward();
+ if (options.attachment) {
+ Assert.ok(attachmentElement.matches(":focus"), "forward to attachments");
+ goForward();
+ }
+
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "forward to notification"
+ );
+ goForward();
+ }
+
+ // From Message Body (or Attachment bucket) to Language button.
+ if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "forward to status bar");
+ goForward();
+ }
+
+ // From Language button to contacts pane.
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "forward to contacts pane"
+ );
+ goForward();
+ }
+
+ // From contacts pane to identity.
+ Assert.ok(identityElement.matches(":focus"), "forward to 'from' row");
+
+ // Back to the To input.
+ goForward();
+ Assert.ok(toInput.matches(":focus"), "forward to 'to' row");
+
+ // Reverse the direction.
+
+ goBackward();
+ Assert.ok(identityElement.matches(":focus"), "backward to 'from' row");
+
+ goBackward();
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "backward to contacts pane"
+ );
+ goBackward();
+ }
+
+ if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "backward to status bar");
+ goBackward();
+ }
+
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "backward to notification"
+ );
+ goBackward();
+ }
+
+ if (options.attachment) {
+ Assert.ok(attachmentElement.matches(":focus"), "backward to attachments");
+ goBackward();
+ }
+
+ Assert.equal(editorElement, doc.activeElement, "backward to message body");
+ goBackward();
+ Assert.ok(subjectInput.matches(":focus"), "backward to subject");
+ goBackward();
+ Assert.ok(otherHeaderInput.matches(":focus"), "backward to other row");
+ goBackward();
+ Assert.ok(bccInput.matches(":focus"), "backward to bcc row");
+ goBackward();
+
+ Assert.ok(toInput.matches(":focus"), "backward to 'to' row");
+
+ // Now test some other elements that aren't the main focus point of their
+ // areas. I.e. focusable elements that are within an area, but are not
+ // focused when the area is *entered* through F6 or Ctrl+Tab. When these
+ // elements have focus, we still want F6 or Ctrl+Tab to move the focus to the
+ // neighbouring area.
+
+ // Focus the close button.
+ let bccCloseButton = doc.querySelector("#addressRowBcc .remove-field-button");
+ bccCloseButton.focus();
+ goForward();
+ Assert.ok(
+ otherHeaderInput.matches(":focus"),
+ "from close bcc button to other row"
+ );
+ goBackward();
+ // The input is focused on return.
+ Assert.ok(bccInput.matches(":focus"), "back to bcc row");
+ // Same the other way.
+ bccCloseButton.focus();
+ goBackward();
+ Assert.ok(toInput.matches(":focus"), "from close bcc button to 'to' row");
+
+ if (options.contacts) {
+ let addressBookList = contactDoc.getElementById("addressbookList");
+ addressBookList.focus();
+ goForward();
+ Assert.ok(
+ identityElement.matches(":focus"),
+ "from addressbook selector to 'from' row"
+ );
+ goBackward();
+ // The input is focused on return.
+ Assert.ok(contactsInput.matches(":focus-within"), "back to contacts input");
+ // Same the other way.
+ addressBookList.focus();
+ goBackward();
+ if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ "from addressbook selector to status bar"
+ );
+ } else if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "from addressbook selector to notification"
+ );
+ } else if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "from addressbook selector to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "from addressbook selector to message body"
+ );
+ }
+ }
+
+ // Cc button and extra address rows menu button are in the same area as the
+ // message identity.
+ let ccButton = doc.getElementById("addr_ccShowAddressRowButton");
+ ccButton.focus();
+ goBackward();
+ if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "from Cc button to contacts"
+ );
+ } else if (options.languageButton) {
+ Assert.ok(languageButton.matches(":focus"), "from Cc button to status bar");
+ } else if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "from Cc button to notification"
+ );
+ } else if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "from Cc button to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "from Cc button to message body"
+ );
+ }
+ goForward();
+ // Return to the input.
+ Assert.ok(identityElement.matches(":focus"), "back to 'from' row");
+
+ // Try in the other direction with the extra menu button.
+ extraMenuButton.focus();
+ goForward();
+ Assert.ok(toInput.matches(":focus"), "from extra menu button to 'to' row");
+ goBackward();
+ // Return to the input.
+ Assert.ok(identityElement.matches(":focus"), "back to 'from' row again");
+
+ if (options.attachment) {
+ let attachmentArea = doc.getElementById("attachmentArea");
+ let attachmentSummary = attachmentArea.querySelector("summary");
+ Assert.ok(attachmentArea.open, "Attachment area should be open");
+ for (let open of [true, false]) {
+ if (open) {
+ Assert.ok(attachmentArea.open, "Attachment area should be open");
+ } else {
+ // Close the attachment bucket. In this case, the focus will move to the
+ // summary element (where the bucket can be shown again).
+ EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
+ Assert.ok(!attachmentArea.open, "Attachment area should be closed");
+ }
+
+ // Focus the attachmentSummary.
+ attachmentSummary.focus();
+ goBackward();
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ `backward from attachment summary (open: ${open}) to message body`
+ );
+ goForward();
+ if (open) {
+ // Focus returns to the bucket when it is open.
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "forward to attachment bucket"
+ );
+ } else {
+ // Otherwise, it returns to the summary.
+ Assert.ok(
+ attachmentSummary.matches(":focus"),
+ "forward to attachment summary"
+ );
+ }
+ // Try reverse.
+ attachmentSummary.focus();
+ goForward();
+ if (options.notifications) {
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ `forward from attachment summary (open: ${open}) to notification`
+ );
+ } else if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ `forward from attachment summary (open: ${open}) to status bar`
+ );
+ } else if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ `forward from attachment summary (open: ${open}) to contacts pane`
+ );
+ } else {
+ Assert.ok(
+ identityElement.matches(":focus"),
+ `forward from attachment summary (open: ${open}) to 'from' row`
+ );
+ }
+ goBackward();
+ if (open) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "return to attachment bucket"
+ );
+ } else {
+ Assert.ok(
+ attachmentSummary.matches(":focus"),
+ "return to attachment summary"
+ );
+ // Open again.
+ EventUtils.synthesizeMouseAtCenter(attachmentSummary, {}, win);
+ Assert.ok(attachmentArea.open, "Attachment area should be open again");
+ }
+ }
+ }
+
+ if (options.notifications) {
+ // Focus inside the notification.
+ let closeButton = (secondNotification || firstNotification).closeButton;
+ closeButton.focus();
+
+ goBackward();
+
+ if (options.attachment) {
+ Assert.ok(
+ attachmentElement.matches(":focus"),
+ "backward from notification button to attachments"
+ );
+ } else {
+ Assert.equal(
+ editorElement,
+ doc.activeElement,
+ "backward from notification button to message body"
+ );
+ }
+ goForward();
+ // Go to the first notification.
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "forward to the first notification"
+ );
+
+ // Try reverse.
+ closeButton.focus();
+ goForward();
+ if (options.languageButton) {
+ Assert.ok(
+ languageButton.matches(":focus"),
+ "forward from notification button to status bar"
+ );
+ } else if (options.contacts) {
+ Assert.ok(
+ contactsInput.matches(":focus-within"),
+ "forward from notification button to contacts pane"
+ );
+ } else {
+ Assert.ok(
+ identityElement.matches(":focus"),
+ "forward from notification button to 'from' row"
+ );
+ }
+ goBackward();
+ Assert.equal(
+ firstNotification,
+ doc.activeElement,
+ "return to the first notification"
+ );
+ }
+
+ // Contacts pane is persistent, so we close it again.
+ if (options.contacts) {
+ // Close the contacts sidebar.
+ EventUtils.synthesizeKey("VK_F9", {}, win);
+ }
+}
+
+add_task(async function test_jump_focus() {
+ // Make sure the accessibility tabfocus is set to 7 to enable normal Tab
+ // focus on non-input field elements. This is necessary only for macOS as
+ // the default value is 2 instead of the default 7 used on Windows and Linux.
+ Services.prefs.setIntPref("accessibility.tabfocus", 7);
+ let prevHeader = Services.prefs.getCharPref("mail.compose.other.header");
+ let prevThreshold = Services.prefs.getIntPref(
+ "mail.compose.warn_public_recipients.threshold"
+ );
+ // Set two custom headers, but only one is shown.
+ Services.prefs.setCharPref(
+ "mail.compose.other.header",
+ "X-Header2,X-Header1"
+ );
+ Services.prefs.setIntPref("mail.compose.warn_public_recipients.threshold", 2);
+ for (let useTab of [false, true]) {
+ for (let attachment of [false, true]) {
+ for (let notifications of [false, true]) {
+ for (let languageButton of [false, true]) {
+ for (let contacts of [false, true]) {
+ let options = {
+ useTab,
+ attachment,
+ notifications,
+ languageButton,
+ contacts,
+ otherHeader: "X-Header1",
+ };
+ info(`Test run: ${JSON.stringify(options)}`);
+ let controller = open_compose_new_mail();
+ await checkFocusCycling(controller, options);
+ close_compose_window(controller);
+ }
+ }
+ }
+ }
+ }
+
+ // Reset the preferences.
+ Services.prefs.clearUserPref("accessibility.tabfocus");
+ Services.prefs.setCharPref("mail.compose.other.header", prevHeader);
+ Services.prefs.setIntPref(
+ "mail.compose.warn_public_recipients.threshold",
+ prevThreshold
+ );
+});
diff --git a/comm/mail/test/browser/composition/browser_font_color.js b/comm/mail/test/browser/composition/browser_font_color.js
new file mode 100644
index 0000000000..e534ab65be
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_font_color.js
@@ -0,0 +1,114 @@
+/* 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/. */
+
+/**
+ * Test font color in messages.
+ */
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_font_color() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let colorSet = [
+ { value: "#0000ff", rgb: [0, 0, 255] },
+ { value: "#fb3e83", rgb: [251, 62, 131] },
+ ];
+
+ // Before focus, disabled.
+ Assert.ok(
+ formatHelper.colorSelector.hasAttribute("disabled"),
+ "Selector should be disabled with no focus"
+ );
+
+ formatHelper.focusMessage();
+ Assert.ok(
+ !formatHelper.colorSelector.hasAttribute("disabled"),
+ "Selector should be enabled with focus"
+ );
+
+ let firstText = "no color";
+ let secondText = "with color";
+
+ for (let color of colorSet) {
+ let value = color.value;
+ await formatHelper.assertShownColor("", `No color at start (${value})`);
+
+ await formatHelper.typeInMessage(firstText);
+ formatHelper.assertMessageParagraph(
+ [firstText],
+ `No color at start after typing (${value})`
+ );
+
+ // Select through toolbar.
+ await formatHelper.selectColor(value);
+ await formatHelper.assertShownColor(color, `Color ${value} selected`);
+
+ await formatHelper.typeInMessage(secondText);
+ await formatHelper.assertShownColor(
+ color,
+ `Color ${value} selected and typing`
+ );
+ formatHelper.assertMessageParagraph(
+ [firstText, { text: secondText, color: value }],
+ `${value} on second half`
+ );
+
+ // Test text selections.
+ for (let [start, end, forward, expect] of [
+ // Make sure we expect changes, so the test does not capture the previous
+ // state.
+ [0, null, true, ""], // At start.
+ [firstText.length + 1, null, true, color], // In the color region.
+ [0, firstText.length + secondText.length, true, null], // Mixed.
+ [firstText.length, null, true, ""], // Boundary travelling forward.
+ [firstText.length, null, false, color], // On boundary travelling backward.
+ ]) {
+ await formatHelper.selectTextRange(start, end, forward);
+ await formatHelper.assertShownColor(
+ expect,
+ `Selecting text with ${value}, from ${start} to ${end} ` +
+ `${forward ? "forwards" : "backwards"}`
+ );
+ }
+
+ // Select mixed.
+ await formatHelper.selectTextRange(3, firstText.length + 1);
+ await formatHelper.assertShownColor(null, `Mixed selection (${value})`);
+
+ // Select the same color.
+ await formatHelper.selectColor(value);
+ await formatHelper.assertShownColor(
+ color,
+ `Selected ${value} color on more`
+ );
+ formatHelper.assertMessageParagraph(
+ [
+ firstText.slice(0, 3),
+ { text: firstText.slice(3) + secondText, color: value },
+ ],
+ `${value} color on more`
+ );
+
+ // Select the default color.
+ let selector = formatHelper.selectColorInDialog(null);
+ // Select through Format menu.
+ formatHelper.selectFromFormatMenu(formatHelper.colorMenuItem);
+ await selector;
+ await formatHelper.assertShownColor("", `Unselected ${value} color`);
+ formatHelper.assertMessageParagraph(
+ [
+ firstText + secondText.slice(0, 1),
+ { text: secondText.slice(1), color: value },
+ ],
+ `Cleared some ${value} color`
+ );
+
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_font_family.js b/comm/mail/test/browser/composition/browser_font_family.js
new file mode 100644
index 0000000000..a365836d27
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_font_family.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/. */
+
+/**
+ * Test font family in messages.
+ */
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_font_family() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ // Before focus, disabled.
+ Assert.ok(
+ formatHelper.fontSelector.disabled,
+ "Selector should be disabled with no focus"
+ );
+
+ formatHelper.focusMessage();
+ Assert.ok(
+ !formatHelper.fontSelector.disabled,
+ "Selector should be enabled with focus"
+ );
+
+ let firstText = "no font";
+ let secondText = "with font";
+
+ // Only test standard fonts.
+ for (let font of formatHelper.commonFonts) {
+ await formatHelper.assertShownFont("", `Variable width at start (${font})`);
+
+ await formatHelper.typeInMessage(firstText);
+ formatHelper.assertMessageParagraph(
+ [firstText],
+ `No font family at start after typing (${font})`
+ );
+
+ // Select through toolbar.
+ await formatHelper.selectFont(font);
+ await formatHelper.assertShownFont(font, `Changed to "${font}"`);
+
+ await formatHelper.typeInMessage(secondText);
+ await formatHelper.assertShownFont(font, `Still "${font}" when typing`);
+ formatHelper.assertMessageParagraph(
+ [firstText, { text: secondText, font }],
+ `"${font}" on second half`
+ );
+
+ // Test text selections.
+ for (let [start, end, forward, expect] of [
+ // Make sure we expect changes, so the test does not capture the previous
+ // state.
+ [0, null, true, ""], // At start.
+ [firstText.length + 1, null, true, font], // In the font region.
+ [0, firstText.length + secondText.length, true, null], // Mixed.
+ [firstText.length, null, true, ""], // On boundary travelling forward.
+ [firstText.length, null, false, font], // On boundary travelling backward.
+ ]) {
+ await formatHelper.selectTextRange(start, end, forward);
+ await formatHelper.assertShownFont(
+ expect,
+ `Selecting text with "${font}", from ${start} to ${end} ` +
+ `${forward ? "forwards" : "backwards"}`
+ );
+ }
+
+ // Select mixed.
+ await formatHelper.selectTextRange(3, firstText.length + 1);
+ await formatHelper.assertShownFont(null, `Mixed selection (${font})`);
+ // Select through menu.
+ let item = formatHelper.getFontMenuItem(font);
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.fontMenu);
+ // See Bug 1718225
+ // await formatHelper.assertShownFont(font, `"${font}" on more`);
+ formatHelper.assertMessageParagraph(
+ [firstText.slice(0, 3), { text: firstText.slice(3) + secondText, font }],
+ `"${font}" on more`
+ );
+
+ await formatHelper.selectFont("");
+ await formatHelper.assertShownFont("", `Cleared some "${font}"`);
+ formatHelper.assertMessageParagraph(
+ [firstText + secondText.slice(0, 1), { text: secondText.slice(1), font }],
+ `Cleared some "${font}"`
+ );
+
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_fixed_width() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let ttStyleItem = formatHelper.getStyleMenuItem("tt");
+
+ formatHelper.focusMessage();
+
+ // Currently, when the monospace font family is selected the UI is updated to
+ // show the tt style as selected (even though the underlying document still
+ // uses <font face="monospace"> rather than <tt>).
+
+ await formatHelper.selectFont("monospace");
+ await formatHelper.assertShownFont("monospace", "Changed to monospace");
+ // See Bug 1716840
+ // await formatHelper.assertShownStyles(
+ // "tt",
+ // "tt style shown after setting to Fixed Width",
+ // );
+ let text = "monospace text content";
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownFont(
+ "monospace",
+ "Still monospace when typing"
+ );
+ await formatHelper.assertShownStyles(
+ "tt",
+ "tt style shown after setting to Fixed Width and typing"
+ );
+ formatHelper.assertMessageParagraph(
+ [{ text, font: "monospace" }],
+ "monospace text"
+ );
+
+ // Trying to unset the font using Text Styles -> Fixed Width is ignored.
+ // NOTE: This is currently asymmetric: i.e. the Text Styles -> Fixed Width
+ // style *can* be removed by changing the Font to Variable Width.
+ await formatHelper.selectFromFormatSubMenu(
+ ttStyleItem,
+ formatHelper.styleMenu
+ );
+ await formatHelper.assertShownFont("monospace", "Still monospace");
+ await formatHelper.typeInMessage("+");
+ await formatHelper.assertShownFont("monospace", "Still monospace after key");
+ formatHelper.assertMessageParagraph(
+ [{ text: text + "+", font: "monospace" }],
+ "still produce monospace text"
+ );
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_font_size.js b/comm/mail/test/browser/composition/browser_font_size.js
new file mode 100644
index 0000000000..a6113188c7
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_font_size.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/. */
+
+/**
+ * Test font size in messages.
+ */
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_font_size() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ const NO_SIZE = formatHelper.NO_SIZE;
+ const MIN_SIZE = formatHelper.MIN_SIZE;
+ const MAX_SIZE = formatHelper.MAX_SIZE;
+
+ // Before focus, disabled.
+ Assert.ok(
+ formatHelper.sizeSelector.disabled,
+ "Selector should be disabled with no focus"
+ );
+
+ formatHelper.focusMessage();
+ Assert.ok(
+ !formatHelper.sizeSelector.disabled,
+ "Selector should be enabled with focus"
+ );
+
+ let firstText = "no size";
+ let secondText = "with size";
+
+ for (let size = MIN_SIZE; size <= MAX_SIZE; size++) {
+ if (size === NO_SIZE) {
+ continue;
+ }
+ await formatHelper.assertShownSize(NO_SIZE, `No size at start (${size})`);
+
+ await formatHelper.typeInMessage(firstText);
+ formatHelper.assertMessageParagraph(
+ [firstText],
+ `No size at start after typing (${size})`
+ );
+
+ // Select through toolbar.
+ await formatHelper.selectSize(size);
+ await formatHelper.assertShownSize(size, `Changed to size ${size}`);
+
+ await formatHelper.typeInMessage(secondText);
+ await formatHelper.assertShownSize(size, `Still size ${size} when typing`);
+ formatHelper.assertMessageParagraph(
+ [firstText, { text: secondText, size }],
+ `size ${size} on second half`
+ );
+
+ // Test text selections.
+ for (let [start, end, forward, expect] of [
+ // Make sure we expect changes, so the test does not capture the previous
+ // state.
+ [0, null, true, NO_SIZE], // At start.
+ [firstText.length + 1, null, true, size], // In the size region.
+ // See Bug 1718227
+ // [0, firstText.length + secondText.length, true, null], // Mixed.
+ [firstText.length, null, true, NO_SIZE], // On boundary travelling forward.
+ [firstText.length, null, false, size], // On boundary travelling backward.
+ ]) {
+ await formatHelper.selectTextRange(start, end, forward);
+ await formatHelper.assertShownSize(
+ expect,
+ `Selecting text with size ${size}, from ${start} to ${end} ` +
+ `${forward ? "forwards" : "backwards"}`
+ );
+ }
+
+ // Select mixed.
+ await formatHelper.selectTextRange(3, firstText.length + 1);
+ // See Bug 1718227
+ // await formatHelper.assertShownSize(null, `Mixed selection (${size})`);
+
+ // Select through Format menu.
+ let item = formatHelper.getSizeMenuItem(size);
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.sizeMenu);
+ await formatHelper.assertShownSize(size, `size ${size} on more`);
+ formatHelper.assertMessageParagraph(
+ [firstText.slice(0, 3), { text: firstText.slice(3) + secondText, size }],
+ `size ${size} on more`
+ );
+
+ await formatHelper.selectSize(NO_SIZE);
+ await formatHelper.assertShownSize(NO_SIZE, `Cleared some size ${size}`);
+ formatHelper.assertMessageParagraph(
+ [firstText + secondText.slice(0, 1), { text: secondText.slice(1), size }],
+ `Cleared some size ${size}`
+ );
+
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_font_size_increment() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ const NO_SIZE = formatHelper.NO_SIZE;
+ const MIN_SIZE = formatHelper.MIN_SIZE;
+ const MAX_SIZE = formatHelper.MAX_SIZE;
+
+ // NOTE: size=3 corresponds to no set size
+ let increaseButton = formatHelper.increaseSizeButton;
+ let decreaseButton = formatHelper.decreaseSizeButton;
+ let increaseItem = formatHelper.increaseSizeMenuItem;
+ let decreaseItem = formatHelper.decreaseSizeMenuItem;
+
+ Assert.ok(
+ increaseButton.disabled,
+ "Increase button should be disabled with no focus"
+ );
+ Assert.ok(
+ decreaseButton.disabled,
+ "Decrease button should be disabled with no focus"
+ );
+
+ formatHelper.focusMessage();
+
+ Assert.ok(
+ !increaseButton.disabled,
+ "Increase button should be enabled with focus"
+ );
+ Assert.ok(
+ !decreaseButton.disabled,
+ "Decrease button should be enabled with focus"
+ );
+
+ async function assertShownAndDisabled(formatHelper, size, message) {
+ await formatHelper.assertShownSize(size, message);
+ switch (size) {
+ case MAX_SIZE:
+ Assert.ok(
+ increaseButton.disabled,
+ `${message}: Increase button should be disabled at max size ${size}`
+ );
+ Assert.ok(
+ !decreaseButton.disabled,
+ `${message}: Decrease button should be enabled at max size ${size}`
+ );
+ await formatHelper.assertWithFormatSubMenu(
+ formatHelper.sizeMenu,
+ () => increaseItem.disabled && !decreaseItem.disabled,
+ `Only the increase menu item should be disabled at max size ${size}`
+ );
+ break;
+ case MIN_SIZE:
+ Assert.ok(
+ !increaseButton.disabled,
+ `${message}: Increase button should be enabled at min size ${size}`
+ );
+ Assert.ok(
+ decreaseButton.disabled,
+ `${message}: Decrease button should be disabled at min size ${size}`
+ );
+ await formatHelper.assertWithFormatSubMenu(
+ formatHelper.sizeMenu,
+ () => !increaseItem.disabled && decreaseItem.disabled,
+ `Only the decrease menu item should be disabled at min size ${size}`
+ );
+ break;
+ default:
+ Assert.ok(
+ !increaseButton.disabled,
+ `${message}: Increase button should be enabled at size ${size}`
+ );
+ Assert.ok(
+ !decreaseButton.disabled,
+ `${message}: Decrease button should be enabled at size ${size}`
+ );
+ await formatHelper.assertWithFormatSubMenu(
+ formatHelper.sizeMenu,
+ () => !increaseItem.disabled && !decreaseItem.disabled,
+ `No menu items should be disabled at size ${size}`
+ );
+ break;
+ }
+ }
+
+ async function assertAndType(formatHelper, size, text, content, message) {
+ await assertShownAndDisabled(
+ formatHelper,
+ size,
+ `${message}: At size ${size}`
+ );
+
+ await formatHelper.typeInMessage(text);
+ await assertShownAndDisabled(
+ formatHelper,
+ size,
+ `${message}: At size ${size} and typing`
+ );
+
+ content.push({ text, size });
+ formatHelper.assertMessageParagraph(
+ content,
+ `${message}: Added size ${size}`
+ );
+ }
+
+ let content = [];
+ let size = NO_SIZE;
+
+ let text = "start";
+ await formatHelper.typeInMessage(text);
+ content.push(text);
+ formatHelper.assertMessageParagraph(content, "Start with no font");
+
+ await assertShownAndDisabled(formatHelper, size, "At start");
+
+ for (size++; size <= MAX_SIZE; size++) {
+ increaseButton.click();
+ await assertAndType(
+ formatHelper,
+ size,
+ `step up to ${size}`,
+ content,
+ `Increase step with button`
+ );
+ }
+
+ // Reverse direction.
+ for (size = MAX_SIZE - 1; size > NO_SIZE; size--) {
+ decreaseButton.click();
+ await assertAndType(
+ formatHelper,
+ size,
+ `step down to ${size}`,
+ content,
+ `Decrease step with button`
+ );
+ }
+
+ decreaseButton.click();
+ text = "middle";
+ await formatHelper.typeInMessage(text);
+ content.push(text);
+ await assertShownAndDisabled(formatHelper, size, "At middle");
+
+ for (size--; size >= MIN_SIZE; size--) {
+ // Use menu item.
+ await formatHelper.selectFromFormatSubMenu(
+ decreaseItem,
+ formatHelper.sizeMenu
+ );
+ await assertAndType(
+ formatHelper,
+ size,
+ `step down to ${size}`,
+ content,
+ `Decrease step with menu item`
+ );
+ }
+
+ for (size = MIN_SIZE + 1; size < NO_SIZE; size++) {
+ // Use menu item.
+ await formatHelper.selectFromFormatSubMenu(
+ increaseItem,
+ formatHelper.sizeMenu
+ );
+ await assertAndType(
+ formatHelper,
+ size,
+ `step up to ${size}`,
+ content,
+ `Increase step with menu item`
+ );
+ }
+
+ await formatHelper.emptyParagraph();
+
+ // Selecting max or min sizes directly also enables or disables the
+ // increase/decrease buttons and items.
+ await formatHelper.selectSize(MAX_SIZE);
+ await assertShownAndDisabled(formatHelper, MAX_SIZE, "Direct to max size");
+ await formatHelper.selectSize(NO_SIZE);
+ await assertShownAndDisabled(formatHelper, NO_SIZE, "Direct to no size");
+ await formatHelper.selectSize(MIN_SIZE);
+ await assertShownAndDisabled(formatHelper, MIN_SIZE, "Direct to min size");
+
+ // Type at min size.
+ text = "text to select";
+ await formatHelper.typeInMessage(text);
+ formatHelper.assertMessageParagraph([{ text, size: MIN_SIZE }], "Min text");
+ // Select all.
+ await formatHelper.selectTextRange(0, text.length);
+
+ for (size = MIN_SIZE + 1; size <= MAX_SIZE; size++) {
+ increaseButton.click();
+ await assertShownAndDisabled(
+ formatHelper,
+ size,
+ `Increase selection to size ${size}`
+ );
+ if (size === NO_SIZE) {
+ formatHelper.assertMessageParagraph([text], "Increase to middle size");
+ } else {
+ formatHelper.assertMessageParagraph(
+ [{ text, size }],
+ `Increase to size ${size}`
+ );
+ }
+ }
+
+ // Reverse
+ for (size = MAX_SIZE - 1; size >= MIN_SIZE; size--) {
+ decreaseButton.click();
+ await assertShownAndDisabled(
+ formatHelper,
+ size,
+ `Decrease selection to size ${size}`
+ );
+ if (size === NO_SIZE) {
+ formatHelper.assertMessageParagraph([text], "Decrease to middle size");
+ } else {
+ formatHelper.assertMessageParagraph(
+ [{ text, size }],
+ `Decrease to size ${size}`
+ );
+ }
+ }
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardDefectiveCharset.js b/comm/mail/test/browser/composition/browser_forwardDefectiveCharset.js
new file mode 100644
index 0000000000..083a3a8161
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardDefectiveCharset.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+/**
+ * Tests that messages without properly declared charset are correctly forwarded.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_forward } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ get_about_message,
+ mc,
+ open_message_from_file,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var folder;
+
+// Some text from defective-charset.eml
+const SOME_SPANISH = "estos テコltimos meses siento";
+
+add_setup(async function () {
+ folder = await create_folder("FolderWithDefectiveCharset");
+ registerCleanupFunction(() => folder.deleteSelf(null));
+});
+
+add_task(async function test_forward_direct() {
+ let file = new FileUtils.File(getTestFilePath("data/defective-charset.eml"));
+ let msgc = await open_message_from_file(file);
+
+ let cwc = open_compose_with_forward(msgc);
+
+ let mailText =
+ cwc.window.document.getElementById("messageEditor").contentDocument.body
+ .textContent;
+
+ Assert.ok(
+ mailText.includes(SOME_SPANISH),
+ "forwarded content should be correctly encoded"
+ );
+
+ close_compose_window(cwc);
+ close_window(msgc);
+});
+
+add_task(async function test_forward_from_folder() {
+ await be_in_folder(folder);
+
+ let file = new FileUtils.File(getTestFilePath("data/defective-charset.eml"));
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+
+ // Copy the message to a folder.
+ let documentChild =
+ aboutMessage.document.getElementById("messagepane").contentDocument
+ .documentElement;
+ EventUtils.synthesizeMouseAtCenter(
+ documentChild,
+ { type: "contextmenu", button: 2 },
+ documentChild.ownerGlobal
+ );
+ await click_menus_in_sequence(
+ aboutMessage.document.getElementById("mailContext"),
+ [
+ { id: "mailContext-copyMenu" },
+ { label: "Local Folders" },
+ { label: folder.name },
+ ]
+ );
+ close_window(msgc);
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ Assert.ok(
+ get_about_message()
+ .document.getElementById("messagepane")
+ .contentDocument.body.textContent.includes(SOME_SPANISH)
+ );
+
+ let cwc = open_compose_with_forward();
+
+ let mailText =
+ cwc.window.document.getElementById("messageEditor").contentDocument.body
+ .textContent;
+
+ Assert.ok(
+ mailText.includes(SOME_SPANISH),
+ "forwarded content should be correctly encoded"
+ );
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardHeaders.js b/comm/mail/test/browser/composition/browser_forwardHeaders.js
new file mode 100644
index 0000000000..1568f134b0
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardHeaders.js
@@ -0,0 +1,175 @@
+/* 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/. */
+
+/**
+ * Tests that headers like References and X-Forwarded-Message-Id are
+ * set properly when forwarding messages.
+ */
+
+"use strict";
+
+var {
+ assert_previous_text,
+ get_compose_body,
+ open_compose_with_forward,
+ open_compose_with_forward_as_attachments,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_sets_to_folders,
+ be_in_folder,
+ create_folder,
+ create_thread,
+ get_special_folder,
+ make_display_unthreaded,
+ mc,
+ press_delete,
+ select_click_row,
+ select_shift_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var { plan_for_window_close, wait_for_window_close } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var cwc = null; // compose window controller
+var folder;
+var gDrafts;
+
+add_setup(async function () {
+ folder = await create_folder("Test");
+ let thread1 = create_thread(10);
+ await add_message_sets_to_folders([folder], [thread1]);
+
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+
+ // Don't create paragraphs in the test.
+ // The test checks for the first DOM node and expects a text and not
+ // a paragraph.
+ Services.prefs.setBoolPref("mail.compose.default_to_paragraph", false);
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("mail.compose.default_to_paragraph");
+});
+
+async function forward_selected_messages_and_go_to_drafts_folder(f) {
+ const kText = "Hey check out this megalol link";
+ // opening a new compose window
+ cwc = f(mc);
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString(kText, cwc.window);
+
+ let mailBody = get_compose_body(cwc);
+ assert_previous_text(mailBody.firstChild, [kText]);
+
+ plan_for_window_close(cwc);
+ // mwc is modal window controller
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog("accept");
+ // quit -> do you want to save ?
+ cwc.window.goDoCommand("cmd_close");
+ await dialogPromise;
+ // Actually quit the window.
+ wait_for_window_close();
+
+ // Visit the existing Drafts folder.
+ await be_in_folder(gDrafts);
+ make_display_unthreaded();
+}
+
+add_task(async function test_forward_inline() {
+ await be_in_folder(folder);
+ make_display_unthreaded();
+ // original message header
+ let oMsgHdr = select_click_row(0);
+
+ await forward_selected_messages_and_go_to_drafts_folder(
+ open_compose_with_forward
+ );
+
+ // forwarded message header
+ let fMsgHdr = select_click_row(0);
+
+ Assert.ok(
+ fMsgHdr.numReferences > 0,
+ "No References Header in forwarded msg."
+ );
+ Assert.equal(
+ fMsgHdr.getStringReference(0),
+ oMsgHdr.messageId,
+ "The forwarded message should have References: = Message-Id: of the original msg"
+ );
+
+ // test for x-forwarded-message id and exercise the js mime representation as
+ // well
+ return new Promise(resolve => {
+ MsgHdrToMimeMessage(fMsgHdr, null, function (aMsgHdr, aMimeMsg) {
+ Assert.equal(
+ aMimeMsg.headers["x-forwarded-message-id"],
+ "<" + oMsgHdr.messageId + ">"
+ );
+ Assert.equal(aMimeMsg.headers.references, "<" + oMsgHdr.messageId + ">");
+
+ press_delete(mc);
+ resolve();
+ });
+ });
+});
+
+add_task(async function test_forward_as_attachments() {
+ await be_in_folder(folder);
+ make_display_unthreaded();
+
+ // original message header
+ let oMsgHdr0 = select_click_row(0);
+ let oMsgHdr1 = select_click_row(1);
+ select_shift_click_row(0);
+
+ await forward_selected_messages_and_go_to_drafts_folder(
+ open_compose_with_forward_as_attachments
+ );
+
+ // forwarded message header
+ let fMsgHdr = select_click_row(0);
+
+ Assert.ok(
+ fMsgHdr.numReferences > 0,
+ "No References Header in forwarded msg."
+ );
+ Assert.ok(
+ fMsgHdr.numReferences > 1,
+ "Only one References Header in forwarded msg."
+ );
+ Assert.equal(
+ fMsgHdr.getStringReference(1),
+ oMsgHdr1.messageId,
+ "The forwarded message should have References: = Message-Id: of the original msg#1"
+ );
+ Assert.equal(
+ fMsgHdr.getStringReference(0),
+ oMsgHdr0.messageId,
+ "The forwarded message should have References: = Message-Id: of the original msg#0"
+ );
+
+ // test for x-forwarded-message id and exercise the js mime representation as
+ // well
+ return new Promise(resolve => {
+ MsgHdrToMimeMessage(fMsgHdr, null, function (aMsgHdr, aMimeMsg) {
+ Assert.equal(
+ aMimeMsg.headers["x-forwarded-message-id"],
+ "<" + oMsgHdr0.messageId + "> <" + oMsgHdr1.messageId + ">"
+ );
+ Assert.equal(
+ aMimeMsg.headers.references,
+ "<" + oMsgHdr0.messageId + "> <" + oMsgHdr1.messageId + ">"
+ );
+
+ press_delete(mc);
+ resolve();
+ });
+ });
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardRFC822Attach.js b/comm/mail/test/browser/composition/browser_forwardRFC822Attach.js
new file mode 100644
index 0000000000..2bcd136aa2
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardRFC822Attach.js
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * Tests that attached messages (message/rfc822) are correctly sent.
+ * It's easiest to test the forward case.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ get_msg_source,
+ open_compose_with_forward_as_attachments,
+ save_compose_message,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ mc,
+ open_message_from_file,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDrafts;
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+async function forwardDirect(aFilePath, aExpectedText) {
+ let file = new FileUtils.File(getTestFilePath(`data/${aFilePath}`));
+ let msgc = await open_message_from_file(file);
+
+ let cwc = open_compose_with_forward_as_attachments(msgc);
+
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+ close_window(msgc);
+
+ await be_in_folder(gDrafts);
+ let draftMsg = select_click_row(0);
+
+ let draftMsgContent = await get_msg_source(draftMsg);
+
+ Assert.ok(
+ draftMsgContent.includes(aExpectedText),
+ "Failed to find expected text"
+ );
+
+ press_delete(mc); // clean up the created draft
+}
+
+add_task(async function test_forwarding_long_html_line_as_attachment() {
+ await forwardDirect("./long-html-line.eml", "We like writing long lines.");
+});
+
+add_task(async function test_forwarding_feed_message_as_attachment() {
+ await forwardDirect("./feed-message.eml", "We like using linefeeds only.");
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardUTF8.js b/comm/mail/test/browser/composition/browser_forwardUTF8.js
new file mode 100644
index 0000000000..494e722bdb
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardUTF8.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+/**
+ * Tests that UTF-8 messages are correctly forwarded.
+ */
+
+"use strict";
+
+var { close_compose_window, get_compose_body, open_compose_with_forward } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ get_about_message,
+ mc,
+ open_message_from_file,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var folderToSendFrom;
+
+add_setup(async function () {
+ requestLongerTimeout(2);
+ folderToSendFrom = await create_folder("FolderWithUTF8");
+});
+
+function check_content(window) {
+ let mailBody = get_compose_body(window);
+
+ let node = mailBody.firstChild;
+ while (node) {
+ if (node.classList.contains("moz-forward-container")) {
+ // We found the forward container. Let's look for our text.
+ node = node.firstChild;
+ while (node) {
+ // We won't find the exact text in the DOM but we'll find our string.
+ if (node.nodeName == "#text" && node.nodeValue.includes("テ。テウテコテ、テカテシテ")) {
+ return;
+ }
+ node = node.nextSibling;
+ }
+ // Text not found in the forward container.
+ Assert.ok(false, "Failed to find forwarded text");
+ return;
+ }
+ node = node.nextSibling;
+ }
+
+ Assert.ok(false, "Failed to find forward container");
+}
+
+async function forwardDirect(aFilePath) {
+ let file = new FileUtils.File(getTestFilePath(`data/${aFilePath}`));
+ let msgc = await open_message_from_file(file);
+
+ let cwc = open_compose_with_forward(msgc);
+
+ check_content(cwc);
+
+ close_compose_window(cwc);
+ close_window(msgc);
+}
+
+async function forwardViaFolder(aFilePath) {
+ await be_in_folder(folderToSendFrom);
+
+ let file = new FileUtils.File(getTestFilePath(`data/${aFilePath}`));
+ let msgc = await open_message_from_file(file);
+ let aboutMessage = get_about_message(msgc.window);
+
+ // Copy the message to a folder.
+ let documentChild =
+ aboutMessage.document.getElementById("messagepane").contentDocument
+ .documentElement;
+ EventUtils.synthesizeMouseAtCenter(
+ documentChild,
+ { type: "contextmenu", button: 2 },
+ documentChild.ownerGlobal
+ );
+ await click_menus_in_sequence(
+ aboutMessage.document.getElementById("mailContext"),
+ [
+ { id: "mailContext-copyMenu" },
+ { label: "Local Folders" },
+ { label: "FolderWithUTF8" },
+ ]
+ );
+ close_window(msgc);
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ Assert.ok(
+ get_about_message()
+ .document.getElementById("messagepane")
+ .contentDocument.body.textContent.includes("テ。テウテコテ、テカテシテ")
+ );
+
+ let fwdWin = open_compose_with_forward();
+
+ check_content(fwdWin);
+
+ close_compose_window(fwdWin);
+
+ press_delete(mc);
+}
+
+add_task(async function test_utf8_forwarding_from_opened_file() {
+ await forwardDirect("./content-utf8-rel-only.eml");
+ await forwardDirect("./content-utf8-rel-alt.eml");
+ await forwardDirect("./content-utf8-alt-rel.eml");
+
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+});
+
+add_task(async function test_utf8_forwarding_from_via_folder() {
+ await forwardViaFolder("./content-utf8-rel-only.eml");
+ await forwardViaFolder("./content-utf8-rel-alt.eml"); // Also tests HTML part without <html> tag.
+ await forwardViaFolder("./content-utf8-alt-rel.eml"); // Also tests <html attr>.
+ await forwardViaFolder("./content-utf8-alt-rel2.eml"); // Also tests content before <html>.
+
+ // Repeat the last three in simple HTML view.
+ Services.prefs.setIntPref("mailnews.display.html_as", 3);
+ await forwardViaFolder("./content-utf8-rel-alt.eml"); // Also tests HTML part without <html> tag.
+ await forwardViaFolder("./content-utf8-alt-rel.eml"); // Also tests <html attr>.
+ await forwardViaFolder("./content-utf8-alt-rel2.eml"); // Also tests content before <html>.
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("mailnews.display.html_as");
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardedContent.js b/comm/mail/test/browser/composition/browser_forwardedContent.js
new file mode 100644
index 0000000000..7f8b25130d
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardedContent.js
@@ -0,0 +1,70 @@
+/* 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/. */
+
+/**
+ * Tests that forwarded content is ok.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_forward } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ create_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var folder = null;
+
+add_setup(async function () {
+ folder = await create_folder("Forward Content Testing");
+ await add_message_to_folder(
+ [folder],
+ create_message({
+ subject: "something like <foo@example>",
+ body: { body: "Testing bug 397021!" },
+ })
+ );
+});
+
+/**
+ * Test that the subject is set properly in the forwarded message content
+ * when you hit forward.
+ */
+add_task(async function test_forwarded_subj() {
+ await be_in_folder(folder);
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let fwdWin = open_compose_with_forward();
+
+ let headerTableText = fwdWin.window.document
+ .getElementById("messageEditor")
+ .contentDocument.querySelector("table").textContent;
+ if (!headerTableText.includes(msg.mime2DecodedSubject)) {
+ throw new Error(
+ "Subject not set correctly in header table: subject=" +
+ msg.mime2DecodedSubject +
+ ", header table text=" +
+ headerTableText
+ );
+ }
+ close_compose_window(fwdWin);
+
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+});
diff --git a/comm/mail/test/browser/composition/browser_forwardedEmlActions.js b/comm/mail/test/browser/composition/browser_forwardedEmlActions.js
new file mode 100644
index 0000000000..609f6c1107
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_forwardedEmlActions.js
@@ -0,0 +1,171 @@
+/* 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/. */
+
+/**
+ * Tests that actions such as replying and forwarding works correctly from
+ * an .eml message that's attached to another mail.
+ */
+
+"use strict";
+
+var { async_wait_for_compose_window, close_compose_window, get_compose_body } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ close_tab,
+ create_folder,
+ get_about_message,
+ mc,
+ select_click_row,
+ wait_for_message_display_completion,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { async_plan_for_new_window, close_window, wait_for_new_window } =
+ ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var folder;
+
+var msgsubject = "mail client suggestions";
+var msgbodyA = "know of a good email client?";
+var msgbodyB = "hi, i think you may know of an email client to recommend?";
+
+add_setup(async function () {
+ folder = await create_folder("FwdedEmlTest");
+
+ let source =
+ "From - Mon Apr 16 22:55:33 2012\n" +
+ "Date: Mon, 16 Apr 2012 22:55:33 +0300\n" +
+ "From: Mr Example <example@invalid>\n" +
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:14.0) Gecko/20120331 Thunderbird/14.0a1\n" +
+ "MIME-Version: 1.0\n" +
+ "To: example@invalid\n" +
+ "Subject: Fwd: " +
+ msgsubject +
+ "\n" +
+ "References: <4F8C78F5.4000704@invalid>\n" +
+ "In-Reply-To: <4F8C78F5.4000704@invalid>\n" +
+ "X-Forwarded-Message-Id: <4F8C78F5.4000704@invalid>\n" +
+ "Content-Type: multipart/mixed;\n" +
+ ' boundary="------------080806020206040800000503"\n' +
+ "\n" +
+ "This is a multi-part message in MIME format.\n" +
+ "--------------080806020206040800000503\n" +
+ "Content-Type: text/plain; charset=ISO-8859-1; format=flowed\n" +
+ "Content-Transfer-Encoding: 7bit\n" +
+ "\n" +
+ msgbodyB +
+ "\n" +
+ "\n" +
+ "--------------080806020206040800000503\n" +
+ "Content-Type: message/rfc822;\n" +
+ ' name="mail client suggestions.eml"\n' +
+ "Content-Transfer-Encoding: 7bit\n" +
+ "Content-Disposition: attachment;\n" +
+ ' filename="mail client suggestions.eml"\n' +
+ "\n" +
+ "Return-Path: <example@invalid>\n" +
+ "Received: from xxx (smtpu [10.0.0.51])\n" +
+ " by storage (Cyrus v2.3.7-Invoca-RPM-2.3.7-1.1) with LMTPA;\n" +
+ " Mon, 16 Apr 2012 22:54:36 +0300\n" +
+ "Message-ID: <4F8C78F5.4000704@invalid>\n" +
+ "Date: Mon, 16 Apr 2012 22:54:29 +0300\n" +
+ "From: Mr Example <example@invalid>\n" +
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:14.0) Gecko/20120331 Thunderbird/14.0a1\n" +
+ "MIME-Version: 1.0\n" +
+ "To: example@invalid\n" +
+ "Subject: mail client suggestions\n" +
+ "Content-Type: text/plain; charset=ISO-8859-1; format=flowed\n" +
+ "Content-Transfer-Encoding: 7bit\n" +
+ "\n" +
+ msgbodyA +
+ "\n" +
+ "\n" +
+ "--------------080806020206040800000503--\n";
+
+ folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ folder.addMessage(source);
+});
+
+/**
+ * Helper to open an attached .eml file, invoke the hotkey and check some
+ * properties of the composition content we get.
+ */
+async function setupWindowAndTest(hotkeyToHit, hotkeyModifiers) {
+ await be_in_folder(folder);
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let tabSelectPromise = BrowserTestUtils.waitForEvent(
+ mc.window.document.getElementById("tabmail").tabContainer,
+ "select"
+ );
+ let aboutMessage = get_about_message();
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentName"),
+ { clickCount: 1 },
+ aboutMessage
+ );
+ await tabSelectPromise;
+ wait_for_message_display_completion(mc, false);
+
+ let newWindowPromise = async_plan_for_new_window("msgcompose");
+ EventUtils.synthesizeKey(hotkeyToHit, hotkeyModifiers, window);
+ let compWin = await async_wait_for_compose_window(window, newWindowPromise);
+
+ let bodyText = get_compose_body(compWin).textContent;
+ if (bodyText.includes("html")) {
+ throw new Error("body text contains raw html; bodyText=" + bodyText);
+ }
+
+ if (!bodyText.includes(msgbodyA)) {
+ throw new Error(
+ "body text didn't contain the body text; msgbodyA=" +
+ msgbodyB +
+ ", bodyText=" +
+ bodyText
+ );
+ }
+
+ let subjectText = compWin.window.document.getElementById("msgSubject").value;
+ if (!subjectText.includes(msgsubject)) {
+ throw new Error(
+ "subject text didn't contain the original subject; " +
+ "msgsubject=" +
+ msgsubject +
+ ", subjectText=" +
+ subjectText
+ );
+ }
+
+ close_compose_window(compWin, false);
+ close_tab(mc.window.document.getElementById("tabmail").currentTabInfo);
+}
+
+/**
+ * Test that replying to an attached .eml contains the expected texts.
+ */
+add_task(function test_reply_to_attached_eml() {
+ return setupWindowAndTest("R", { shiftKey: false, accelKey: true });
+});
+
+/**
+ * Test that forwarding an attached .eml contains the expected texts.
+ */
+add_task(async function test_forward_attached_eml() {
+ await setupWindowAndTest("L", { shiftKey: false, accelKey: true });
+
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+});
diff --git a/comm/mail/test/browser/composition/browser_imageDisplay.js b/comm/mail/test/browser/composition/browser_imageDisplay.js
new file mode 100644
index 0000000000..8d759832a9
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_imageDisplay.js
@@ -0,0 +1,172 @@
+/* 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/. */
+
+/**
+ * Tests that we load and display embedded images in messages.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var {
+ close_compose_window,
+ open_compose_with_forward,
+ open_compose_with_reply,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ get_about_message,
+ mc,
+ open_message_from_file,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var gImageFolder;
+
+add_setup(async function () {
+ gImageFolder = await create_folder("ImageFolder");
+ registerCleanupFunction(() => {
+ gImageFolder.deleteSelf(null);
+ });
+});
+
+/**
+ * Check dimensions of the embedded image and whether it could be loaded.
+ */
+async function check_image_size(aController, aImage, aSrcStart) {
+ Assert.notEqual(null, aImage, "should have a image");
+ await TestUtils.waitForCondition(
+ () => aImage.complete,
+ "waiting for image.complete"
+ );
+ // There should not be a cid: URL now.
+ Assert.ok(!aImage.src.startsWith("cid:"));
+ if (aSrcStart) {
+ Assert.ok(aImage.src.startsWith(aSrcStart));
+ }
+
+ // Check if there are height and width attributes forcing the image to a size.
+ let id = aImage.id;
+ Assert.ok(
+ aImage.hasAttribute("height"),
+ "Image " + id + " is missing a required attribute"
+ );
+ Assert.ok(
+ aImage.hasAttribute("width"),
+ "Image " + id + " is missing a required attribute"
+ );
+
+ Assert.ok(
+ aImage.height > 0,
+ "Image " + id + " is missing a required attribute"
+ );
+ Assert.ok(
+ aImage.width > 0,
+ "Image " + id + " is missing a required attribute"
+ );
+
+ // If the image couldn't be loaded, the naturalWidth and Height are zero.
+ Assert.ok(
+ aImage.naturalHeight > 0,
+ "Loaded image " + id + " is of zero size"
+ );
+ Assert.ok(aImage.naturalWidth > 0, "Loaded image " + id + " is of zero size");
+}
+
+/**
+ * Bug 1352701 and bug 1360443
+ * Test that showing an image with cid: URL in a HTML message from file will work.
+ */
+add_task(async function test_cid_image_load() {
+ let file = new FileUtils.File(
+ getTestFilePath("data/content-utf8-rel-only.eml")
+ );
+
+ // Make sure there is a cid: referenced image in the message.
+ let msgSource = await IOUtils.readUTF8(file.path);
+ Assert.ok(msgSource.includes('<img src="cid:'));
+
+ // Our image should be in the loaded eml document.
+ let msgc = await open_message_from_file(file);
+ let messageDoc = msgc.window.content.document;
+ let image = messageDoc.getElementById("cidImage");
+ await check_image_size(msgc, image, "mailbox://");
+ image = messageDoc.getElementById("cidImageOrigin");
+ check_image_size(msgc, image, "mailbox://");
+
+ // Copy the message to a folder.
+ let documentChild = messageDoc.firstElementChild;
+ EventUtils.synthesizeMouseAtCenter(
+ documentChild,
+ { type: "contextmenu", button: 2 },
+ documentChild.ownerGlobal
+ );
+ let aboutMessage = get_about_message(msgc.window);
+ await click_menus_in_sequence(
+ aboutMessage.document.getElementById("mailContext"),
+ [
+ { id: "mailContext-copyMenu" },
+ { label: "Local Folders" },
+ { label: gImageFolder.prettyName },
+ ]
+ );
+ close_window(msgc);
+});
+
+/**
+ * Bug 1352701 and bug 1360443
+ * Test that showing an image with cid: URL in a HTML message in a folder with work.
+ */
+add_task(async function test_cid_image_view() {
+ // Preview the message in the folder.
+ await be_in_folder(gImageFolder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ // Check image in the preview.
+ let messageDoc =
+ get_about_message().document.getElementById("messagepane").contentDocument;
+ let image = messageDoc.getElementById("cidImage");
+ await check_image_size(mc, image, gImageFolder.server.localStoreType + "://");
+ image = messageDoc.getElementById("cidImageOrigin");
+ await check_image_size(mc, image, gImageFolder.server.localStoreType + "://");
+});
+
+/**
+ * Bug 1352701 and bug 1360443
+ * Test that showing an image with cid: URL in a HTML message will work
+ * in a composition.
+ */
+async function check_cid_image_compose(cwc) {
+ // Our image should also be in composition when the message is forwarded/replied.
+ let image = cwc.window.document
+ .getElementById("messageEditor")
+ .contentDocument.getElementById("cidImage");
+ await check_image_size(cwc, image, "data:");
+ image = cwc.window.document
+ .getElementById("messageEditor")
+ .contentDocument.getElementById("cidImageOrigin");
+ await check_image_size(cwc, image, "data:");
+}
+
+add_task(async function test_cid_image_compose_fwd() {
+ // Our image should also be in composition when the message is forwarded.
+ let cwc = open_compose_with_forward();
+ await check_cid_image_compose(cwc);
+ close_compose_window(cwc);
+});
+
+add_task(async function test_cid_image_compose_re() {
+ // Our image should also be in composition when the message is replied.
+ let cwc = open_compose_with_reply();
+ await check_cid_image_compose(cwc);
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_imageInsertionDialog.js b/comm/mail/test/browser/composition/browser_imageInsertionDialog.js
new file mode 100644
index 0000000000..83a6e3d52f
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_imageInsertionDialog.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+/**
+ * Tests the image insertion dialog functionality.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_new_mail } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var { input_value } = ChromeUtils.import(
+ "resource://testing-common/mozmill/KeyboardHelpers.jsm"
+);
+
+var {
+ click_menus_in_sequence,
+ plan_for_modal_dialog,
+ wait_for_window_close,
+ wait_for_modal_dialog,
+} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+add_task(async function test_image_insertion_dialog_persist() {
+ let cwc = open_compose_new_mail();
+
+ // First focus on the editor element
+ cwc.window.document.getElementById("messageEditor").focus();
+
+ // Now open the image window
+ plan_for_modal_dialog("Mail:image", async function insert_image(mwc) {
+ // Insert the url of the image.
+ let srcloc = mwc.window.document.getElementById("srcInput");
+ srcloc.focus();
+
+ let file = new FileUtils.File(getTestFilePath("data/tb-logo.png"));
+ input_value(mwc, Services.io.newFileURI(file).spec);
+
+ // Don't add alternate text
+ let noAlt = mwc.window.document.getElementById("noAltTextRadio");
+ EventUtils.synthesizeMouseAtCenter(noAlt, {}, noAlt.ownerGlobal);
+ await new Promise(resolve => setTimeout(resolve));
+ mwc.window.document.documentElement.querySelector("dialog").acceptDialog();
+ });
+
+ let insertMenu = cwc.window.document.getElementById("InsertPopupButton");
+ let insertMenuPopup = cwc.window.document.getElementById("InsertPopup");
+
+ EventUtils.synthesizeMouseAtCenter(insertMenu, {}, insertMenu.ownerGlobal);
+ await click_menus_in_sequence(insertMenuPopup, [{ id: "InsertImageItem" }]);
+
+ wait_for_modal_dialog();
+ wait_for_window_close();
+ await new Promise(resolve => setTimeout(resolve));
+
+ let img = cwc.window.document
+ .getElementById("messageEditor")
+ .contentDocument.querySelector("img");
+ Assert.ok(!!img, "editor should contain an image");
+
+ info("Will check that radio option persists");
+
+ // Check that the radio option persists
+ plan_for_modal_dialog("Mail:image", async function insert_image(mwc) {
+ Assert.ok(
+ mwc.window.document.getElementById("noAltTextRadio").selected,
+ "We should persist the previously selected value"
+ );
+ // We change to "use alt text"
+ let altTextRadio = mwc.window.document.getElementById("altTextRadio");
+ EventUtils.synthesizeMouseAtCenter(
+ altTextRadio,
+ {},
+ altTextRadio.ownerGlobal
+ );
+ await new Promise(resolve => setTimeout(resolve));
+ mwc.window.document.documentElement.querySelector("dialog").cancelDialog();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(insertMenu, {}, insertMenu.ownerGlobal);
+ await click_menus_in_sequence(insertMenuPopup, [{ id: "InsertImageItem" }]);
+ wait_for_modal_dialog();
+ wait_for_window_close();
+ await new Promise(resolve => setTimeout(resolve));
+
+ info("Will check that radio option really persists");
+
+ // Check that the radio option still persists (be really sure)
+ plan_for_modal_dialog("Mail:image", function insert_image(mwc) {
+ Assert.ok(
+ mwc.window.document.getElementById("altTextRadio").selected,
+ "We should persist the previously selected value"
+ );
+ // Accept the dialog
+ mwc.window.document.documentElement.querySelector("dialog").cancelDialog();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(insertMenu, {}, insertMenu.ownerGlobal);
+ await click_menus_in_sequence(insertMenuPopup, [{ id: "InsertImageItem" }]);
+ wait_for_modal_dialog();
+ wait_for_window_close();
+
+ info("Will check we switch to 'no alt text'");
+
+ // Get the inserted image, double-click it, make sure we switch to "no alt
+ // text", despite the persisted value being "use alt text"
+ plan_for_modal_dialog("Mail:image", function insert_image(mwc) {
+ Assert.ok(
+ mwc.window.document.getElementById("noAltTextRadio").selected,
+ "We shouldn't use the persisted value because the insert image has no alt text"
+ );
+ mwc.window.document.documentElement.querySelector("dialog").cancelDialog();
+ });
+ EventUtils.synthesizeMouseAtCenter(img, { clickCount: 2 }, img.ownerGlobal);
+ wait_for_modal_dialog();
+ wait_for_window_close();
+
+ info("Will check using alt text");
+
+ // Now use some alt text for the edit image dialog
+ plan_for_modal_dialog("Mail:image", async function insert_image(mwc) {
+ Assert.ok(
+ mwc.window.document.getElementById("noAltTextRadio").selected,
+ "That value should persist still..."
+ );
+ let altTextRadio = mwc.window.document.getElementById("altTextRadio");
+ EventUtils.synthesizeMouseAtCenter(
+ altTextRadio,
+ {},
+ altTextRadio.ownerGlobal
+ );
+
+ let srcloc = mwc.window.document.getElementById("altTextInput");
+ srcloc.focus();
+ input_value(mwc, "some alt text");
+ await new Promise(resolve => setTimeout(resolve));
+ // Accept the dialog
+ mwc.window.document.documentElement.querySelector("dialog").acceptDialog();
+ });
+ EventUtils.synthesizeMouseAtCenter(img, { clickCount: 2 }, img.ownerGlobal);
+ wait_for_modal_dialog();
+ wait_for_window_close();
+
+ info("Will check next time we edit, we still have 'use alt text' selected");
+
+ // Make sure next time we edit it, we still have "use alt text" selected.
+ img = cwc.window.document
+ .getElementById("messageEditor")
+ .contentDocument.querySelector("img");
+ plan_for_modal_dialog("Mail:image", function insert_image(mwc) {
+ Assert.ok(
+ mwc.window.document.getElementById("altTextRadio").selected,
+ "We edited the image to make it have alt text, we should keep it selected"
+ );
+ // Accept the dialog
+ mwc.window.document.documentElement.querySelector("dialog").cancelDialog();
+ });
+ EventUtils.synthesizeMouseAtCenter(img, { clickCount: 2 }, img.ownerGlobal);
+ wait_for_modal_dialog();
+ wait_for_window_close();
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_inlineImage.js b/comm/mail/test/browser/composition/browser_inlineImage.js
new file mode 100644
index 0000000000..5b26b05ac5
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_inlineImage.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+/**
+ * Test sending message with inline image.
+ */
+
+var { get_msg_source, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { plan_for_window_close, wait_for_window_close } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gOutboxFolder;
+
+var kBoxId = "compose-notification-bottom";
+var kNotificationId = "blockedContent";
+
+function typedArrayToString(buffer) {
+ var string = "";
+ for (let i = 0; i < buffer.length; i += 100) {
+ string += String.fromCharCode.apply(undefined, buffer.subarray(i, i + 100));
+ }
+ return string;
+}
+
+function putHTMLOnClipboard(html) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+
+ // Register supported data flavors
+ trans.init(null);
+ trans.addDataFlavor("text/html");
+
+ let wapper = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ wapper.data = html;
+ trans.setTransferData("text/html", wapper);
+
+ Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+}
+
+add_setup(async function () {
+ gOutboxFolder = await get_special_folder(Ci.nsMsgFolderFlags.Queue);
+});
+
+/**
+ * Tests that sending message with inline image works, and we pick a file name
+ * for data uri if needed.
+ */
+add_task(async function test_send_inline_image() {
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "someone@example.com",
+ "Test sending inline image",
+ "The image doesn't display because we changed the data URI\n"
+ );
+
+ let fileBuf = await IOUtils.read(getTestFilePath("data/nest.png"));
+ let fileContent = btoa(typedArrayToString(fileBuf));
+ let dataURI = `data:image/png;base64,${fileContent}`;
+
+ putHTMLOnClipboard(`<img id="inline-img" src="${dataURI}">`);
+ cwc.window.document.getElementById("messageEditor").focus();
+ // Ctrl+V = Paste
+ EventUtils.synthesizeKey(
+ "v",
+ { shiftKey: false, accelKey: true },
+ cwc.window
+ );
+
+ plan_for_window_close(cwc);
+ cwc.window.goDoCommand("cmd_sendLater");
+ wait_for_window_close();
+
+ await be_in_folder(gOutboxFolder);
+ let msgLoaded = BrowserTestUtils.waitForEvent(
+ get_about_message(),
+ "MsgLoaded"
+ );
+ let outMsg = select_click_row(0);
+ await msgLoaded;
+ let outMsgContent = await get_msg_source(outMsg);
+
+ ok(
+ outMsgContent.includes('id="inline-img" src="cid:'),
+ "inline-img should be cid after send"
+ );
+ ok(
+ /Content-Type: image\/png;\s* name="\w{16}.png"/.test(outMsgContent),
+ `file name should have 16 characters: ${outMsgContent}`
+ );
+
+ press_delete(); // Delete the msg from Outbox.
+});
diff --git a/comm/mail/test/browser/composition/browser_linkPreviews.js b/comm/mail/test/browser/composition/browser_linkPreviews.js
new file mode 100644
index 0000000000..5316d8904c
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_linkPreviews.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+/**
+ * Test link previews.
+ */
+
+var { close_compose_window, open_compose_new_mail } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+
+var url =
+ "http://mochi.test:8888/browser/comm/mail/test/browser/composition/html/linkpreview.html";
+
+add_task(async function previewEnabled() {
+ Services.prefs.setBoolPref("mail.compose.add_link_preview", true);
+ let controller = open_compose_new_mail();
+ await navigator.clipboard.writeText(url);
+
+ let messageEditor =
+ controller.window.document.getElementById("messageEditor");
+ messageEditor.focus();
+
+ // Ctrl+V = Paste
+ EventUtils.synthesizeKey(
+ "v",
+ { shiftKey: false, accelKey: true },
+ controller.window
+ );
+
+ await TestUtils.waitForCondition(
+ () => messageEditor.contentDocument.body.querySelector(".moz-card"),
+ "link preview should have appeared"
+ );
+
+ close_compose_window(controller);
+ Services.prefs.clearUserPref("mail.compose.add_link_preview");
+});
diff --git a/comm/mail/test/browser/composition/browser_messageBody.js b/comm/mail/test/browser/composition/browser_messageBody.js
new file mode 100644
index 0000000000..c9b16cc370
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_messageBody.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+/**
+ * Tests related to message body.
+ */
+
+var { get_msg_source, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { plan_for_window_close, wait_for_window_close } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var gOutboxFolder;
+
+add_setup(async function () {
+ gOutboxFolder = await get_special_folder(Ci.nsMsgFolderFlags.Queue);
+});
+
+/**
+ * Tests that sending link with invalid data uri works.
+ */
+add_task(async function test_invalid_data_uri() {
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "someone@example.com",
+ "Test sending link with invalid data uri",
+ ""
+ );
+
+ cwc.window
+ .GetCurrentEditor()
+ .insertHTML("<a href=data:1>invalid data uri</a>");
+ plan_for_window_close(cwc);
+ cwc.window.goDoCommand("cmd_sendLater");
+ wait_for_window_close();
+
+ await be_in_folder(gOutboxFolder);
+ let msgLoaded = BrowserTestUtils.waitForEvent(
+ get_about_message(),
+ "MsgLoaded"
+ );
+ let outMsg = select_click_row(0);
+ await msgLoaded;
+ let outMsgContent = await get_msg_source(outMsg);
+
+ ok(
+ outMsgContent.includes("invalid data uri"),
+ "message containing invalid data uri should be sent"
+ );
+
+ press_delete(); // Delete the msg from Outbox.
+});
+
+/**
+ * Tests that when converting <a href="$1">$2</a> to text/plain, if $1 matches
+ * with $2, $1 should be discarded to prevent duplicated links.
+ */
+add_task(async function test_freeTextLink() {
+ let prevSendFormat = Services.prefs.getIntPref("mail.default_send_format");
+ Services.prefs.setIntPref(
+ "mail.default_send_format",
+ Ci.nsIMsgCompSendFormat.PlainText
+ );
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(cwc, "someone@example.com", "Test free text link", "");
+
+ let link1 = "https://example.com";
+ let link2 = "name@example.com";
+ let link3 = "https://example.net";
+ cwc.window
+ .GetCurrentEditor()
+ .insertHTML(
+ `<a href="${link1}/">${link1}</a> <a href="mailto:${link2}">${link2}</a> <a href="${link3}">link3</a>`
+ );
+ plan_for_window_close(cwc);
+ cwc.window.goDoCommand("cmd_sendLater");
+ wait_for_window_close();
+
+ await be_in_folder(gOutboxFolder);
+ let msgLoaded = BrowserTestUtils.waitForEvent(
+ get_about_message(),
+ "MsgLoaded"
+ );
+ let outMsg = select_click_row(0);
+ await msgLoaded;
+ let outMsgContent = await get_msg_source(outMsg);
+
+ Assert.equal(
+ getMessageBody(outMsgContent),
+ `${link1} ${link2} link3 <${link3}>\r\n`,
+ "Links should be correctly converted to plain text"
+ );
+
+ press_delete(); // Delete the msg from Outbox.
+
+ Services.prefs.setIntPref("mail.default_send_format", prevSendFormat);
+});
diff --git a/comm/mail/test/browser/composition/browser_multipartRelated.js b/comm/mail/test/browser/composition/browser_multipartRelated.js
new file mode 100644
index 0000000000..0f35194193
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_multipartRelated.js
@@ -0,0 +1,143 @@
+/* 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/. */
+
+/**
+ * Tests that multipart/related messages are handled properly.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+
+var { close_compose_window, open_compose_new_mail, save_compose_message } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder, get_special_folder, mc, press_delete, select_click_row } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+ );
+var {
+ click_menus_in_sequence,
+ plan_for_modal_dialog,
+ wait_for_modal_dialog,
+ wait_for_window_close,
+} = ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+var gDrafts;
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Helper to get the full message content.
+ *
+ * @param aMsgHdr: nsIMsgDBHdr object whose text body will be read
+ * @returns {Map(partnum -> message headers), Map(partnum -> message text)}
+ */
+function getMsgHeaders(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ let handler = {
+ _done: false,
+ _data: new Map(),
+ _text: new Map(),
+ endMessage() {
+ this._done = true;
+ },
+ deliverPartData(num, text) {
+ this._text.set(num, this._text.get(num) + text);
+ },
+ startPart(num, headers) {
+ this._data.set(num, headers);
+ this._text.set(num, "");
+ },
+ };
+ let streamListener = MimeParser.makeStreamListenerParser(handler, {
+ strformat: "unicode",
+ });
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ utils.waitFor(() => handler._done);
+ return { headers: handler._data, text: handler._text };
+}
+
+/**
+ */
+add_task(async function test_basic_multipart_related() {
+ let compWin = open_compose_new_mail();
+ compWin.window.focus();
+ EventUtils.sendString("someone@example.com", compWin.window);
+ compWin.window.document.getElementById("msgSubject").focus();
+ EventUtils.sendString("multipart/related", compWin.window);
+ compWin.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Here is a prologue.\n", compWin.window);
+
+ const fname = "data/tb-logo.png";
+ let file = new FileUtils.File(getTestFilePath(fname));
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let fileURL = fileHandler.getURLSpecFromActualFile(file);
+
+ // Add a simple image to our dialog
+ plan_for_modal_dialog("Mail:image", async function (dialog) {
+ // Insert the url of the image.
+ dialog.window.focus();
+ EventUtils.sendString(fileURL, dialog.window);
+ dialog.window.document.getElementById("altTextInput").focus();
+ EventUtils.sendString("Alt text", dialog.window);
+ await new Promise(resolve => setTimeout(resolve));
+
+ // Accept the dialog
+ dialog.window.document.querySelector("dialog").acceptDialog();
+ });
+
+ let insertMenu = compWin.window.document.getElementById("InsertPopupButton");
+ let insertMenuPopup = compWin.window.document.getElementById("InsertPopup");
+
+ EventUtils.synthesizeMouseAtCenter(insertMenu, {}, insertMenu.ownerGlobal);
+ await click_menus_in_sequence(insertMenuPopup, [{ id: "InsertImageItem" }]);
+
+ wait_for_modal_dialog();
+ wait_for_window_close();
+ await new Promise(resolve => setTimeout(resolve));
+
+ await save_compose_message(compWin.window);
+ close_compose_window(compWin);
+ await TestUtils.waitForCondition(
+ () => gDrafts.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ // Make sure that the headers are right on this one.
+ await be_in_folder(gDrafts);
+ let draftMsg = select_click_row(0);
+ let { headers, text } = getMsgHeaders(draftMsg, true);
+ Assert.equal(headers.get("").contentType.type, "multipart/related");
+ Assert.equal(headers.get("1").contentType.type, "text/html");
+ Assert.equal(headers.get("2").contentType.type, "image/png");
+ Assert.equal(headers.get("2").get("Content-Transfer-Encoding"), "base64");
+ Assert.equal(
+ headers.get("2").getRawHeader("Content-Disposition")[0],
+ 'inline; filename="tb-logo.png"'
+ );
+ let cid = headers.get("2").getRawHeader("Content-ID")[0].slice(1, -1);
+ if (!text.get("1").includes('src="cid:' + cid + '"')) {
+ throw new Error("Expected HTML to refer to cid " + cid);
+ }
+ press_delete(mc); // Delete message
+});
diff --git a/comm/mail/test/browser/composition/browser_newmsgComposeIdentity.js b/comm/mail/test/browser/composition/browser_newmsgComposeIdentity.js
new file mode 100644
index 0000000000..ad796dbfaa
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_newmsgComposeIdentity.js
@@ -0,0 +1,273 @@
+/* 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/. */
+
+/**
+ * Tests that compose new message chooses the correct initial identity when
+ * called from the context of an open composer.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+var {
+ close_compose_window,
+ open_compose_new_mail,
+ save_compose_message,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder, get_special_folder, mc, press_delete, select_click_row } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+ );
+var { click_menus_in_sequence, plan_for_new_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gInbox;
+var gDrafts;
+var account;
+
+var identityKey1;
+var identity1Email = "x@example.invalid";
+var identityKey2;
+var identity2Email = "y@example.invalid";
+var identity2Name = "User Y";
+var identity2From = identity2Name + " <" + identity2Email + ">";
+var identityKey3;
+var identity3Email = "z@example.invalid";
+var identity3Name = "User Z";
+var identity3Label = "Label Z";
+var identityKey4;
+
+add_setup(async function () {
+ // Now set up an account with some identities.
+ account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "New Msg Compose Identity Testing",
+ "pop3"
+ );
+
+ let identity1 = MailServices.accounts.createIdentity();
+ identity1.email = identity1Email;
+ account.addIdentity(identity1);
+ identityKey1 = identity1.key;
+
+ let identity2 = MailServices.accounts.createIdentity();
+ identity2.email = identity2Email;
+ identity2.fullName = identity2Name;
+ account.addIdentity(identity2);
+ identityKey2 = identity2.key;
+
+ let identity3 = MailServices.accounts.createIdentity();
+ identity3.email = identity3Email;
+ identity3.fullName = identity3Name;
+ identity3.label = identity3Label;
+ account.addIdentity(identity3);
+ identityKey3 = identity3.key;
+
+ // Identity with no data.
+ let identity4 = MailServices.accounts.createIdentity();
+ account.addIdentity(identity4);
+ identityKey4 = identity4.key;
+
+ gInbox = account.incomingServer.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+/**
+ * Helper to check that a suitable From identity was set up in the given
+ * composer window.
+ *
+ * @param cwc Compose window controller.
+ * @param aIdentityKey The key of the expected identity.
+ * @param aIdentityAlias The displayed label of the expected identity.
+ * @param aIdentityValue The value of the expected identity
+ * (the sender address to be sent out).
+ */
+function checkCompIdentity(cwc, aIdentityKey, aIdentityAlias, aIdentityValue) {
+ let identityList = cwc.window.document.getElementById("msgIdentity");
+
+ Assert.equal(
+ cwc.window.getCurrentIdentityKey(),
+ aIdentityKey,
+ "The From identity is not correctly selected"
+ );
+
+ if (aIdentityAlias) {
+ Assert.equal(
+ identityList.label,
+ aIdentityAlias,
+ "The From address does not have the correct label"
+ );
+ }
+
+ if (aIdentityValue) {
+ Assert.equal(
+ identityList.value,
+ aIdentityValue,
+ "The From address does not have the correct value"
+ );
+ }
+}
+
+/**
+ * Test that starting a new message from an open compose window gets the
+ * expected initial identity.
+ */
+add_task(async function test_compose_from_composer() {
+ await be_in_folder(gInbox);
+
+ let cwc = open_compose_new_mail();
+ checkCompIdentity(cwc, account.defaultIdentity.key);
+
+ // Compose a new message from the compose window.
+ plan_for_new_window("msgcompose");
+ EventUtils.synthesizeKey(
+ "n",
+ { shiftKey: false, accelKey: true },
+ cwc.window
+ );
+ let newCompWin = wait_for_compose_window();
+ checkCompIdentity(newCompWin, account.defaultIdentity.key);
+ close_compose_window(newCompWin);
+
+ // Switch to identity2 in the main compose window, new compose windows
+ // starting from here should use the same identity as its "parent".
+ await chooseIdentity(cwc.window, identityKey2);
+ checkCompIdentity(cwc, identityKey2);
+
+ // Compose a second new message from the compose window.
+ plan_for_new_window("msgcompose");
+ EventUtils.synthesizeKey(
+ "n",
+ { shiftKey: false, accelKey: true },
+ cwc.window
+ );
+ let newCompWin2 = wait_for_compose_window();
+ checkCompIdentity(newCompWin2, identityKey2);
+
+ close_compose_window(newCompWin2);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 87987
+ * Test editing the identity email/name for the current composition.
+ */
+add_task(async function test_editing_identity() {
+ Services.prefs.setBoolPref("mail.compose.warned_about_customize_from", true);
+ await be_in_folder(gInbox);
+
+ let compWin = open_compose_new_mail();
+ checkCompIdentity(compWin, account.defaultIdentity.key, identity1Email);
+
+ // Input custom identity data into the From field.
+ let customName = "custom";
+ let customEmail = "custom@edited.invalid";
+ let identityCustom = customName + " <" + customEmail + ">";
+
+ EventUtils.synthesizeMouseAtCenter(
+ compWin.window.document.getElementById("msgIdentity"),
+ {},
+ compWin.window.document.getElementById("msgIdentity").ownerGlobal
+ );
+ await click_menus_in_sequence(
+ compWin.window.document.getElementById("msgIdentityPopup"),
+ [{ command: "cmd_customizeFromAddress" }]
+ );
+ utils.waitFor(
+ () => compWin.window.document.getElementById("msgIdentity").editable
+ );
+
+ compWin.window.document.getElementById("msgIdentityPopup").focus();
+ EventUtils.sendString(identityCustom, compWin.window);
+ checkCompIdentity(
+ compWin,
+ account.defaultIdentity.key,
+ identityCustom,
+ identityCustom
+ );
+ close_compose_window(compWin);
+
+ /* Temporarily disabled due to intermittent failure, bug 1237565.
+ TODO: To be reeabled in bug 1238264.
+ // Save message with this changed identity.
+ compWin.window.SaveAsDraft();
+
+ // Switch to another identity to see if editable field still obeys predefined
+ // identity values.
+ await click_menus_in_sequence(compWin.window.document.getElementById("msgIdentityPopup"),
+ [ { identitykey: identityKey2 } ]);
+ checkCompIdentity(compWin, identityKey2, identity2From, identity2From);
+
+ // This should not save the identity2 to the draft message.
+ close_compose_window(compWin);
+
+ await be_in_folder(gDrafts);
+ let curMessage = select_click_row(0);
+ Assert.equal(curMessage.author, identityCustom);
+ // Remove the saved draft.
+ press_delete(mc);
+ */
+ Services.prefs.setBoolPref("mail.compose.warned_about_customize_from", false);
+});
+
+/**
+ * Bug 318495
+ * Test how an identity displays and behaves in the compose window.
+ */
+add_task(async function test_display_of_identities() {
+ await be_in_folder(gInbox);
+
+ let cwc = open_compose_new_mail();
+ checkCompIdentity(cwc, account.defaultIdentity.key, identity1Email);
+
+ await chooseIdentity(cwc.window, identityKey2);
+ checkCompIdentity(cwc, identityKey2, identity2From, identity2From);
+
+ await chooseIdentity(cwc.window, identityKey4);
+ checkCompIdentity(
+ cwc,
+ identityKey4,
+ "[nsIMsgIdentity: " + identityKey4 + "]"
+ );
+
+ await chooseIdentity(cwc.window, identityKey3);
+ let identity3From = identity3Name + " <" + identity3Email + ">";
+ checkCompIdentity(
+ cwc,
+ identityKey3,
+ identity3From + " (" + identity3Label + ")",
+ identity3From
+ );
+
+ // Bug 1152045, check that the email address from the selected identity
+ // is properly used for the From field in the created message.
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await be_in_folder(gDrafts);
+ let curMessage = select_click_row(0);
+ Assert.equal(curMessage.author, identity3From);
+ // Remove the saved draft.
+ press_delete(mc);
+});
+
+registerCleanupFunction(function () {
+ account.removeIdentity(MailServices.accounts.getIdentity(identityKey1));
+ account.removeIdentity(MailServices.accounts.getIdentity(identityKey2));
+ account.removeIdentity(MailServices.accounts.getIdentity(identityKey3));
+
+ // The last identity of an account can't be removed so clear all its prefs
+ // which effectively destroys it.
+ MailServices.accounts.getIdentity(identityKey4).clearAllValues();
+ MailServices.accounts.removeAccount(account);
+});
diff --git a/comm/mail/test/browser/composition/browser_paragraph_state.js b/comm/mail/test/browser/composition/browser_paragraph_state.js
new file mode 100644
index 0000000000..f6101de44c
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_paragraph_state.js
@@ -0,0 +1,888 @@
+/* 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/. */
+
+/**
+ * Test paragraph state.
+ */
+
+requestLongerTimeout(2);
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_newline_p() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let firstText = "first line";
+ let secondText = "second line";
+ let thirdText = "third line";
+
+ formatHelper.focusMessage();
+
+ await formatHelper.selectParagraphState("p");
+ await formatHelper.typeInMessage(firstText);
+
+ // Pressing Enter, without Shift, creates a new block.
+ await formatHelper.typeEnterInMessage(false);
+ formatHelper.assertMessageBodyContent(
+ // Empty "P" block must contain some content, otherwise it will collapse,
+ // currently this is achieved with a <BR>.
+ [
+ { block: "P", content: [firstText] },
+ { block: "P", content: ["<BR>"] },
+ ],
+ "After Enter (no Shift)"
+ );
+ await formatHelper.typeInMessage(secondText);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: "P", content: [firstText] },
+ { block: "P", content: [secondText] },
+ ],
+ "After Enter (no Shift) and typing"
+ );
+
+ // Pressing Shift+Enter creates a break.
+ await formatHelper.typeEnterInMessage(true);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: "P", content: [firstText] },
+ // NOTE: that the two <BR> are necessary, the first produces the newline,
+ // whilst the second stops the new line from collapsing without any text.
+ { block: "P", content: [secondText + "<BR><BR>"] },
+ ],
+ "After Shift+Enter"
+ );
+ await formatHelper.typeInMessage(thirdText);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: "P", content: [firstText] },
+ // NOTE: with the next line being non-empty, the extra <BR> is no longer
+ // needed to stop the line from collapsing.
+ { block: "P", content: [secondText + "<BR>" + thirdText] },
+ ],
+ "After Shift+Enter and typing"
+ );
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_newline_headers() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let firstText = "first line";
+ let secondText = "second line";
+ let thirdText = "third line";
+
+ formatHelper.focusMessage();
+
+ for (let num = 1; num <= 6; num++) {
+ let state = `h${num}`;
+ let block = `H${num}`;
+
+ await formatHelper.selectParagraphState(state);
+ await formatHelper.typeInMessage(firstText);
+
+ // Pressing Shift+Enter creates a break.
+ await formatHelper.typeEnterInMessage(true);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [firstText + "<BR><BR>"] }],
+ `After Shift+Enter in ${state}`
+ );
+ await formatHelper.typeInMessage(secondText);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [firstText + "<BR>" + secondText] }],
+ `After Shift+Enter in ${state} and typing`
+ );
+
+ // Pressing Enter, without Shift, creates a new paragraph.
+ await formatHelper.typeEnterInMessage(false);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block, content: [firstText + "<BR>" + secondText] },
+ { block: "P", content: ["<BR>"] },
+ ],
+ `After Enter (no Shift) in ${state}`
+ );
+
+ await formatHelper.assertShownParagraphState(
+ "p",
+ `Shows paragraph state after Enter (no Shift) in ${state}`
+ );
+
+ await formatHelper.typeInMessage(thirdText);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block, content: [firstText + "<BR>" + secondText] },
+ { block: "P", content: [thirdText] },
+ ],
+ `After Enter (no Shift) in ${state} and typing`
+ );
+
+ await formatHelper.deleteAll();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_newline_pre_and_address() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let firstText = "first line";
+ let secondText = "second line";
+ let thirdText = "third line";
+
+ formatHelper.focusMessage();
+
+ for (let state of ["pre", "address"]) {
+ let block = state.toUpperCase();
+
+ await formatHelper.selectParagraphState(state);
+ await formatHelper.typeInMessage(firstText);
+
+ // Pressing Shift+Enter creates a break.
+ await formatHelper.typeEnterInMessage(true);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [firstText + "<BR><BR>"] }],
+ `After Shift+Enter in ${state}`
+ );
+ await formatHelper.typeInMessage(secondText);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [firstText + "<BR>" + secondText] }],
+ `After Shift+Enter in ${state} and typing`
+ );
+
+ // Pressing Enter, without Shift, does the same.
+ await formatHelper.typeEnterInMessage(false);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [firstText + "<BR>" + secondText + "<BR><BR>"] }],
+ `After Enter (no Shift) in ${state}`
+ );
+
+ await formatHelper.typeInMessage(thirdText);
+ formatHelper.assertMessageBodyContent(
+ [
+ {
+ block,
+ content: [firstText + "<BR>" + secondText + "<BR>" + thirdText],
+ },
+ ],
+ `After Enter (no Shift) in ${state} and typing`
+ );
+
+ await formatHelper.deleteAll();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_newline_body() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let firstText = "first line";
+ let secondText = "second line";
+ let thirdText = "third line";
+
+ formatHelper.focusMessage();
+
+ await formatHelper.selectParagraphState("");
+ await formatHelper.typeInMessage(firstText);
+
+ // Pressing Shift+Enter creates a break.
+ await formatHelper.typeEnterInMessage(true);
+ formatHelper.assertMessageBodyContent(
+ [firstText + "<BR><BR>"],
+ "After Shift+Enter in body"
+ );
+ await formatHelper.typeInMessage(secondText);
+ formatHelper.assertMessageBodyContent(
+ [firstText + "<BR>" + secondText],
+ "After Shift+Enter in body and typing"
+ );
+
+ // Pressing Enter, without Shift, either side of the Enter get converted into
+ // paragraphs.
+ await formatHelper.typeEnterInMessage(false);
+ formatHelper.assertMessageBodyContent(
+ [
+ firstText,
+ { block: "P", content: [secondText] },
+ { block: "P", content: ["<BR>"] },
+ ],
+ "After Enter (no Shift) in body"
+ );
+ await formatHelper.assertShownParagraphState(
+ "p",
+ "Shows paragraph state after Enter (no Shift) in body"
+ );
+
+ await formatHelper.typeInMessage(thirdText);
+ formatHelper.assertMessageBodyContent(
+ [
+ firstText,
+ { block: "P", content: [secondText] },
+ { block: "P", content: [thirdText] },
+ ],
+ "After Enter (no Shift) in ${state} and typing"
+ );
+
+ close_compose_window(controller);
+});
+
+async function initialiseParagraphs(formatHelper) {
+ let blockSet = [];
+ let start = 0;
+ let first = true;
+ for (let text of ["first block", "second block", "third block"]) {
+ if (first) {
+ first = false;
+ } else {
+ await formatHelper.typeEnterInMessage();
+ }
+ await formatHelper.typeInMessage(text);
+
+ let end = start + text.length;
+ blockSet.push({ text, start, end });
+ start = end + 1; // Plus newline.
+ }
+
+ return blockSet;
+}
+
+add_task(async function test_non_body_paragraph_state() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ // NOTE: we don't start with the default paragraph state because we want to
+ // detect a *change* in the paragraph state from the previous state.
+ let stateSet = ["address", "pre"];
+ for (let i = 1; i <= 6; i++) {
+ stateSet.push(`h${i}`);
+ }
+ stateSet.push("p");
+
+ // Before focus, disabled.
+ Assert.ok(
+ formatHelper.paragraphStateSelector.disabled,
+ "Selector should be disabled with no focus"
+ );
+
+ formatHelper.focusMessage();
+ Assert.ok(
+ !formatHelper.paragraphStateSelector.disabled,
+ "Selector should be enabled with focus"
+ );
+
+ // Initially in the paragraph state.
+ await formatHelper.assertShownParagraphState("p", "Initial paragraph");
+
+ let blockSet = await initialiseParagraphs(formatHelper);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: "P", content: [blockSet[0].text] },
+ { block: "P", content: [blockSet[1].text] },
+ { block: "P", content: [blockSet[2].text] },
+ ],
+ "Three paragraphs"
+ );
+
+ let prevState = "p";
+ for (let state of stateSet) {
+ // Select end.
+ let prevBlock = prevState.toUpperCase();
+ let block = state.toUpperCase();
+ await formatHelper.selectTextRange(blockSet[2].end);
+ // Select through menu.
+ await formatHelper.selectParagraphState(state);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: prevBlock, content: [blockSet[0].text] },
+ { block: prevBlock, content: [blockSet[1].text] },
+ { block, content: [blockSet[2].text] },
+ ],
+ `${state} on last block`
+ );
+
+ await formatHelper.assertShownParagraphState(
+ state,
+ `${state} on last block`
+ );
+ // Select across second block.
+ await formatHelper.selectTextRange(
+ blockSet[1].start + 2,
+ blockSet[1].end - 2
+ );
+ await formatHelper.assertShownParagraphState(
+ prevState,
+ `${state} on last block, with second block selected`
+ );
+
+ await formatHelper.selectFromFormatSubMenu(
+ formatHelper.getParagraphStateMenuItem(state),
+ formatHelper.paragraphStateMenu
+ );
+ formatHelper.assertMessageBodyContent(
+ [
+ { block: prevBlock, content: [blockSet[0].text] },
+ { block, content: [blockSet[1].text] },
+ { block, content: [blockSet[2].text] },
+ ],
+ `${state} on last two blocks`
+ );
+
+ // Select across first and second line.
+ await formatHelper.selectTextRange(2, blockSet[1].start + 2);
+ // Mixed state has no value.
+ await formatHelper.assertShownParagraphState(
+ null,
+ `${state} on last two blocks, with mixed selection`
+ );
+ await formatHelper.selectParagraphState(state);
+
+ formatHelper.assertMessageBodyContent(
+ [
+ { block, content: [blockSet[0].text] },
+ { block, content: [blockSet[1].text] },
+ { block, content: [blockSet[2].text] },
+ ],
+ `${state} on all blocks`
+ );
+ prevState = state;
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_body_paragraph_state() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ let blockSet = await initialiseParagraphs(formatHelper);
+
+ await formatHelper.selectTextRange(0);
+ // Body state has value "".
+ await formatHelper.selectParagraphState("");
+ formatHelper.assertMessageBodyContent(
+ [
+ blockSet[0].text,
+ { block: "P", content: [blockSet[1].text] },
+ { block: "P", content: [blockSet[2].text] },
+ ],
+ "body on first block"
+ );
+ await formatHelper.assertShownParagraphState("", "body on first block");
+ await formatHelper.selectTextRange(blockSet[1].start, blockSet[2].start + 1);
+ await formatHelper.assertShownParagraphState("p", "last two selected");
+
+ await formatHelper.selectFromFormatSubMenu(
+ formatHelper.getParagraphStateMenuItem(""),
+ formatHelper.paragraphStateMenu
+ );
+ formatHelper.assertMessageBodyContent(
+ [blockSet[0].text + "<BR>" + blockSet[1].text + "<BR>" + blockSet[2].text],
+ "body on all blocks"
+ );
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_convert_from_body_paragraph_state() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let stateSet = ["p", "address", "pre"];
+ for (let i = 1; i <= 6; i++) {
+ stateSet.push(`h${i}`);
+ }
+
+ let firstText = "first line";
+ let secondText = "second line";
+ // Plus newline break.
+ let fullLength = firstText.length + 1 + secondText.length;
+
+ formatHelper.focusMessage();
+
+ for (let state of stateSet) {
+ let block = state.toUpperCase();
+
+ await formatHelper.selectParagraphState("");
+ await formatHelper.typeInMessage(firstText);
+ await formatHelper.typeEnterInMessage(true);
+ await formatHelper.typeInMessage(secondText);
+ formatHelper.assertMessageBodyContent(
+ [firstText + "<BR>" + secondText],
+ `body at start (${state})`
+ );
+
+ // Changing to a non-body state replaces each line with a block.
+ await formatHelper.selectTextRange(0, fullLength);
+ await formatHelper.selectParagraphState(state);
+ formatHelper.assertMessageBodyContent(
+ [
+ { block, content: [firstText] },
+ { block, content: [secondText] },
+ ],
+ `${state} at end`
+ );
+
+ await formatHelper.deleteAll();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_heading_implies_bold() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ let boldItem = formatHelper.getStyleMenuItem("bold");
+ let strongItem = formatHelper.getStyleMenuItem("strong");
+
+ for (let num = 1; num <= 6; num++) {
+ let state = `h${num}`;
+ let block = `H${num}`;
+ let text = "some text";
+
+ await formatHelper.selectParagraphState(state);
+ await formatHelper.assertShownStyles(
+ "bold",
+ `Bold on change to ${state} state`
+ );
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownStyles(
+ "bold",
+ `Bold when typing in ${state} state`
+ );
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ `${state} state, without any explicit styling`
+ );
+
+ // Trying to undo bold does nothing.
+ formatHelper.boldButton.click();
+ // See Bug 1718534
+ // await formatHelper.assertShownStyles(
+ // "bold",
+ // `Still bold when clicking bold in the ${state} state`
+ // );
+ text += "a";
+ await formatHelper.typeInMessage("a");
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ `${state} state, without style change, after clicking bold`
+ );
+ await formatHelper.assertShownStyles(
+ "bold",
+ `Still bold when clicking bold in the ${state} state and typing`
+ );
+
+ // Select through the style menu.
+ await formatHelper.selectFromFormatSubMenu(
+ boldItem,
+ formatHelper.styleMenu
+ );
+ // See Bug 1718534
+ // await formatHelper.assertShownStyles(
+ // "bold",
+ // `Still bold when selecting bold in the ${state} state`
+ // );
+ text += "b";
+ await formatHelper.typeInMessage("b");
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ `${state} state, without style change, after selecting bold`
+ );
+ await formatHelper.assertShownStyles(
+ "bold",
+ `Still bold when selecting bold in the ${state} state and typing`
+ );
+
+ // Can still add and remove a style that implies bold.
+ let strongText = " Strong ";
+ await formatHelper.selectFromFormatSubMenu(
+ strongItem,
+ formatHelper.styleMenu
+ );
+ // See Bug 1716840
+ // await formatHelper.assertShownStyles(
+ // "strong",
+ // `Selecting strong in ${state} state`
+ // );
+ await formatHelper.typeInMessage(strongText);
+ await formatHelper.assertShownStyles(
+ "strong",
+ `Selecting strong in ${state} state and typing`
+ );
+ // Deselect.
+ await formatHelper.selectFromFormatSubMenu(
+ strongItem,
+ formatHelper.styleMenu
+ );
+ // See Bug 1716840
+ // await formatHelper.assertShownStyles(
+ // "bold",
+ // `UnSelecting strong in ${state} state`
+ // );
+
+ let moreText = "more";
+ await formatHelper.typeInMessage(moreText);
+ await formatHelper.assertShownStyles(
+ "bold",
+ `UnSelecting strong in ${state} state and typing`
+ );
+
+ formatHelper.assertMessageBodyContent(
+ [
+ {
+ block,
+ content: [text, { tags: ["STRONG"], text: strongText }, moreText],
+ },
+ ],
+ `Strong region in ${state} state`
+ );
+
+ // Change to paragraph.
+ await formatHelper.selectParagraphState("p");
+ await formatHelper.assertShownStyles(
+ null,
+ `Lose bold when switching to Paragraph from ${state} state`
+ );
+ formatHelper.assertMessageBodyContent(
+ [
+ {
+ block: "P",
+ content: [text, { tags: ["STRONG"], text: strongText }, moreText],
+ },
+ ],
+ `Paragraph block from ${state} state`
+ );
+
+ // NOTE: Switching from "p" state to a heading state will *not* remove the
+ // bold tags.
+
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_address_implies_italic() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ let italicItem = formatHelper.getStyleMenuItem("italic");
+
+ let otherStyles = Array.from(formatHelper.styleDataMap.values()).filter(
+ data => data.implies?.name === "italic" || data.linked?.name === "italic"
+ );
+
+ let block = "ADDRESS";
+ let text = "some text";
+
+ await formatHelper.selectParagraphState("address");
+ await formatHelper.assertShownStyles(
+ "italic",
+ "Italic on change to address state"
+ );
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownStyles(
+ "italic",
+ "Italic when typing in address state"
+ );
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ "Address state, without any explicit styling"
+ );
+
+ // Trying to undo italic does nothing.
+ formatHelper.italicButton.click();
+ // See Bug 1718534
+ // await formatHelper.assertShownStyles(
+ // "italic",
+ // "Still italic when clicking italic in the address state"
+ // );
+ text += "a";
+ await formatHelper.typeInMessage("a");
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ "Address state, without style change, after clicking italic"
+ );
+ await formatHelper.assertShownStyles(
+ "italic",
+ "Still italic when clicking italic in the address state and typing"
+ );
+
+ // Select through the style menu.
+ await formatHelper.selectFromFormatSubMenu(
+ italicItem,
+ formatHelper.styleMenu
+ );
+ // See Bug 1718534
+ // await formatHelper.assertShownStyles(
+ // "italic",
+ // "Still italic when selecting italic in the address state"
+ // );
+ text += "b";
+ await formatHelper.typeInMessage("b");
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ "Address state, without style change, after selecting italic"
+ );
+ await formatHelper.assertShownStyles(
+ "italic",
+ "Still italic when selecting italic in the address state and typing"
+ );
+
+ let content = [text];
+ // Can still add and remove a style that implies italic.
+ for (let style of otherStyles) {
+ let { name, item, tag } = style;
+ let otherText = name;
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.styleMenu);
+ // See Bug 1716840
+ // await formatHelper.assertShownStyles(
+ // style,
+ // `Selecting ${name} in address state`
+ // );
+ await formatHelper.typeInMessage(otherText);
+ await formatHelper.assertShownStyles(
+ style,
+ `Selecting ${name} in address state and typing`
+ );
+ // Deselect.
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.styleMenu);
+ // See Bug 1716840
+ // await formatHelper.assertShownStyles(
+ // "italic",
+ // `UnSelecting ${name} in address state`
+ // );
+
+ let moreText = "more";
+ await formatHelper.typeInMessage(moreText);
+ await formatHelper.assertShownStyles(
+ "italic",
+ `UnSelecting ${name} in address state and typing`
+ );
+
+ content.push({ text: otherText, tags: [tag] });
+ content.push(moreText);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content }],
+ `${name} region in address state`
+ );
+ }
+
+ // Change to paragraph.
+ await formatHelper.selectParagraphState("p");
+ await formatHelper.assertShownStyles(
+ null,
+ "Lose italic when switching to Paragraph from address state"
+ );
+ formatHelper.assertMessageBodyContent(
+ [{ block: "P", content }],
+ "Paragraph block"
+ );
+
+ // NOTE: Switching from "p" state to a heading state will *not* remove the
+ // italic tags.
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_preformat_implies_fixed_width() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ let ttItem = formatHelper.getStyleMenuItem("tt");
+
+ let otherStyles = Array.from(formatHelper.styleDataMap.values()).filter(
+ data => data.implies?.name === "tt" || data.linked?.name === "tt"
+ );
+
+ async function assertFontAndStyle(font, style, message) {
+ await formatHelper.assertShownFont(
+ font,
+ `${message}: Font family "${font}" is shown`
+ );
+ await formatHelper.assertShownStyles(
+ style,
+ `${message}: ${style} is shown`
+ );
+ }
+
+ let block = "PRE";
+ let text = "some text";
+
+ await formatHelper.selectParagraphState("pre");
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ "Fixed width on change to preformat state"
+ );
+ await formatHelper.typeInMessage(text);
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ "Fixed width when typing in preformat state"
+ );
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ "Preformat state, without any explicit styling"
+ );
+
+ // Try to change the font to Variable Width.
+ await formatHelper.selectFont("");
+ // See Bug 1718534
+ // await assertFontAndStyle(
+ // "monospace",
+ // "tt",
+ // "Still fixed width when selecting Variable Width font"
+ // );
+ text += "b";
+ await formatHelper.typeInMessage("b");
+ formatHelper.assertMessageBodyContent(
+ [{ block, content: [text] }],
+ "Preformat state, without style change, after unselecting font"
+ );
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ "Still fixed width when selecting Variable Width font and typing"
+ );
+
+ let content = [text];
+ // Can still set other fonts.
+ let font = "Helvetica, Arial, sans-serif";
+ await formatHelper.selectFont(font);
+ // See Bug 1716840 (comment 3).
+ // await assertFontAndStyle(
+ // font,
+ // null,
+ // `Selecting font "${font}" in preformat state`
+ // );
+ let fontText = "some font text";
+ await formatHelper.typeInMessage(fontText);
+ content.push({ text: fontText, font });
+ await assertFontAndStyle(
+ font,
+ null,
+ `Selecting font "${font}" in preformat state and typing`
+ );
+ // Deselect.
+ // See Bug 1718563 for why we need to select Variable Width instead of Fixed
+ // Width.
+ // await formatHelper.selectFont("monospace");
+ await formatHelper.selectFont("");
+ // See Bug 1718534
+ // await assertFontAndStyle(
+ // "monospace",
+ // "tt",
+ // `UnSelecting font "${font}" in preformat state`
+ // );
+
+ fontText = "no more font";
+ await formatHelper.typeInMessage(fontText);
+ content.push(fontText);
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ `UnSelecting font "${font}" in preformat state and typing`
+ );
+
+ formatHelper.assertMessageBodyContent(
+ [{ block, content }],
+ `"${font}" region in preformat state`
+ );
+
+ // Trying to undo tt does nothing.
+ await formatHelper.selectFromFormatSubMenu(ttItem, formatHelper.styleMenu);
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ "Still fixed width when selecting Fixed Width style"
+ );
+ await formatHelper.typeInMessage("a");
+ content[content.length - 1] += "a";
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ "Still fixed width when selecting Fixed Width style and typing"
+ );
+
+ formatHelper.assertMessageBodyContent(
+ [{ block, content }],
+ "Preformat state, without style change, after selecting tt"
+ );
+
+ // Can still add and remove a style that implies tt.
+ for (let style of otherStyles) {
+ let { name, item, tag } = style;
+ let otherText = name;
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.styleMenu);
+ // See Bug 1716840
+ // await assertFontAndStyle(
+ // "monospace",
+ // name,
+ // `Selecting ${name} in preformat state`
+ // );
+ await formatHelper.typeInMessage(otherText);
+ await assertFontAndStyle(
+ "monospace",
+ name,
+ `Selecting ${name} in preformat state and typing`
+ );
+ // Deselect.
+ await formatHelper.selectFromFormatSubMenu(item, formatHelper.styleMenu);
+ // See Bug 1716840
+ // await assertFontAndStyle(
+ // "monospace",
+ // "tt",
+ // `UnSelecting ${name} in preformat state`
+ // );
+
+ let moreText = "more";
+ await formatHelper.typeInMessage(moreText);
+ await assertFontAndStyle(
+ "monospace",
+ "tt",
+ `UnSelecting ${name} in preformat state and typing`
+ );
+
+ content.push({ text: otherText, tags: [tag] });
+ content.push(moreText);
+ formatHelper.assertMessageBodyContent(
+ [{ block, content }],
+ `${name} region in preformat state`
+ );
+ }
+
+ // Change to paragraph.
+ await formatHelper.selectParagraphState("p");
+ await assertFontAndStyle(
+ "",
+ null,
+ "Lose fixed width when switching to Paragraph from preformat state"
+ );
+ formatHelper.assertMessageBodyContent(
+ [{ block: "P", content }],
+ "Paragraph block"
+ );
+
+ // NOTE: Switching from "p" state to a heading state will *not* remove the
+ // monospace font.
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_publicRecipientsWarning.js b/comm/mail/test/browser/composition/browser_publicRecipientsWarning.js
new file mode 100644
index 0000000000..8cf6aea9c7
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_publicRecipientsWarning.js
@@ -0,0 +1,734 @@
+/* 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/. */
+
+/**
+ * Tests the warning notification that appears when there are too many public
+ * recipients.
+ */
+
+"use strict";
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var {
+ close_compose_window,
+ open_compose_new_mail,
+ open_compose_with_reply_to_all,
+ setup_msg_contents,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+let publicRecipientLimit = Services.prefs.getIntPref(
+ "mail.compose.warn_public_recipients.threshold"
+);
+
+requestLongerTimeout(5);
+
+/**
+ * Test we only show one warning when "To" recipients goes over the limit
+ * for a reply all.
+ */
+add_task(async function testWarningShowsOnceWhenToFieldOverLimit() {
+ // Now set up an account with some identities.
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "BCC Reply Testing",
+ "pop3"
+ );
+
+ let folder = account.incomingServer.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("Msgs4Reply");
+
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = "bcc@example.com";
+ account.addIdentity(identity);
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ let i = 1;
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "test@example.org,"
+ .repeat(publicRecipientLimit + 100)
+ .replace(/test@/g, () => `test${i++}@`),
+ cc: "Lisa <lisa@example.com>",
+ subject: "msg over the limit for bulk warning",
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+ let cwc = open_compose_with_reply_to_all();
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warning shown when "To" recipients >= ${publicRecipientLimit}`
+ );
+
+ Assert.equal(
+ 1,
+ cwc.window.document.querySelectorAll(
+ `notification-message[value="warnPublicRecipientsNotification"]`
+ ).length,
+ "should have exactly one notification about it"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning displays when the "To" recipients list hits the limit.
+ */
+add_task(async function testWarningShowsWhenToFieldHitsLimit() {
+ let cwc = open_compose_new_mail();
+ let i = 1;
+
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing To Field",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warning shown when "To" recipients >= ${publicRecipientLimit}`
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning displays when the "Cc" recipients list hits the limit.
+ */
+add_task(async function testWarningShowsWhenCcFieldHitLimit() {
+ let cwc = open_compose_new_mail();
+
+ // Click on the Cc recipient label.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !cwc.window.document
+ .getElementById("ccAddrInput")
+ .closest(".address-row")
+ .classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing Cc Field",
+ "",
+ "ccAddrInput"
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warning shown when "Cc" recipients >= ${publicRecipientLimit}`
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning displays when both the "To" and "Cc" recipients lists
+ * combined hit the limit.
+ */
+add_task(async function testWarningShowsWhenToAndCcFieldHitLimit() {
+ let cwc = open_compose_new_mail();
+
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit - 1)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing To and Cc Fields",
+ ""
+ );
+
+ // Click on the Cc recipient label.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !cwc.window.document
+ .getElementById("ccAddrInput")
+ .closest(".address-row")
+ .classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+
+ setup_msg_contents(cwc, "test@example.org", "", "", "ccAddrInput");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warning shown "To" and "Cc" recipients >= ${publicRecipientLimit}`
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the "To" recipients are moved to the "Bcc" field when the user selects
+ * that option.
+ */
+add_task(async function testToRecipientsMovedToBcc() {
+ let cwc = open_compose_new_mail();
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing move to Bcc",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.firstElementChild,
+ {},
+ cwc.window
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ publicRecipientLimit,
+ "Bcc field populated with addresses"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#toAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the To field"
+ );
+
+ await notificationHidden;
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that all the "To" recipients are moved to the "Bcc" field when the
+ * address count is over the limit.
+ */
+add_task(async function testAllToRecipientsMovedToBccWhenOverLimit() {
+ let cwc = open_compose_new_mail();
+ let limit = publicRecipientLimit + 1;
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,".repeat(limit).replace(/test@/g, () => `test${i++}@`),
+ "Testing move to Bcc",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.firstElementChild,
+ {},
+ cwc.window
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ limit,
+ "Bcc field populated with addresses"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#toAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the To field"
+ );
+
+ await notificationHidden;
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the "Cc" recipients are moved to the "Bcc" field when the user selects
+ * that option.
+ */
+add_task(async function testCcRecipientsMovedToBcc() {
+ let cwc = open_compose_new_mail();
+
+ // Click on the Cc recipient label.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !cwc.window.document
+ .getElementById("ccAddrInput")
+ .closest(".address-row")
+ .classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing move to Bcc",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.firstElementChild,
+ {},
+ cwc.window
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ publicRecipientLimit,
+ "Bcc field populated with addresses"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#ccAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the Cc field"
+ );
+
+ await notificationHidden;
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that all the "Cc" recipients are moved to the "Bcc" field when the
+ * address count is over the limit.
+ */
+add_task(async function testAllCcRecipientsMovedToBccWhenOverLimit() {
+ let cwc = open_compose_new_mail();
+ let limit = publicRecipientLimit + 1;
+
+ // Click on the Cc recipient label.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !cwc.window.document
+ .getElementById("ccAddrInput")
+ .closest(".address-row")
+ .classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,".repeat(limit).replace(/test@/g, () => `test${i++}@`),
+ "Testing move to Bcc",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.firstElementChild,
+ {},
+ cwc.window
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ limit,
+ "Bcc field populated with addresses"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#ccAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the Cc field"
+ );
+
+ await notificationHidden;
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that both the "To" and "Cc" recipients are moved to the "Bcc" field when
+ * the user selects that option.
+ */
+add_task(async function testToAndCcRecipientsMovedToBcc() {
+ let cwc = open_compose_new_mail();
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit - 1)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing move to Bcc",
+ ""
+ );
+
+ // Click on the Cc recipient label.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("addr_ccShowAddressRowButton"),
+ {},
+ cwc.window
+ );
+ // The Cc field should now be visible.
+ Assert.ok(
+ !cwc.window.document
+ .getElementById("ccAddrInput")
+ .closest(".address-row")
+ .classList.contains("hidden"),
+ "The Cc field is visible"
+ );
+ setup_msg_contents(cwc, "test@example.org", "", "");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.firstElementChild,
+ {},
+ cwc.window
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ publicRecipientLimit,
+ "Bcc field populated with addresses"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#toAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the To field"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#ccAddrContainer > mail-address-pill")
+ .length,
+ 0,
+ "addresses removed from the Cc field"
+ );
+
+ await notificationHidden;
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the warning is removed when the user chooses to "Keep Recipients Public".
+ */
+add_task(async function testWarningRemovedWhenKeepPublic() {
+ let cwc = open_compose_new_mail();
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing dismissal",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ notification.buttonContainer.lastElementChild,
+ {},
+ cwc.window
+ );
+
+ await notificationHidden;
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll("#toAddrContainer > mail-address-pill")
+ .length,
+ publicRecipientLimit,
+ "addresses were not removed from the field"
+ );
+
+ Assert.equal(
+ cwc.window.document.querySelectorAll(
+ "#bccAddrContainer > mail-address-pill"
+ ).length,
+ 0,
+ "no addresses added to the Bcc field"
+ );
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test that the warning is not shown again if the user dismisses it.
+ */
+add_task(async function testWarningNotShownAfterDismissal() {
+ let cwc = open_compose_new_mail();
+ let i = 1;
+ setup_msg_contents(
+ cwc,
+ "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`),
+ "Testing dismissal",
+ ""
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notificationHidden = BrowserTestUtils.waitForCondition(
+ () =>
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning was not removed in time"
+ );
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ EventUtils.synthesizeMouseAtCenter(notification.closeButton, {}, cwc.window);
+
+ await notificationHidden;
+
+ let input = cwc.window.document.getElementById("toAddrInput");
+ input.focus();
+
+ let recipString = "test@example.org,"
+ .repeat(publicRecipientLimit)
+ .replace(/test@/g, () => `test${i++}@`);
+ EventUtils.sendString(recipString, cwc.window);
+
+ // Wait a little in case the notification bar mistakenly appears.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ Assert.ok(
+ !cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ "public recipients warning did not appear after dismissal"
+ );
+ close_compose_window(cwc);
+});
+
+/**
+ * Tests that the individual addresses of a mailing list are considered.
+ */
+add_task(async function testMailingListMembersCounted() {
+ let book = MailServices.ab.getDirectoryFromId(
+ MailServices.ab.newAddressBook("Mochitest", null, 101)
+ );
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "Test List";
+ list = book.addMailList(list);
+
+ for (let i = 0; i < publicRecipientLimit; i++) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.primaryEmail = `test${i}@example`;
+ list.addCard(card);
+ }
+ list.editMailListToDatabase(null);
+
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(cwc, "Test List", "Testing mailing lists", "");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ ),
+ `Timeout waiting for warnPublicRecipientsNotification`
+ );
+
+ let notification = cwc.window.gComposeNotification.getNotificationWithValue(
+ "warnPublicRecipientsNotification"
+ );
+ Assert.equal(
+ notification.messageText.textContent,
+ `The ${publicRecipientLimit} recipients in To and Cc will see each other窶冱 address. You can avoid disclosing recipients by using Bcc instead.`,
+ "total count equals all addresses plus list expanded"
+ );
+
+ MailServices.ab.deleteAddressBook(book.URI);
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_quoteMessage.js b/comm/mail/test/browser/composition/browser_quoteMessage.js
new file mode 100644
index 0000000000..72f53d8315
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_quoteMessage.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+/**
+ * Tests that in the compose window, Options > Quote Message works well for
+ * non-UTF8 encoding.
+ */
+
+var { close_compose_window, open_compose_with_reply, get_compose_body } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ create_folder,
+ get_about_message,
+ open_message_from_file,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var folderToStoreMessages;
+
+add_setup(async function () {
+ folderToStoreMessages = await create_folder("QuoteTestFolder");
+});
+
+add_task(async function test_quoteMessage() {
+ await be_in_folder(folderToStoreMessages);
+
+ let file = new FileUtils.File(getTestFilePath("data/iso-2022-jp.eml"));
+ let msgc = await open_message_from_file(file);
+ // Copy the message to a folder, so that Quote Message menu item is enabled.
+ let documentChild = msgc.window.content.document.documentElement;
+ EventUtils.synthesizeMouseAtCenter(
+ documentChild,
+ { type: "contextmenu", button: 2 },
+ documentChild.ownerGlobal
+ );
+ let aboutMessage = get_about_message(msgc.window);
+ await click_menus_in_sequence(
+ aboutMessage.document.getElementById("mailContext"),
+ [
+ { id: "mailContext-copyMenu" },
+ { label: "Local Folders" },
+ { label: "QuoteTestFolder" },
+ ]
+ );
+ close_window(msgc);
+
+ // Select message and click reply.
+ select_click_row(0);
+ let cwc = open_compose_with_reply();
+ let composeBody = get_compose_body(cwc).textContent;
+ Assert.equal(
+ composeBody.match(/荳也阜/g).length,
+ 1,
+ "Message should be quoted by replying"
+ );
+
+ if (["linux", "win"].includes(AppConstants.platform)) {
+ // Click Options > Quote Message.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("optionsMenu"),
+ {},
+ cwc.window.document.getElementById("optionsMenu").ownerGlobal
+ );
+ await click_menus_in_sequence(
+ cwc.window.document.getElementById("optionsMenuPopup"),
+ [{ id: "menu_quoteMessage" }]
+ );
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+ } else {
+ // Native menubar is used on macOS, didn't find a way to click it.
+ cwc.window.goDoCommand("cmd_quoteMessage");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1));
+ }
+ composeBody = get_compose_body(cwc).textContent;
+ Assert.equal(
+ composeBody.match(/荳也阜/g).length,
+ 2,
+ "Message should be quoted again by Options > Quote Message."
+ );
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_recipientPillsSelection.js b/comm/mail/test/browser/composition/browser_recipientPillsSelection.js
new file mode 100644
index 0000000000..db593530ae
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_recipientPillsSelection.js
@@ -0,0 +1,264 @@
+/* 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/. */
+
+/*
+ * Test the various selection interaction with the recipient pills.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { close_popup } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var modifiers =
+ AppConstants.platform == "macosx" ? { accelKey: true } : { ctrlKey: true };
+
+/**
+ * Test the correct pill selection behavior to properly handle multi selection
+ * and accidental deselection when interacting with other elements.
+ */
+add_task(async function test_pill_selection() {
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.org, test@invalid.foo, test@tinderborx.invalid, alice@foo.test",
+ "Testing recipient pills selection!",
+ "Testing testing testing! "
+ );
+
+ let cDoc = cwc.window.document;
+ let recipientsContainer = cDoc.getElementById("recipientsContainer");
+ let allPills = recipientsContainer.getAllPills();
+
+ Assert.equal(allPills.length, 4, "Pills correctly created");
+
+ // Click on the To input field to move the focus there.
+ EventUtils.synthesizeMouseAtCenter(
+ cDoc.getElementById("toAddrInput"),
+ {},
+ cwc.window
+ );
+ // Ctrl/Cmd+a should select all pills.
+ EventUtils.synthesizeKey("a", modifiers, cwc.window);
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ allPills.length,
+ "All pills currently selected"
+ );
+
+ // Right click on the last pill to open the context menu.
+ let pill3 = allPills[3];
+ let contextMenu = cDoc.getElementById("emailAddressPillPopup");
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ pill3,
+ { type: "contextmenu" },
+ pill3.ownerGlobal
+ );
+ await popupPromise;
+ // The selection should not have changed.
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ allPills.length,
+ "All pills currently selected"
+ );
+ close_popup(cwc, contextMenu);
+
+ // Click on the input field, the pills should all be deselected.
+ EventUtils.synthesizeMouseAtCenter(
+ cDoc.getElementById("toAddrInput"),
+ {},
+ cwc.window
+ );
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ 0,
+ "All pills currently deselected"
+ );
+
+ let popupPromise2 = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ let pill0 = allPills[0];
+ // Right click on the first pill to open the context menu.
+ EventUtils.synthesizeMouseAtCenter(
+ pill0,
+ { type: "contextmenu" },
+ pill0.ownerGlobal
+ );
+ await popupPromise2;
+
+ // The first pill should be selected.
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ 1,
+ "One pill currently selected"
+ );
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills()[0],
+ allPills[0],
+ "The first pill was selected"
+ );
+ close_popup(cwc, contextMenu);
+
+ // Click on the first pill, which should be selected, to trigger edit mode.
+ EventUtils.synthesizeMouseAtCenter(allPills[0], {}, cwc.window);
+ Assert.ok(allPills[0].isEditing, "The pill is in edit mode");
+
+ // Click on the input field, the pills should all be deselected.
+ EventUtils.synthesizeMouseAtCenter(
+ cDoc.getElementById("toAddrInput"),
+ {},
+ cwc.window
+ );
+
+ // Click on the first pill to select it.
+ EventUtils.synthesizeMouseAtCenter(allPills[0], {}, cwc.window);
+ // Ctrl/Cmd+Click ont he second pill to add it to the selection.
+ EventUtils.synthesizeMouseAtCenter(allPills[1], modifiers, cwc.window);
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ 2,
+ "Two pills currently selected"
+ );
+
+ let popupPromise3 = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ let pill2 = allPills[2];
+ // Right click on the thirds pill, which should be selected, to select it
+ // while opening the context menu and deselecting the other two pills.
+ EventUtils.synthesizeMouseAtCenter(
+ pill2,
+ { type: "contextmenu" },
+ pill2.ownerGlobal
+ );
+ await popupPromise3;
+
+ // Only one pills should be selected
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ 1,
+ "One pill currently selected"
+ );
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills()[0],
+ allPills[2],
+ "The third pill was selected"
+ );
+ close_popup(cwc, contextMenu);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Test the correct behavior of the pill context menu items to edit, remove, and
+ * move the currently selected pills.
+ */
+add_task(async function test_pill_context_menu() {
+ let cwc = open_compose_new_mail();
+ setup_msg_contents(
+ cwc,
+ "test@example.org, test@invalid.foo, test@tinderborx.invalid, alice@foo.test",
+ "Testing recipient pills context menu!",
+ "Testing testing testing! "
+ );
+
+ let cDoc = cwc.window.document;
+ let recipientsContainer = cDoc.getElementById("recipientsContainer");
+ let allPills = recipientsContainer.getAllPills();
+
+ Assert.equal(allPills.length, 4, "Pills correctly created");
+
+ let contextMenu = cDoc.getElementById("emailAddressPillPopup");
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ // Right click on the first pill to open the context menu.
+ let pill = allPills[0];
+ EventUtils.synthesizeMouseAtCenter(
+ pill,
+ { type: "contextmenu" },
+ pill.ownerGlobal
+ );
+ await popupPromise;
+ // The selection should not have changed.
+ Assert.equal(
+ recipientsContainer.getAllSelectedPills().length,
+ 1,
+ "The first pill was selected"
+ );
+
+ let pillMoved = BrowserTestUtils.waitForCondition(
+ () =>
+ cDoc.querySelectorAll("#ccAddrContainer mail-address-pill").length == 1,
+ "Timeout waiting for the pill to be moved to the Cc field"
+ );
+
+ let movePillCc = contextMenu.querySelector("#moveAddressPillCc");
+ // Move the pill to the Cc field.
+ if (AppConstants.platform == "macosx") {
+ // We need to use click() since the synthesizeMouseAtCenter doesn't work for
+ // context menu items on macos.
+ movePillCc.click();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(movePillCc, {}, movePillCc.ownerGlobal);
+ }
+ await pillMoved;
+
+ close_popup(cwc, contextMenu);
+
+ let ccContainer = cDoc.getElementById("ccAddrContainer");
+ let ccPill = ccContainer.querySelector("mail-address-pill");
+
+ // Assert the pill was moved to the Cc filed and it's still selected.
+ Assert.equal(
+ ccPill.fullAddress,
+ allPills[0].fullAddress,
+ "The first pill was moved to the Cc field"
+ );
+ Assert.ok(ccPill.hasAttribute("selected"), "The pill is selected");
+
+ let popupPromise2 = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ // Right click on the same pill to open the context menu.
+ EventUtils.synthesizeMouseAtCenter(
+ ccPill,
+ { type: "contextmenu" },
+ ccPill.ownerGlobal
+ );
+ await popupPromise2;
+
+ let pillMoved2 = BrowserTestUtils.waitForCondition(
+ () =>
+ cDoc.querySelectorAll("#bccAddrContainer mail-address-pill").length == 1,
+ "Timeout waiting for the pill to be moved to the Bcc field"
+ );
+
+ // Move the pill to the Bcc field.
+ let moveAdd = contextMenu.querySelector("#moveAddressPillBcc");
+ if (AppConstants.platform == "macosx") {
+ // We need to use click() since the synthesizeMouseAtCenter doesn't work for
+ // context menu items on macos.
+ moveAdd.click();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(moveAdd, {}, moveAdd.ownerGlobal);
+ }
+ await pillMoved2;
+
+ close_popup(cwc, contextMenu);
+
+ let bccContainer = cDoc.getElementById("bccAddrContainer");
+ let bccPill = bccContainer.querySelector("mail-address-pill");
+
+ // Assert the pill was moved to the Cc filed and it's still selected.
+ Assert.equal(
+ bccPill.fullAddress,
+ allPills[0].fullAddress,
+ "The first pill was moved to the Bcc field"
+ );
+ Assert.ok(bccPill.hasAttribute("selected"), "The pill is selected");
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_redirect.js b/comm/mail/test/browser/composition/browser_redirect.js
new file mode 100644
index 0000000000..9375f3925e
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_redirect.js
@@ -0,0 +1,212 @@
+/* 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/. */
+
+/**
+ * Tests that the message redirect works as it should
+ */
+
+"use strict";
+
+var { async_wait_for_compose_window, close_compose_window } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+var { async_plan_for_new_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ get_about_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var folder;
+var i = 0;
+
+var myEmail = "me@example.com";
+var myEmail2 = "otherme@example.com";
+
+var identity;
+var identity2;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_setup(function () {
+ // Now set up an account with some identities.
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "Redirect Addresses Testing",
+ "pop3"
+ );
+
+ folder = account.incomingServer.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("Msgs4Redirect");
+
+ identity = MailServices.accounts.createIdentity();
+ identity.email = myEmail;
+ account.addIdentity(identity);
+
+ identity2 = MailServices.accounts.createIdentity();
+ identity2.email = myEmail2;
+ account.addIdentity(identity2);
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ // Let's add messages to the folder later as we go, it's hard to read
+ // out of context what the expected results should be.
+});
+
+/**
+ * Helper to check that the compose window has the expected address fields.
+ */
+function checkAddresses(win, expectedFields) {
+ let rows = win.document.querySelectorAll(
+ "#recipientsContainer .address-row:not(.hidden)"
+ );
+
+ let obtainedFields = [];
+ for (let row of rows) {
+ let addresses = [];
+ for (let pill of row.querySelectorAll("mail-address-pill")) {
+ addresses.push(pill.fullAddress);
+ }
+
+ obtainedFields[row.dataset.recipienttype] = addresses;
+ }
+
+ // Check what we expect is there.
+ for (let type in expectedFields) {
+ let expected = expectedFields[type];
+ let obtained = obtainedFields[type];
+
+ for (let i = 0; i < expected.length; i++) {
+ if (!obtained || !obtained.includes(expected[i])) {
+ throw new Error(
+ expected[i] +
+ " is not in " +
+ type +
+ " fields; " +
+ "obtained=" +
+ obtained
+ );
+ }
+ }
+ Assert.equal(
+ obtained.length,
+ expected.length,
+ "Unexpected number of fields obtained for type=" +
+ type +
+ "; obtained=" +
+ obtained +
+ "; expected=" +
+ expected
+ );
+ }
+
+ // Check there's no "extra" fields either.
+ for (let type in obtainedFields) {
+ let expected = expectedFields[type];
+ let obtained = obtainedFields[type];
+ if (!expected) {
+ throw new Error(
+ "Didn't expect a field for type=" + type + "; obtained=" + obtained
+ );
+ }
+ }
+
+ // Check if the input "aria-label" attribute was properly updated.
+ for (let row of rows) {
+ let addrLabel = row.querySelector(".address-label-container > label").value;
+ let addrTextbox = row.querySelector(".address-row-input");
+ let ariaLabel = addrTextbox.getAttribute("aria-label");
+ let pillCount = row.querySelectorAll("mail-address-pill").length;
+
+ switch (pillCount) {
+ case 0:
+ Assert.equal(ariaLabel, addrLabel);
+ break;
+ case 1:
+ Assert.equal(
+ ariaLabel,
+ addrLabel + " with one address, use left arrow key to focus on it."
+ );
+ break;
+ default:
+ Assert.equal(
+ ariaLabel,
+ addrLabel +
+ " with " +
+ pillCount +
+ " addresses, use left arrow key to focus on them."
+ );
+ break;
+ }
+ }
+}
+
+/**
+ * Tests that addresses get set properly when doing a redirect to a mail
+ * w/ Reply-To.
+ */
+add_task(async function testRedirectToMe() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: myEmail2,
+ cc: "Lisa <lisa@example.com>",
+ subject: "testRedirectToMe",
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ // Open Other Actions.
+ let aboutMessage = get_about_message();
+ let otherActionsButton =
+ aboutMessage.document.getElementById("otherActionsButton");
+ EventUtils.synthesizeMouseAtCenter(otherActionsButton, {}, aboutMessage);
+ let otherActionsPopup =
+ aboutMessage.document.getElementById("otherActionsPopup");
+ let popupshown = BrowserTestUtils.waitForEvent(
+ otherActionsPopup,
+ "popupshown"
+ );
+ await popupshown;
+ info("otherActionsButton popup shown");
+
+ let compWinPromise = async_plan_for_new_window("msgcompose");
+ // Click the Redirect menu item
+ EventUtils.synthesizeMouseAtCenter(
+ otherActionsPopup.firstElementChild,
+ {},
+ aboutMessage
+ );
+ let cwc = await async_wait_for_compose_window(mc, compWinPromise);
+ Assert.equal(
+ cwc.window.getCurrentIdentityKey(),
+ identity2.key,
+ "should be from second identity"
+ );
+ checkAddresses(
+ cwc.window,
+ // What would go into a reply should now be in Reply-To
+ {
+ addr_to: [], // empty
+ addr_reply: ["Homer <homer@example.com>"],
+ }
+ );
+ close_compose_window(cwc, false);
+});
diff --git a/comm/mail/test/browser/composition/browser_remove_text_styling.js b/comm/mail/test/browser/composition/browser_remove_text_styling.js
new file mode 100644
index 0000000000..f1be88b95d
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_remove_text_styling.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+/**
+ * Test removing styling from messages.
+ */
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_remove_text_styling() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ const NO_SIZE = formatHelper.NO_SIZE;
+
+ let removeButton = formatHelper.removeStylingButton;
+ let removeItem = formatHelper.removeStylingMenuItem;
+
+ // Before focus.
+ Assert.ok(
+ removeButton.disabled,
+ "Remove button should be disabled before focus"
+ );
+
+ formatHelper.focusMessage();
+
+ Assert.ok(
+ !removeButton.disabled,
+ "Remove button should be enabled after focus"
+ );
+
+ async function assertShown(styleSet, font, size, color, state, message) {
+ await formatHelper.assertShownStyles(styleSet, message);
+ await formatHelper.assertShownFont(font, message);
+ await formatHelper.assertShownSize(size, message);
+ await formatHelper.assertShownColor(color, message);
+ await formatHelper.assertShownParagraphState(state, message);
+ }
+
+ let styleSet = [
+ formatHelper.styleDataMap.get("underline"),
+ formatHelper.styleDataMap.get("superscript"),
+ formatHelper.styleDataMap.get("strong"),
+ ];
+ let tags = new Set();
+ styleSet.forEach(style => tags.add(style.tag));
+
+ let color = { value: "#0000ff", rgb: [0, 0, 255] };
+ let font = formatHelper.commonFonts[0];
+ let size = 4;
+
+ // In paragraph state.
+
+ for (let style of styleSet) {
+ await formatHelper.selectStyle(style);
+ }
+ await formatHelper.selectColor(color.value);
+ await formatHelper.selectFont(font);
+ await formatHelper.selectSize(size);
+
+ let text = "some text to apply styling to";
+ await formatHelper.typeInMessage(text);
+
+ formatHelper.assertMessageParagraph(
+ [{ tags, color: color.value, font, size, text }],
+ "Initial styled text"
+ );
+ await assertShown(styleSet, font, size, color, "p", "Set styling and typing");
+
+ removeButton.click();
+ await assertShown(null, "", NO_SIZE, "", "p", "Clicked to stop style");
+
+ let moreText = " without any styling";
+ await formatHelper.typeInMessage(moreText);
+ await assertShown(null, "", NO_SIZE, "", "p", "Typing with no styling");
+
+ formatHelper.assertMessageParagraph(
+ [{ tags, color: color.value, font, size, text }, moreText],
+ "Unstyled at end"
+ );
+
+ // Initialize some styling for the next typed character.
+ // Don't select any Text Styles because of Bug 1716840.
+ // for (let style of styleSet) {
+ // await formatHelper.selectStyle(style);
+ // }
+ await formatHelper.selectColor(color.value);
+ await formatHelper.selectFont(font);
+ await formatHelper.selectSize(size);
+
+ await assertShown(null, font, size, color, "p", "Getting some styling ready");
+
+ // Select through menu.
+ await formatHelper.selectFromFormatMenu(removeItem);
+ await assertShown(null, "", NO_SIZE, "", "p", "Removed readied styling");
+
+ await formatHelper.typeInMessage("a");
+ moreText += "a";
+ await assertShown(null, "", NO_SIZE, "", "p", "Still unstyled when typing");
+
+ formatHelper.assertMessageParagraph(
+ [{ tags, color: color.value, font, size, text }, moreText],
+ "Remains unstyled at end"
+ );
+
+ await formatHelper.selectTextRange(0, 3);
+ await assertShown(styleSet, font, size, color, "p", "Select start");
+
+ removeButton.click();
+ await assertShown(null, "", NO_SIZE, "", "p", "Selection is unstyled");
+ formatHelper.assertMessageParagraph(
+ [
+ text.slice(0, 3),
+ { tags, color: color.value, font, size, text: text.slice(3) },
+ moreText,
+ ],
+ "Becomes unstyled at start"
+ );
+
+ await formatHelper.selectTextRange(1, text.length + 3);
+ // Mixed selection
+ // See Bug 1718227 (the size menu does not respond to mixed selections)
+ // await assertShown(null, null, null, null, "p", "Select mixed");
+
+ // Select through menu.
+ await formatHelper.selectFromFormatMenu(removeItem);
+ await assertShown(null, "", NO_SIZE, "", "p", "Mixed selection now unstyled");
+ formatHelper.assertMessageParagraph(
+ [text + moreText],
+ "Style is fully stripped"
+ );
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_replyAddresses.js b/comm/mail/test/browser/composition/browser_replyAddresses.js
new file mode 100644
index 0000000000..0cf540297c
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replyAddresses.js
@@ -0,0 +1,1143 @@
+/* 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/. */
+
+/**
+ * Tests that we get correct adressees for different type of replies:
+ * reply to sender, reply to all, reply to list, mail-followup-tp,
+ * mail-reply-to, and reply to self.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ open_compose_with_reply,
+ open_compose_with_reply_to_all,
+ open_compose_with_reply_to_list,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var folder;
+var i = 0;
+
+var myEmail = "me@example.com";
+var myEmail2 = "otherme@example.com";
+
+var identity;
+var identity2;
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_setup(function () {
+ requestLongerTimeout(4);
+
+ // Now set up an account with some identities.
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "Reply Addresses Testing",
+ "pop3"
+ );
+
+ folder = account.incomingServer.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("Msgs4Reply");
+
+ identity = MailServices.accounts.createIdentity();
+ identity.email = myEmail;
+ account.addIdentity(identity);
+
+ identity2 = MailServices.accounts.createIdentity();
+ identity2.email = myEmail2;
+ account.addIdentity(identity2);
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ });
+
+ // Let's add messages to the folder later as we go, it's hard to read
+ // out of context what the expected results should be.
+});
+
+/**
+ * Helper to open a reply, check the fields are as expected, and close the
+ * reply window.
+ *
+ * @param aReplyFunction which reply function to call
+ * @param aExpectedFields the fields expected
+ */
+function checkReply(aReplyFunction, aExpectedFields) {
+ let rwc = aReplyFunction();
+ checkToAddresses(rwc, aExpectedFields);
+ close_compose_window(rwc);
+}
+
+/**
+ * Helper to check that the reply window has the expected address fields.
+ */
+function checkToAddresses(replyWinController, expectedFields) {
+ let rows = replyWinController.window.document.querySelectorAll(
+ "#recipientsContainer .address-row:not(.hidden)"
+ );
+
+ let obtainedFields = [];
+ for (let row of rows) {
+ let addresses = [];
+ for (let pill of row.querySelectorAll("mail-address-pill")) {
+ addresses.push(pill.fullAddress);
+ }
+
+ obtainedFields[row.dataset.recipienttype] = addresses;
+ }
+
+ // Check what we expect is there.
+ for (let type in expectedFields) {
+ let expected = expectedFields[type];
+ let obtained = obtainedFields[type];
+
+ for (let i = 0; i < expected.length; i++) {
+ if (!obtained || !obtained.includes(expected[i])) {
+ throw new Error(
+ expected[i] +
+ " is not in " +
+ type +
+ " fields; " +
+ "obtained=" +
+ obtained
+ );
+ }
+ }
+ Assert.equal(
+ obtained.length,
+ expected.length,
+ "Unexpected number of fields obtained for type=" +
+ type +
+ "; obtained=" +
+ obtained +
+ "; expected=" +
+ expected
+ );
+ }
+
+ // Check there's no "extra" fields either.
+ for (let type in obtainedFields) {
+ let expected = expectedFields[type];
+ let obtained = obtainedFields[type];
+ if (!expected) {
+ throw new Error(
+ "Didn't expect a field for type=" + type + "; obtained=" + obtained
+ );
+ }
+ }
+
+ // Check if the input "aria-label" attribute was properly updated.
+ for (let row of rows) {
+ let addrLabel = row.querySelector(".address-label-container > label").value;
+ let addrTextbox = row.querySelector(".address-row-input");
+ let ariaLabel = addrTextbox.getAttribute("aria-label");
+ let pillCount = row.querySelectorAll("mail-address-pill").length;
+
+ switch (pillCount) {
+ case 0:
+ Assert.equal(ariaLabel, addrLabel);
+ break;
+ case 1:
+ Assert.equal(
+ ariaLabel,
+ addrLabel + " with one address, use left arrow key to focus on it."
+ );
+ break;
+ default:
+ Assert.equal(
+ ariaLabel,
+ addrLabel +
+ " with " +
+ pillCount +
+ " addresses, use left arrow key to focus on them."
+ );
+ break;
+ }
+ }
+}
+
+/**
+ * Helper to set an auto-Cc list for an identity.
+ */
+function useAutoCc(aIdentity, aCcList) {
+ aIdentity.doCc = true;
+ aIdentity.doCcList = aCcList;
+}
+
+/**
+ * Helper to stop using auto-Cc for an identity.
+ */
+function stopUsingAutoCc(aIdentity) {
+ aIdentity.doCc = false;
+ aIdentity.doCcList = "";
+}
+
+/**
+ * Helper to ensure autoCc is turned off.
+ */
+function ensureNoAutoCc(aIdentity) {
+ aIdentity.doCc = false;
+}
+
+/**
+ * Helper to set an auto-bcc list for an identity.
+ */
+function useAutoBcc(aIdentity, aBccList) {
+ aIdentity.doBcc = true;
+ aIdentity.doBccList = aBccList;
+}
+
+/**
+ * Helper to stop using auto-bcc for an identity.
+ */
+function stopUsingAutoBcc(aIdentity) {
+ aIdentity.doBcc = false;
+ aIdentity.doBccList = "";
+}
+
+/**
+ * Helper to ensure auto-bcc is turned off.
+ */
+function ensureNoAutoBcc(aIdentity) {
+ aIdentity.doBcc = false;
+}
+
+/**
+ * Tests that for a list post with munged Reply-To:
+ * - reply: goes to From
+ * - reply all: includes From + the usual thing
+ * - reply list: goes to the list
+ */
+add_task(async function testReplyToMungedReplyToList() {
+ let msg0 = create_message({
+ from: "Tester <test@example.com>",
+ to: "munged.list@example.com, someone.else@example.com",
+ subject: "testReplyToMungedReplyToList",
+ clobberHeaders: {
+ "Reply-To": "Munged List <munged.list@example.com>",
+ "List-Post": "<mailto:munged.list@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+
+ checkReply(open_compose_with_reply, {
+ addr_to: ["Tester <test@example.com>"],
+ });
+
+ checkReply(open_compose_with_reply_to_all, {
+ addr_to: [
+ "Munged List <munged.list@example.com>",
+ "someone.else@example.com",
+ "Tester <test@example.com>",
+ ],
+ });
+
+ checkReply(open_compose_with_reply_to_list, {
+ addr_to: ["munged.list@example.com"],
+ });
+});
+
+/**
+ * Tests that addresses get set properly when doing a normal reply.
+ */
+add_task(async function testToCcReply() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "Mr Burns <mrburns@example.com>, workers@example.com, " + myEmail,
+ cc: "Lisa <lisa@example.com>",
+ subject: "testToCcReply - normal mail with to and cc (me in To)",
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: From
+ { addr_to: ["Homer <homer@example.com>"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply,
+ // To: From
+ // Cc: identity Cc list, including self.
+ {
+ addr_to: ["Homer <homer@example.com>"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a normal reply to all.
+ */
+add_task(async function testToCcReplyAll() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "Mr Burns <mrburns@example.com>, workers@example.com, " + myEmail,
+ cc: "Lisa <lisa@example.com>",
+ subject: "testToCcReplyAll - normal mail with to and cc (me in To)",
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + Tos without me.
+ // Cc: original Ccs
+ {
+ addr_to: [
+ "Homer <homer@example.com>",
+ "Mr Burns <mrburns@example.com>",
+ "workers@example.com",
+ ],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + Tos without me.
+ // Cc: original Ccs + auto-Ccs
+ {
+ addr_to: [
+ "Homer <homer@example.com>",
+ "Mr Burns <mrburns@example.com>",
+ "workers@example.com",
+ ],
+ addr_cc: ["Lisa <lisa@example.com>", myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that that addresses get set properly when doing a normal reply to all
+ * where when recipients aren't all ascii.
+ */
+add_task(async function testToCcReplyAllInternational() {
+ let msg0 = create_message({
+ from: "Hideaki / =?iso-2022-jp?B?GyRCNUhGIzFRTEAbKEI=?= <hideaki@example.com>",
+ to:
+ "Mr Burns <mrburns@example.com>, =?UTF-8?B?w4VrZQ==?= <ake@example.com>, " +
+ "=?KOI8-R?Q?=E9=D7=C1=CE?= <ivan@example.com>, " +
+ myEmail,
+ cc: "=?Big5?B?pP2oca1e?= <xiuying@example.com>",
+ subject:
+ "testToCcReplyAllInternational - non-ascii people mail with to and cc (me in To)",
+ clobberHeaders: { "Content-Transfer-Encoding": "quoted-printable" },
+ // Content-Transfer-Encoding ^^^ should be set from the body encoding below,
+ // but that doesn't seem to work. (No Content-Transfer-Encoding header is
+ // generated).
+ body: {
+ charset: "windows-1251",
+ encoding: "quoted-printable",
+ body: "=CF=F0=E8=E2=E5=F2 =E8=E7 =CC=EE=F1=EA=E2=FB",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + Tos without me.
+ // Cc: original Ccs
+ {
+ addr_to: [
+ "Hideaki / 蜷芽陸闍ア譏 <hideaki@example.com>",
+ "Mr Burns <mrburns@example.com>",
+ "テke <ake@example.com>",
+ "ミ侑イミーミス <ivan@example.com>",
+ ],
+ addr_cc: ["邇狗ァ闍ア <xiuying@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, "テsa <asa@example.com>");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + Tos without me.
+ // Cc: original Ccs + auto-Ccs
+ {
+ addr_to: [
+ "Hideaki / 蜷芽陸闍ア譏 <hideaki@example.com>",
+ "Mr Burns <mrburns@example.com>",
+ "テke <ake@example.com>",
+ "ミ侑イミーミス <ivan@example.com>",
+ ],
+ addr_cc: ["邇狗ァ闍ア <xiuying@example.com>", "テsa <asa@example.com>"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that that addresses get set properly when doing a reply to a mail with
+ * reply-to set.
+ */
+add_task(async function testToCcReplyWhenReplyToSet() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers@example.com",
+ cc: "Lisa <lisa@example.com>, " + myEmail,
+ subject:
+ "testToCcReplyWhenReplyToSet - to/cc mail with reply-to set (me in Cc)",
+ clobberHeaders: {
+ "Reply-To": "marge@example.com",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: reply-to
+ { addr_to: ["marge@example.com"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply,
+ // To: reply-to
+ // Cc: auto-Ccs
+ {
+ addr_to: ["marge@example.com"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply to all for a mail
+ * w/ Reply-To.
+ */
+add_task(async function testToCcReplyAllWhenReplyToSet() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers@example.com",
+ cc: "Lisa <lisa@example.com>, " + myEmail,
+ subject:
+ "testToCcReplyAllWhenReplyToSet - to/cc mail with reply-to set (me in Cc)",
+ clobberHeaders: {
+ "Reply-To": "marge@example.com",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Reply-To + Tos
+ // Cc: original Ccs without me.
+ {
+ addr_to: ["marge@example.com", "workers@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Reply-To + Tos
+ // Cc: original Ccs + auto-Ccs (which includes me!)
+ {
+ addr_to: ["marge@example.com", "workers@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>", myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply to list.
+ */
+add_task(async function testReplyToList() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com",
+ cc: "Lisa <lisa@example.com>, " + myEmail,
+ subject: "testReplyToList - mailing list message (me in Cc)",
+ clobberHeaders: {
+ "List-Post": "<mailto:workers-list@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_list,
+ // To: the list
+ { addr_to: ["workers-list@example.com"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_list,
+ // To: the list
+ // Cc: auto-Ccs
+ {
+ addr_to: ["workers-list@example.com"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply to sender for a
+ * list post.
+ */
+add_task(async function testReplySenderForListPost() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com",
+ cc: "Lisa <lisa@example.com>, " + myEmail,
+ subject: "testReplySenderForListPost - mailing list message (me in Cc)",
+ clobberHeaders: {
+ "List-Post": "<mailto:workers-list@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: From
+ { addr_to: ["Homer <homer@example.com>"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply,
+ // To: From
+ // Cc: auto-Ccs
+ {
+ addr_to: ["Homer <homer@example.com>"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply all to a list post.
+ */
+add_task(async function testReplyToAllForListPost() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com",
+ cc: "Lisa <lisa@example.com>, " + myEmail,
+ subject: "testReplyToAllForListPost - mailing list message (me in Cc)",
+ clobberHeaders: {
+ "List-Post": "<mailto:workers-list@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + original To
+ // Cc: original CC without me
+ {
+ addr_to: ["Homer <homer@example.com>", "workers-list@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + original To
+ // Cc: original CC + auto-Ccs (including me!)
+ {
+ addr_to: ["Homer <homer@example.com>", "workers-list@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>", myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply to all for a list
+ * post when also reply-to is set.
+ */
+add_task(async function testReplyToListWhenReplyToSet() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com, " + myEmail,
+ cc: "Lisa <lisa@example.com>",
+ subject:
+ "testReplyToListWhenReplyToSet - mailing list message w/ cc, reply-to (me in To)",
+ clobberHeaders: {
+ "Reply-To": "marge@example.com",
+ "List-Post": "<mailto:workers-list@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Reply-To, original Tos
+ // Cc: original Cc
+ {
+ addr_to: ["marge@example.com", "workers-list@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Reply-To, original Tos
+ // Cc: original Cc + auto-Ccs
+ {
+ addr_to: ["marge@example.com", "workers-list@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>", myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Test that addresses get set properly for Mail-Reply-To. Mail-Reply-To should
+ * be used for reply to author, if present.
+ *
+ * @see http://cr.yp.to/proto/replyto.html
+ */
+add_task(async function testMailReplyTo() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com",
+ cc: "Lisa <lisa@example.com>",
+ subject: "testMailReplyTo - mail with Mail-Reply-To header",
+ clobberHeaders: {
+ "Reply-To": "workers-list@example.com", // reply-to munging
+ "Mail-Reply-To": "Homer S. <homer@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: Mail-Reply-To
+ { addr_to: ["Homer S. <homer@example.com>"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply,
+ // To: Mail-Reply-To
+ // Cc: auto-Ccs
+ {
+ addr_to: ["Homer S. <homer@example.com>"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Test that addresses get set properly Mail-Followup-To. Mail-Followup-To
+ * should be the default recipient list for reply-all, if present.
+ *
+ * @see http://cr.yp.to/proto/replyto.html
+ */
+add_task(async function testMailFollowupTo() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "workers-list@example.com, " + myEmail,
+ cc: "Lisa <lisa@example.com>",
+ subject: "testMailFollowupTo - mail with Mail-Followup-To header",
+ clobberHeaders: {
+ // Homer is on the list, and don't want extra copies, so he has
+ // set the Mail-Followup-To header so followups go to the list.
+ "Mail-Followup-To": "workers-list@example.com",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Mail-Followup-To
+ { addr_to: ["workers-list@example.com"] }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: Mail-Followup-To
+ // Cc: auto-Ccs
+ {
+ addr_to: ["workers-list@example.com"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly for reply to self.
+ */
+add_task(async function testReplyToSelfReply() {
+ let msg0 = create_message({
+ // Upper case just to make sure we don't care about case sensitivity.
+ from: myEmail.toUpperCase(),
+ to: "Bart <bart@example.com>, Maggie <maggie@example.com>",
+ cc: "Lisa <lisa@example.com>",
+ subject: "testReplyToSelfReply - reply to self",
+ clobberHeaders: {
+ Bcc: "Moe <moe@example.com>",
+ "Reply-To": "Flanders <flanders@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: original To
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply,
+ // To: original To
+ // Cc: auto-Ccs
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly for a reply all to self - this should
+ * be treated as a followup.
+ */
+add_task(async function testReplyToSelfReplyAll() {
+ let msg0 = create_message({
+ from: myEmail,
+ to: "Bart <bart@example.com>, Maggie <maggie@example.com>",
+ cc: "Lisa <lisa@example.com>",
+ subject: "testReplyToSelfReplyAll - reply to self",
+ clobberHeaders: {
+ Bcc: "Moe <moe@example.com>",
+ "Reply-To": "Flanders <flanders@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc
+ // Bcc: original Bcc
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ addr_bcc: ["Moe <moe@example.com>"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ useAutoBcc(identity, "Lisa <lisa@example.com>");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc (auto-Ccs would have been included here already)
+ // Bcc: original Bcc
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ addr_bcc: ["Moe <moe@example.com>"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+ stopUsingAutoCc(identity);
+ stopUsingAutoBcc(identity);
+});
+
+/**
+ * Tests that addresses get set properly for a reply all to self - but for a
+ * message that is not really the original sent message. Like an auto-bcc:d copy
+ * or from Gmail. This should be treated as a followup.
+ */
+add_task(async function testReplyToSelfNotOriginalSourceMsgReplyAll() {
+ let msg0 = create_message({
+ from: myEmail2,
+ to: "Bart <bart@example.com>, Maggie <maggie@example.com>",
+ cc: "Lisa <lisa@example.com>",
+ subject: "testReplyToSelfNotOriginalSourceMsgReplyAll - reply to self",
+ clobberHeaders: {
+ "Reply-To": "Flanders <flanders@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity2);
+ useAutoBcc(identity2, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc
+ // Bcc: auto-bccs
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ addr_bcc: [myEmail, "smithers@example.com"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+ stopUsingAutoBcc(identity2);
+
+ useAutoCc(identity2, myEmail + ", smithers@example.com");
+ useAutoBcc(identity2, "moe@example.com,bart@example.com,lisa@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc (auto-Ccs would have been included here already)
+ // Bcc: auto-bcc minus addresses already in To/Cc
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: ["Lisa <lisa@example.com>", myEmail, "smithers@example.com"],
+ addr_bcc: ["moe@example.com"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+ stopUsingAutoCc(identity2);
+ stopUsingAutoBcc(identity2);
+
+ useAutoBcc(identity2, myEmail2 + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc (auto-Ccs would have been included here already)
+ // Bcc: auto-bccs
+ // Reply-To: original Reply-To
+ {
+ addr_to: ["Bart <bart@example.com>", "Maggie <maggie@example.com>"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ addr_bcc: [myEmail2, "smithers@example.com"],
+ addr_reply: ["Flanders <flanders@example.com>"],
+ }
+ );
+ stopUsingAutoBcc(identity2);
+});
+
+/**
+ * Tests that a reply to an other identity isn't treated as a reply to self
+ * followup.
+ */
+add_task(async function testReplyToOtherIdentity() {
+ let msg0 = create_message({
+ from: myEmail,
+ to: myEmail2 + ", barney@example.com",
+ cc: "Lisa <lisa@example.com>",
+ subject: "testReplyToOtherIdentity - reply to other identity",
+ clobberHeaders: {
+ "Reply-To": "secretary@example.com",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity2);
+ ensureNoAutoBcc(identity2);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: from + to (except me2)
+ // Cc: original Cc
+ //
+ {
+ addr_to: ["secretary@example.com", "barney@example.com"],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ }
+ );
+});
+
+/**
+ * Tests that addresses get set properly for a reply all to self w/ bccs -
+ * this should be treated as a followup.
+ */
+add_task(async function testReplyToSelfWithBccs() {
+ let msg0 = create_message({
+ from: myEmail,
+ to: myEmail,
+ cc: myEmail2 + ", Lisa <lisa@example.com>",
+ subject: "testReplyToSelfWithBccs - reply to self",
+ clobberHeaders: {
+ Bcc: "Moe <moe@example.com>, Barney <barney@example.com>",
+ "Reply-To": myEmail2,
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc
+ // Bcc: original Bcc
+ // Reply-To: original Reply-To
+ {
+ addr_to: [myEmail],
+ addr_cc: [myEmail2, "Lisa <lisa@example.com>"],
+ addr_bcc: ["Moe <moe@example.com>", "Barney <barney@example.com>"],
+ addr_reply: [myEmail2],
+ }
+ );
+});
+
+/**
+ * Tests that addresses get set properly for a reply all to other identity w/ bccs -
+ * this be treated as a followup.
+ */
+add_task(async function testReplyToOtherIdentityWithBccs() {
+ let msg0 = create_message({
+ from: myEmail,
+ to: myEmail2,
+ cc: "Lisa <lisa@example.com>",
+ subject: "testReplyToOtherIdentityWithBccs - reply to other identity",
+ clobberHeaders: {
+ Bcc: "Moe <moe@example.com>, Barney <barney@example.com>",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: original To
+ // Cc: original Cc
+ // Bcc: original Bcc
+ {
+ addr_to: [myEmail2],
+ addr_cc: ["Lisa <lisa@example.com>"],
+ addr_bcc: ["Moe <moe@example.com>", "Barney <barney@example.com>"],
+ }
+ );
+});
+
+/**
+ * Tests that addresses get set properly for a nntp reply-all.
+ */
+add_task(async function testNewsgroupsReplyAll() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "test1-list@example.org",
+ subject: "testNewsgroupsReplyAll - sent to two newsgroups and a list",
+ clobberHeaders: {
+ Newsgroups: "example.test1, example.test2",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From, original To
+ // Newsgroups: original Ccs
+ {
+ addr_to: ["Homer <homer@example.com>", "test1-list@example.org"],
+ addr_newsgroups: ["example.test1", "example.test2"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From, original To
+ // Newsgroups: original Ccs
+ {
+ addr_to: ["Homer <homer@example.com>", "test1-list@example.org"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ addr_newsgroups: ["example.test1", "example.test2"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly for an nntp followup, when Followup-To
+ * is set.
+ */
+add_task(async function testNewsgroupsReplyAllFollowupTo() {
+ let msg0 = create_message({
+ from: "Homer <homer@example.com>",
+ to: "test1-list@example.org, " + myEmail,
+ subject: "testNewsgroupsReplyAllFollowupTo - Followup-To set",
+ clobberHeaders: {
+ Newsgroups: "example.test1, example.test2",
+ "Followup-To": "example.test2",
+ },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + original To (except me)
+ // Newsgroups: <Followup-To>
+ {
+ addr_to: ["Homer <homer@example.com>", "test1-list@example.org"],
+ addr_newsgroups: ["example.test2"],
+ }
+ );
+
+ useAutoCc(identity, myEmail + ", smithers@example.com");
+ checkReply(
+ open_compose_with_reply_to_all,
+ // To: From + original To (except me)
+ // Cc: auto-Ccs
+ // Newsgroups: <Followup-To>
+ {
+ addr_to: ["Homer <homer@example.com>", "test1-list@example.org"],
+ addr_cc: [myEmail, "smithers@example.com"],
+ addr_newsgroups: ["example.test2"],
+ }
+ );
+ stopUsingAutoCc(identity);
+});
+
+/**
+ * Tests that addresses get set properly when doing a reply where To=From
+ * and a Reply-To exists.
+ */
+add_task(async function testToFromWithReplyTo() {
+ let msg0 = create_message({
+ from: myEmail,
+ to: myEmail,
+ subject: "testToFromWithReplyTo - To=From w/ Reply-To set",
+ clobberHeaders: { "Reply-To": "Flanders <flanders@example.com>" },
+ });
+ await add_message_to_folder([folder], msg0);
+
+ await be_in_folder(folder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+
+ ensureNoAutoCc(identity);
+ checkReply(
+ open_compose_with_reply,
+ // To: Reply-To
+ { addr_to: ["Flanders <flanders@example.com>"] }
+ );
+});
diff --git a/comm/mail/test/browser/composition/browser_replyCatchAll.js b/comm/mail/test/browser/composition/browser_replyCatchAll.js
new file mode 100644
index 0000000000..b53be9bd1c
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replyCatchAll.js
@@ -0,0 +1,271 @@
+/* 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/. */
+
+/**
+ * Tests that reply messages use the correct identity and sender dependent
+ * on the catchAll setting.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_reply } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_message,
+ mc,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { assert_notification_displayed, wait_for_notification_to_show } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+ );
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var i = 0;
+
+var id1Domain = "example.com";
+var id2Domain = "example.net";
+var myIdentityEmail1 = "me@example.com";
+var myIdentityEmail2 = "otherme@example.net";
+var envelopeToAddr = "envelope@example.net";
+var notMyEmail = "otherme@example.org";
+
+var identity1;
+var identity2;
+
+var gAccount;
+var gFolder;
+
+add_setup(function () {
+ requestLongerTimeout(4);
+
+ // Now set up an account with some identities.
+ gAccount = MailServices.accounts.createAccount();
+ gAccount.incomingServer = MailServices.accounts.createIncomingServer(
+ "nobody",
+ "Reply Identity Testing",
+ "pop3"
+ );
+
+ gFolder = gAccount.incomingServer.rootFolder
+ .QueryInterface(Ci.nsIMsgLocalMailFolder)
+ .createLocalSubfolder("Msgs4Reply");
+
+ identity1 = MailServices.accounts.createIdentity();
+ identity1.email = myIdentityEmail1;
+ gAccount.addIdentity(identity1);
+ info(`Added identity1; key=${identity1.key}, email=${identity1.email}`);
+
+ identity2 = MailServices.accounts.createIdentity();
+ identity2.email = myIdentityEmail2;
+ gAccount.addIdentity(identity2);
+ info(`Added identity2; key=${identity2.key}, email=${identity2.email}`);
+});
+
+/**
+ * Create and select a new message to do a reply with.
+ */
+async function create_replyMsg(aTo, aEnvelopeTo) {
+ let msg0 = create_message({
+ from: "Tester <test@example.com>",
+ to: aTo,
+ subject: "test",
+ clobberHeaders: {
+ "envelope-to": aEnvelopeTo,
+ },
+ });
+ await add_message_to_folder([gFolder], msg0);
+
+ await be_in_folder(gFolder);
+ let msg = select_click_row(i++);
+ assert_selected_and_displayed(mc, msg);
+}
+
+/**
+ * The tests.
+ */
+add_task(async function test_reply_identity_selection() {
+ let tests = [
+ {
+ desc: "No catchAll, 'From' will be set to recipient",
+ to: myIdentityEmail2,
+ envelopeTo: myIdentityEmail2,
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: false,
+ catchAllHintId2: "",
+ replyIdKey: identity2.key,
+ replyIdFrom: myIdentityEmail2,
+ warning: false,
+ },
+ {
+ desc: "No catchAll, 'From' will be set to second id's email (without name).",
+ to: "Mr.X <" + myIdentityEmail2 + ">",
+ envelopeTo: "",
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: false,
+ catchAllHintId2: "",
+ replyIdKey: identity2.key,
+ replyIdFrom: myIdentityEmail2,
+ warning: false,
+ },
+ {
+ desc: "With catchAll, 'From' will be set to senders address (with name).",
+ to: "Mr.X <" + myIdentityEmail2 + ">",
+ envelopeTo: "",
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: true,
+ catchAllHintId2: "*@" + id2Domain,
+ replyIdKey: identity2.key,
+ replyIdFrom: "Mr.X <" + myIdentityEmail2 + ">",
+ warning: false,
+ },
+ {
+ desc: "With catchAll #2, 'From' will be set to senders address (with name).",
+ to: myIdentityEmail2,
+ envelopeTo: "Mr.X <" + myIdentityEmail2 + ">",
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: true,
+ catchAllHintId2: "*@" + id2Domain,
+ replyIdKey: identity2.key,
+ replyIdFrom: "Mr.X <" + myIdentityEmail2 + ">",
+ warning: false,
+ },
+ {
+ desc: "With catchAll, 'From' will be set to second id's email.",
+ to: myIdentityEmail2,
+ envelopeTo: envelopeToAddr,
+ catchAllId1: false,
+ catchAllId2: true,
+ replyIdKey: identity2.key,
+ replyIdFrom: myIdentityEmail2,
+ warning: false,
+ },
+ {
+ desc: `With catchAll, 'From' will be set to ${envelopeToAddr}`,
+ to: notMyEmail,
+ envelopeTo: envelopeToAddr,
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: true,
+ catchAllHintId2: "*@" + id2Domain,
+ replyIdKey: identity2.key,
+ replyIdFrom: envelopeToAddr,
+ warning: true,
+ },
+ {
+ desc: "Without catchAll, mail to another recipient.",
+ to: notMyEmail,
+ envelopeTo: "",
+ catchAllId1: false,
+ catchAllHintId1: "",
+ catchAllId2: false,
+ catchAllHintId2: "",
+ replyIdKey: identity1.key,
+ replyIdFrom: myIdentityEmail1,
+ warning: false,
+ },
+ {
+ desc: " With catchAll, mail to another recipient (domain not matching).",
+ to: notMyEmail,
+ envelopeTo: "",
+ catchAllId1: true,
+ catchAllHintId1: "*@" + id1Domain,
+ catchAllId2: true,
+ catchAllHintId2: "*@" + id2Domain,
+ replyIdKey: identity1.key,
+ replyIdFrom: myIdentityEmail1,
+ warning: false,
+ },
+ ];
+
+ for (let test of tests) {
+ info(`Running test: ${test.desc}`);
+ test.replyIndex = await create_replyMsg(test.to, test.envelopeTo);
+
+ identity1.catchAll = test.catchAllId1;
+ identity1.catchAllHint = test.catchAllHintId1;
+ info(
+ `... identity1.catchAll=${identity1.catchAll}, identity1.catchAllHint=${identity1.catchAllHint}`
+ );
+
+ identity2.catchAll = test.catchAllId2;
+ identity2.catchAllHint = test.catchAllHintId2;
+ info(
+ `... identity2.catchAll=${identity2.catchAll}, identity2.catchAllHint=${identity2.catchAllHint}`
+ );
+
+ let cwc = open_compose_with_reply();
+
+ info("Checking reply identity: " + JSON.stringify(test, null, 2));
+ checkCompIdentity(cwc, test.replyIdKey, test.replyIdFrom);
+
+ if (test.warning) {
+ wait_for_notification_to_show(
+ cwc.window,
+ "compose-notification-bottom",
+ "identityWarning"
+ );
+ } else {
+ assert_notification_displayed(
+ cwc.window,
+ "compose-notification-bottom",
+ "identityWarning",
+ false
+ );
+ }
+
+ close_compose_window(cwc, false);
+ }
+});
+
+/**
+ * Helper to check that a suitable From identity was set up in the given
+ * composer window.
+ *
+ * @param cwc Compose window controller.
+ * @param aIdentityKey The key of the expected identity.
+ * @param aFrom The expected displayed From address.
+ */
+function checkCompIdentity(cwc, identityKey, from) {
+ Assert.equal(
+ cwc.window.document.getElementById("msgIdentity").value,
+ from,
+ "msgIdentity value should be as expected."
+ );
+ Assert.equal(
+ cwc.window.getCurrentIdentityKey(),
+ identityKey,
+ "The From identity should be correctly selected."
+ );
+}
+
+registerCleanupFunction(async function () {
+ await be_in_folder(gFolder);
+ while (gFolder.getTotalMessages(false) > 0) {
+ select_click_row(0);
+ press_delete();
+ }
+
+ gAccount.removeIdentity(identity2);
+
+ // The last identity of an account can't be removed so clear all its prefs
+ // which effectively destroys it.
+ identity1.clearAllValues();
+ MailServices.accounts.removeAccount(gAccount);
+ gAccount = null;
+});
diff --git a/comm/mail/test/browser/composition/browser_replyFormatFlowed.js b/comm/mail/test/browser/composition/browser_replyFormatFlowed.js
new file mode 100644
index 0000000000..8f4f286739
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replyFormatFlowed.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/**
+ * Tests that the reply to a format=flowed message is also flowed.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ get_msg_source,
+ open_compose_with_reply,
+ save_compose_message,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ be_in_folder,
+ get_special_folder,
+ get_about_message,
+ open_message_from_file,
+ press_delete,
+ select_click_row,
+ select_none,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDrafts;
+
+add_setup(async function () {
+ gDrafts = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+
+ Services.prefs.setBoolPref("mail.identity.id1.compose_html", false);
+});
+
+async function subtest_reply_format_flowed(aFlowed) {
+ let file = new FileUtils.File(getTestFilePath("data/format-flowed.eml"));
+ let msgc = await open_message_from_file(file);
+
+ Services.prefs.setBoolPref("mailnews.send_plaintext_flowed", aFlowed);
+
+ let cwc = open_compose_with_reply(msgc);
+
+ close_window(msgc);
+
+ // Now save the message as a draft.
+ await save_compose_message(cwc.window);
+ close_compose_window(cwc);
+
+ await TestUtils.waitForCondition(
+ () => gDrafts.getTotalMessages(false) == 1,
+ "message saved to drafts folder"
+ );
+
+ // Now check the message content in the drafts folder.
+ await be_in_folder(gDrafts);
+ let message = select_click_row(0);
+ let messageContent = await get_msg_source(message);
+
+ // Check for a single line that contains text and make sure there is a
+ // space at the end for a flowed reply.
+ Assert.ok(
+ messageContent.includes(
+ "\r\n> text text text text text text text text text text text text text text" +
+ (aFlowed ? " \r\n" : "\r\n")
+ ),
+ "Expected line not found in message."
+ );
+
+ // Delete the outgoing message.
+ press_delete();
+}
+
+add_task(async function test_reply_format_flowed() {
+ await subtest_reply_format_flowed(true);
+ await subtest_reply_format_flowed(false);
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("mail.identity.id1.compose_html");
+ Services.prefs.clearUserPref("mailnews.send_plaintext_flowed");
+});
diff --git a/comm/mail/test/browser/composition/browser_replyMultipartCharset.js b/comm/mail/test/browser/composition/browser_replyMultipartCharset.js
new file mode 100644
index 0000000000..e3e9982045
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replyMultipartCharset.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+/**
+ * This has become a "mixed bag" of tests for various bugs.
+ *
+ * Bug 1026989:
+ * Tests that the reply to a message picks up the charset from the body
+ * and not from an attachment. Also test "Edit as new", forward inline and
+ * forward as attachment.
+ *
+ * Bug 961983:
+ * Tests that UTF-16 is not used in a composition.
+ *
+ * Bug 1323377:
+ * Tests that the correct charset is used, even if the message
+ * wasn't viewed before answering/forwarding.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ open_compose_with_edit_as_new,
+ open_compose_with_forward,
+ open_compose_with_forward_as_attachments,
+ open_compose_with_reply,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ get_about_message,
+ mc,
+ open_message_from_file,
+ press_delete,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var folder;
+
+add_setup(async function () {
+ requestLongerTimeout(2);
+ folder = await create_folder("FolderWithMessages");
+});
+
+async function subtest_replyEditAsNewForward_charset(
+ aAction,
+ aFile,
+ aViewed = true
+) {
+ await be_in_folder(folder);
+
+ let file = new FileUtils.File(getTestFilePath(`data/${aFile}`));
+ let msgc = await open_message_from_file(file);
+
+ // Copy the message to a folder. We run the message through a folder
+ // since replying/editing as new/forwarding directly to the message
+ // opened from a file gives different results on different platforms.
+ // All platforms behave the same when using a folder-stored message.
+ let documentChild = msgc.window.content.document.documentElement;
+ EventUtils.synthesizeMouseAtCenter(
+ documentChild,
+ { type: "contextmenu", button: 2 },
+ documentChild.ownerGlobal
+ );
+ let aboutMessage = get_about_message(msgc.window);
+ await click_menus_in_sequence(
+ aboutMessage.document.getElementById("mailContext"),
+ [
+ { id: "mailContext-copyMenu" },
+ { label: "Local Folders" },
+ { label: "FolderWithMessages" },
+ ]
+ );
+ close_window(msgc);
+
+ let msg = select_click_row(0);
+ if (aViewed) {
+ // Only if the preview pane is on, we can check the following.
+ assert_selected_and_displayed(mc, msg);
+ }
+
+ let fwdWin;
+ switch (aAction) {
+ case 1: // Reply.
+ fwdWin = open_compose_with_reply();
+ break;
+ case 2: // Edit as new.
+ fwdWin = open_compose_with_edit_as_new();
+ break;
+ case 3: // Forward inline.
+ fwdWin = open_compose_with_forward();
+ break;
+ case 4: // Forward as attachment.
+ fwdWin = open_compose_with_forward_as_attachments();
+ break;
+ }
+
+ // Check the charset in the compose window.
+ let charset =
+ fwdWin.window.document.getElementById("messageEditor").contentDocument
+ .charset;
+ Assert.equal(charset, "UTF-8", "Compose window has the wrong charset");
+ close_compose_window(fwdWin);
+
+ press_delete(mc);
+}
+
+add_task(async function test_replyEditAsNewForward_charsetFromBody() {
+ // Check that the charset is taken from the message body (bug 1026989).
+ await subtest_replyEditAsNewForward_charset(1, "./multipart-charset.eml");
+ await subtest_replyEditAsNewForward_charset(2, "./multipart-charset.eml");
+ await subtest_replyEditAsNewForward_charset(3, "./multipart-charset.eml");
+ // For "forward as attachment" we use the default charset (which is UTF-8).
+ await subtest_replyEditAsNewForward_charset(4, "./multipart-charset.eml");
+});
+
+add_task(async function test_reply_noUTF16() {
+ // Check that a UTF-16 encoded e-mail is forced to UTF-8 when replying (bug 961983).
+ await subtest_replyEditAsNewForward_charset(1, "./body-utf16.eml", "UTF-8");
+});
+
+add_task(async function test_replyEditAsNewForward_noPreview() {
+ // Check that it works even if the message wasn't viewed before, so
+ // switch off the preview pane (bug 1323377).
+ await be_in_folder(folder);
+ mc.window.goDoCommand("cmd_toggleMessagePane");
+
+ await subtest_replyEditAsNewForward_charset(1, "./format-flowed.eml", false);
+ await subtest_replyEditAsNewForward_charset(2, "./body-greek.eml", false);
+ await subtest_replyEditAsNewForward_charset(
+ 3,
+ "./multipart-charset.eml",
+ false
+ );
+
+ mc.window.goDoCommand("cmd_toggleMessagePane");
+});
+
+registerCleanupFunction(() => {
+ folder.deleteSelf(null);
+});
diff --git a/comm/mail/test/browser/composition/browser_replySelection.js b/comm/mail/test/browser/composition/browser_replySelection.js
new file mode 100644
index 0000000000..0abdd55194
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replySelection.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+/**
+ * Tests that reply with selection works properly.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_reply } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var { get_about_message, open_message_from_file } = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { click_menus_in_sequence, close_window } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+add_task(async function test_reply_w_selection_direct() {
+ let file = new FileUtils.File(getTestFilePath("data/non-flowed-plain.eml"));
+ let msgc = await open_message_from_file(file);
+
+ let aboutMessage = get_about_message(msgc);
+ let win = aboutMessage.document.getElementById("messagepane").contentWindow;
+ let doc = aboutMessage.document.getElementById("messagepane").contentDocument;
+ let selection = win.getSelection();
+
+ let text = doc.querySelector("body > div.moz-text-plain > pre.moz-quote-pre");
+
+ // Lines 2-3 of the text.
+ let range1 = doc.createRange();
+ range1.setStart(text.firstChild, 6);
+ range1.setEnd(text.firstChild, 20);
+
+ // The <pre> node itself.
+ let range2 = doc.createRange();
+ range2.setStart(text, 0);
+ range2.setEnd(text, 1);
+
+ for (let range of [range1, range2]) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ let cwc = await open_compose_with_reply(msgc);
+ let blockquote = cwc.document
+ .getElementById("messageEditor")
+ .contentDocument.body.querySelector("blockquote");
+
+ Assert.ok(
+ blockquote.querySelector(":scope > pre"),
+ "the non-flowed content should be in a <pre>"
+ );
+
+ Assert.ok(
+ !blockquote.querySelector(":scope > pre").innerHTML.includes("<"),
+ "should be all text, no tags in the message text"
+ );
+ await close_compose_window(cwc);
+ }
+
+ await BrowserTestUtils.closeWindow(msgc);
+});
diff --git a/comm/mail/test/browser/composition/browser_replySignature.js b/comm/mail/test/browser/composition/browser_replySignature.js
new file mode 100644
index 0000000000..6053171468
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_replySignature.js
@@ -0,0 +1,117 @@
+/* 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/. */
+
+/**
+ * Tests the mail.strip_sig_on_reply pref.
+ */
+
+"use strict";
+
+var { close_compose_window, get_compose_body, open_compose_with_reply } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ create_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var sig = "roses are red";
+var folder;
+
+add_setup(async function () {
+ folder = await create_folder("SigStripTest");
+ registerCleanupFunction(() => folder.deleteSelf(null));
+
+ let msg = create_message({
+ subject: "msg with signature; format=flowed",
+ body: {
+ body:
+ "get with the flow! get with the flow! get with the flow! " +
+ "get with the \n flow! get with the flow!\n-- \n" +
+ sig +
+ "\n",
+ contentType: "text/plain",
+ charset: "UTF-8",
+ format: "flowed",
+ },
+ });
+ await add_message_to_folder([folder], msg);
+ let msg2 = create_message({
+ subject: "msg with signature; format not flowed",
+ body: {
+ body:
+ "not flowed! not flowed! not flowed! \n" +
+ "not flowed!\n-- \n" +
+ sig +
+ "\n",
+ contentType: "text/plain",
+ charset: "UTF-8",
+ format: "",
+ },
+ });
+ await add_message_to_folder([folder], msg2);
+});
+
+/** Test sig strip true for format flowed. */
+add_task(async function test_sig_strip_true_ff() {
+ Services.prefs.setBoolPref("mail.strip_sig_on_reply", true);
+ await check_sig_strip_works(0, true);
+ Services.prefs.clearUserPref("mail.strip_sig_on_reply");
+});
+
+/** Test sig strip false for format flowed. */
+add_task(async function test_sig_strip_false_ff() {
+ Services.prefs.setBoolPref("mail.strip_sig_on_reply", false);
+ await check_sig_strip_works(0, false);
+ Services.prefs.clearUserPref("mail.strip_sig_on_reply");
+});
+
+/** Test sig strip true for non-format flowed. */
+add_task(async function test_sig_strip_true_nonff() {
+ Services.prefs.setBoolPref("mail.strip_sig_on_reply", true);
+ await check_sig_strip_works(1, true);
+ Services.prefs.clearUserPref("mail.strip_sig_on_reply");
+});
+
+/** Test sig strip false for non-format flowed. */
+add_task(async function test_sig_strip_false_nonff() {
+ Services.prefs.setBoolPref("mail.strip_sig_on_reply", false);
+ await check_sig_strip_works(1, false);
+ Services.prefs.clearUserPref("mail.strip_sig_on_reply");
+});
+
+/**
+ * Helper function to check signature stripping works as it should.
+ *
+ * @param aRow the row index of the message to test
+ * @param aShouldStrip true if the signature should be stripped
+ */
+async function check_sig_strip_works(aRow, aShouldStrip) {
+ await be_in_folder(folder);
+ let msg = select_click_row(aRow);
+ assert_selected_and_displayed(mc, msg);
+
+ let rwc = open_compose_with_reply();
+ let body = get_compose_body(rwc);
+
+ if (aShouldStrip && body.textContent.includes(sig)) {
+ throw new Error("signature was not stripped; body=" + body.textContent);
+ } else if (!aShouldStrip && !body.textContent.includes(sig)) {
+ throw new Error("signature stripped; body=" + body.textContent);
+ }
+ close_compose_window(rwc);
+
+ Assert.report(
+ false,
+ undefined,
+ undefined,
+ "Test ran to completion successfully"
+ );
+}
diff --git a/comm/mail/test/browser/composition/browser_saveChangesOnQuit.js b/comm/mail/test/browser/composition/browser_saveChangesOnQuit.js
new file mode 100644
index 0000000000..6b9bc8c177
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_saveChangesOnQuit.js
@@ -0,0 +1,420 @@
+/* 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/. */
+
+/**
+ * Tests that we prompt the user if they'd like to save their message when they
+ * try to quit/close with an open compose window with unsaved changes, and
+ * that we don't prompt if there are no changes.
+ */
+
+"use strict";
+
+var {
+ close_compose_window,
+ open_compose_new_mail,
+ open_compose_with_forward,
+ open_compose_with_reply,
+ wait_for_compose_window,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ create_message,
+ get_about_message,
+ get_special_folder,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var { gMockPromptService } = ChromeUtils.import(
+ "resource://testing-common/mozmill/PromptHelpers.jsm"
+);
+var { wait_for_notification_to_show, get_notification } = ChromeUtils.import(
+ "resource://testing-common/mozmill/NotificationBoxHelpers.jsm"
+);
+var { plan_for_new_window, wait_for_window_focused } = ChromeUtils.import(
+ "resource://testing-common/mozmill/WindowHelpers.jsm"
+);
+
+var SAVE = 0;
+var CANCEL = 1;
+var DONT_SAVE = 2;
+
+var cwc = null; // compose window controller
+var folder = null;
+var gDraftFolder = null;
+
+add_setup(async function () {
+ requestLongerTimeout(3);
+
+ folder = await create_folder("PromptToSaveTest");
+
+ await add_message_to_folder([folder], create_message()); // row 0
+ let localFolder = folder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ localFolder.addMessage(msgSource("content type: text", "text")); // row 1
+ localFolder.addMessage(msgSource("content type missing", null)); // row 2
+ gDraftFolder = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+});
+
+function msgSource(aSubject, aContentType) {
+ let msgId = Services.uuid.generateUUID() + "@invalid";
+
+ return (
+ "From - Sun Apr 07 22:47:11 2013\r\n" +
+ "X-Mozilla-Status: 0001\r\n" +
+ "X-Mozilla-Status2: 00000000\r\n" +
+ "Message-ID: <" +
+ msgId +
+ ">\r\n" +
+ "Date: Sun, 07 Apr 2013 22:47:11 +0300\r\n" +
+ "From: Someone <some.one@invalid>\r\n" +
+ "To: someone.else@invalid\r\n" +
+ "Subject: " +
+ aSubject +
+ "\r\n" +
+ "MIME-Version: 1.0\r\n" +
+ (aContentType ? "Content-Type: " + aContentType + "\r\n" : "") +
+ "Content-Transfer-Encoding: 7bit\r\n\r\n" +
+ "A msg with contentType " +
+ aContentType +
+ "\r\n"
+ );
+}
+
+/**
+ * Test that when a compose window is open with changes, and
+ * a Quit is requested (for example, from File > Quit from the
+ * 3pane), that the user gets a confirmation dialog to discard
+ * the changes. This also tests that the user can cancel the
+ * quit request.
+ */
+add_task(function test_can_cancel_quit_on_changes() {
+ // Register the Mock Prompt Service
+ gMockPromptService.register();
+
+ // opening a new compose window
+ cwc = open_compose_new_mail(mc);
+
+ // Make some changes
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Hey check out this megalol link", cwc.window);
+
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+
+ // Set the Mock Prompt Service to return false, so that we
+ // cancel the quit.
+ gMockPromptService.returnValue = CANCEL;
+ // Trigger the quit-application-request notification
+
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ let promptState = gMockPromptService.promptState;
+ Assert.notEqual(null, promptState, "Expected a confirmEx prompt");
+
+ Assert.equal("confirmEx", promptState.method);
+ // Since we returned false on the confirmation dialog,
+ // we should be cancelling the quit - so cancelQuit.data
+ // should now be true
+ Assert.ok(cancelQuit.data, "Didn't cancel the quit");
+
+ close_compose_window(cwc);
+
+ // Unregister the Mock Prompt Service
+ gMockPromptService.unregister();
+});
+
+/**
+ * Test that when a compose window is open with changes, and
+ * a Quit is requested (for example, from File > Quit from the
+ * 3pane), that the user gets a confirmation dialog to discard
+ * the changes. This also tests that the user can let the quit
+ * occur.
+ */
+add_task(function test_can_quit_on_changes() {
+ // Register the Mock Prompt Service
+ gMockPromptService.register();
+
+ // opening a new compose window
+ cwc = open_compose_new_mail(mc);
+
+ // Make some changes
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Hey check out this megalol link", cwc.window);
+
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+
+ // Set the Mock Prompt Service to return true, so that we're
+ // allowing the quit to occur.
+ gMockPromptService.returnValue = DONT_SAVE;
+
+ // Trigger the quit-application-request notification
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ let promptState = gMockPromptService.promptState;
+ Assert.notEqual(null, promptState, "Expected a confirmEx prompt");
+
+ Assert.equal("confirmEx", promptState.method);
+ // Since we returned true on the confirmation dialog,
+ // we should be quitting - so cancelQuit.data should now be
+ // false
+ Assert.ok(!cancelQuit.data, "The quit request was cancelled");
+
+ close_compose_window(cwc);
+
+ // Unregister the Mock Prompt Service
+ gMockPromptService.unregister();
+});
+
+/**
+ * Bug 698077 - test that when quitting with two compose windows open, if
+ * one chooses "Don't Save", and the other chooses "Cancel", that the first
+ * window's state is such that subsequent quit requests still cause the
+ * Don't Save / Cancel / Save dialog to come up.
+ */
+add_task(async function test_window_quit_state_reset_on_aborted_quit() {
+ // Register the Mock Prompt Service
+ gMockPromptService.register();
+
+ // open two new compose windows
+ let cwc1 = open_compose_new_mail(mc);
+ let cwc2 = open_compose_new_mail(mc);
+
+ // Type something in each window.
+ cwc1.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Marco!", cwc1.window);
+
+ cwc2.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Polo!", cwc2.window);
+
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+
+ // This is a hacky method for making sure that the second window
+ // receives a CANCEL click in the popup dialog.
+ var numOfPrompts = 0;
+ gMockPromptService.onPromptCallback = function () {
+ numOfPrompts++;
+
+ if (numOfPrompts > 1) {
+ gMockPromptService.returnValue = CANCEL;
+ }
+ };
+
+ gMockPromptService.returnValue = DONT_SAVE;
+
+ // Trigger the quit-application-request notification
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ // We should have cancelled the quit appropriately.
+ Assert.ok(cancelQuit.data);
+
+ // The quit behaviour is that the second window to spawn is the first
+ // one that prompts for Save / Don't Save, etc.
+ gMockPromptService.reset();
+
+ // The first window should still prompt when attempting to close the
+ // window.
+ gMockPromptService.returnValue = DONT_SAVE;
+
+ // Unclear why the timeout is needed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ cwc2.window.goDoCommand("cmd_close");
+
+ TestUtils.waitForCondition(
+ () => !!gMockPromptService.promptState,
+ "Expected a confirmEx prompt to come up"
+ );
+
+ close_compose_window(cwc1);
+
+ gMockPromptService.unregister();
+});
+
+/**
+ * Tests that we don't get a prompt to save if there has been no user input
+ * into the message yet, when trying to close.
+ */
+add_task(async function test_no_prompt_on_close_for_unmodified() {
+ await be_in_folder(folder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let nwc = open_compose_new_mail();
+ close_compose_window(nwc, false);
+
+ let rwc = open_compose_with_reply();
+ close_compose_window(rwc, false);
+
+ let fwc = open_compose_with_forward();
+ close_compose_window(fwc, false);
+});
+
+/**
+ * Tests that we get a prompt to save if the user made changes to the message
+ * before trying to close it.
+ */
+add_task(async function test_prompt_on_close_for_modified() {
+ await be_in_folder(folder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let nwc = open_compose_new_mail();
+ nwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Hey hey hey!", nwc.window);
+ close_compose_window(nwc, true);
+
+ let rwc = open_compose_with_reply();
+ rwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Howdy!", rwc.window);
+ close_compose_window(rwc, true);
+
+ let fwc = open_compose_with_forward();
+ fwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("Greetings!", fwc.window);
+ close_compose_window(fwc, true);
+});
+
+/**
+ * Test there's no prompt on close when no changes was made in reply/forward
+ * windows - for the case the original msg had content type "text".
+ */
+add_task(
+ async function test_no_prompt_on_close_for_unmodified_content_type_text() {
+ await be_in_folder(folder);
+ let msg = select_click_row(1); // row 1 is the one with content type text
+ assert_selected_and_displayed(mc, msg);
+
+ let rwc = open_compose_with_reply();
+ close_compose_window(rwc, false);
+
+ let fwc = open_compose_with_forward();
+ Assert.equal(
+ fwc.window.document.getElementById("attachmentBucket").getRowCount(),
+ 0,
+ "forwarding msg created attachment"
+ );
+ close_compose_window(fwc, false);
+ }
+);
+
+/**
+ * Test there's no prompt on close when no changes was made in reply/forward
+ * windows - for the case the original msg had no content type.
+ */
+add_task(
+ async function test_no_prompt_on_close_for_unmodified_no_content_type() {
+ await be_in_folder(folder);
+ let msg = select_click_row(2); // row 2 is the one with no content type
+ assert_selected_and_displayed(mc, msg);
+
+ let rwc = open_compose_with_reply();
+ close_compose_window(rwc, false);
+
+ let fwc = open_compose_with_forward();
+ Assert.equal(
+ fwc.window.document.getElementById("attachmentBucket").getRowCount(),
+ 0,
+ "forwarding msg created attachment"
+ );
+ close_compose_window(fwc, false);
+ }
+);
+
+add_task(async function test_prompt_save_on_pill_editing() {
+ cwc = open_compose_new_mail(mc);
+
+ // Focus should be on the To field, so just type an address.
+ EventUtils.sendString("test@foo.invalid", cwc.window);
+ let pillCreated = TestUtils.waitForCondition(
+ () => cwc.window.document.querySelectorAll("mail-address-pill").length == 1,
+ "One pill was created"
+ );
+ // Trigger the saving of the draft.
+ EventUtils.synthesizeKey("s", { accelKey: true }, cwc.window);
+ await pillCreated;
+ Assert.ok(cwc.window.gSaveOperationInProgress, "Should start save operation");
+ await TestUtils.waitForCondition(
+ () => !cwc.window.gSaveOperationInProgress && !cwc.window.gWindowLock,
+ "Waiting for the save operation to complete"
+ );
+
+ // All leftover text should have been cleared and pill should have been
+ // created before the draft is actually saved.
+ Assert.equal(
+ cwc.window.document.activeElement.id,
+ "toAddrInput",
+ "The input field is focused."
+ );
+ Assert.equal(
+ cwc.window.document.activeElement.value,
+ "",
+ "The input field is empty."
+ );
+
+ // Close the compose window after the saving operation is completed.
+ close_compose_window(cwc, false);
+
+ // Move to the drafts folder and select the recently saved message.
+ await be_in_folder(gDraftFolder);
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ // Click on the "edit draft" notification.
+ let aboutMessage = get_about_message();
+ let kBoxId = "mail-notification-top";
+ wait_for_notification_to_show(aboutMessage, kBoxId, "draftMsgContent");
+ let box = get_notification(aboutMessage, kBoxId, "draftMsgContent");
+
+ plan_for_new_window("msgcompose");
+ // Click on the "Edit" button in the draft notification.
+ EventUtils.synthesizeMouseAtCenter(
+ box.buttonContainer.firstElementChild,
+ {},
+ aboutMessage
+ );
+ cwc = wait_for_compose_window();
+
+ // Make sure the address was saved correctly.
+ let pill = cwc.window.document.querySelector("mail-address-pill");
+ Assert.equal(
+ pill.fullAddress,
+ "test@foo.invalid",
+ "the email address matches"
+ );
+ let isEditing = TestUtils.waitForCondition(
+ () => pill.isEditing,
+ "Pill is being edited"
+ );
+
+ let focusPromise = TestUtils.waitForCondition(
+ () => cwc.window.document.activeElement == pill,
+ "Pill is focused"
+ );
+ // The focus should be on the subject since we didn't write anything,
+ // so shift+tab to move the focus on the To field, and pressing Arrow Left
+ // should correctly focus the previously generated pill.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, cwc.window);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, cwc.window);
+ await focusPromise;
+ EventUtils.synthesizeKey("VK_RETURN", {}, cwc.window);
+ await isEditing;
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialog("extra1");
+ // Try to quit after entering the pill edit mode, a "unsaved changes" dialog
+ // should be triggered.
+ cwc.window.goDoCommand("cmd_close");
+ await promptPromise;
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_sendButton.js b/comm/mail/test/browser/composition/browser_sendButton.js
new file mode 100644
index 0000000000..304f1cb780
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_sendButton.js
@@ -0,0 +1,363 @@
+/* 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/. */
+
+/**
+ * Tests proper enabling of send buttons depending on addresses input.
+ */
+
+"use strict";
+
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+
+var { create_contact, create_mailing_list, load_contacts_into_address_book } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/AddressBookHelpers.jsm"
+ );
+var {
+ be_in_folder,
+ click_tree_row,
+ FAKE_SERVER_HOSTNAME,
+ get_special_folder,
+ wait_for_popup_to_open,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+var {
+ clear_recipients,
+ get_first_pill,
+ close_compose_window,
+ open_compose_new_mail,
+ setup_msg_contents,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { plan_for_modal_dialog, wait_for_frame_load, wait_for_modal_dialog } =
+ ChromeUtils.import("resource://testing-common/mozmill/WindowHelpers.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var account = null;
+
+add_setup(async function () {
+ // Ensure we're in the tinderbox account as that has the right identities set
+ // up for this test.
+ let server = MailServices.accounts.findServer(
+ "tinderbox",
+ FAKE_SERVER_HOSTNAME,
+ "pop3"
+ );
+ account = MailServices.accounts.FindAccountForServer(server);
+ let inbox = await get_special_folder(
+ Ci.nsMsgFolderFlags.Inbox,
+ false,
+ server
+ );
+ await be_in_folder(inbox);
+});
+
+/**
+ * Check if the send commands are in the wished state.
+ *
+ * @param aCwc The compose window controller.
+ * @param aEnabled The expected state of the commands.
+ */
+function check_send_commands_state(aCwc, aEnabled) {
+ Assert.equal(
+ aCwc.window.document
+ .getElementById("cmd_sendButton")
+ .hasAttribute("disabled"),
+ !aEnabled
+ );
+ Assert.equal(
+ aCwc.window.document.getElementById("cmd_sendNow").hasAttribute("disabled"),
+ !aEnabled
+ );
+ Assert.equal(
+ aCwc.window.document
+ .getElementById("cmd_sendWithCheck")
+ .hasAttribute("disabled"),
+ !aEnabled
+ );
+ Assert.equal(
+ aCwc.window.document
+ .getElementById("cmd_sendLater")
+ .hasAttribute("disabled"),
+ !aEnabled
+ );
+
+ // The toolbar buttons and menuitems should be linked to these commands
+ // thus inheriting the enabled state. Check that on the Send button
+ // and Send Now menuitem.
+ Assert.equal(
+ aCwc.window.document.getElementById("button-send").getAttribute("command"),
+ "cmd_sendButton"
+ );
+ Assert.equal(
+ aCwc.window.document
+ .getElementById("menu-item-send-now")
+ .getAttribute("command"),
+ "cmd_sendNow"
+ );
+}
+
+/**
+ * Bug 431217
+ * Test that the Send buttons are properly enabled if an addressee is input
+ * by the user.
+ */
+add_task(async function test_send_enabled_manual_address() {
+ let cwc = open_compose_new_mail(); // compose controller
+ let menu = cwc.window.document.getElementById("extraAddressRowsMenu"); // extra recipients menu
+ let menuButton = cwc.window.document.getElementById(
+ "extraAddressRowsMenuButton"
+ );
+
+ // On an empty window, Send must be disabled.
+ check_send_commands_state(cwc, false);
+
+ // On valid "To:" addressee input, Send must be enabled.
+ setup_msg_contents(cwc, " recipient@fake.invalid ", "", "");
+ check_send_commands_state(cwc, true);
+
+ // When the addressee is not in To, Cc, Bcc or Newsgroup, disable Send again.
+ clear_recipients(cwc);
+ EventUtils.synthesizeMouseAtCenter(menuButton, {}, menuButton.ownerGlobal);
+ await new Promise(resolve => setTimeout(resolve));
+ await wait_for_popup_to_open(menu);
+ menu.activateItem(
+ cwc.window.document.getElementById("addr_replyShowAddressRowMenuItem")
+ );
+ setup_msg_contents(cwc, " recipient@fake.invalid ", "", "", "replyAddrInput");
+ check_send_commands_state(cwc, false);
+
+ clear_recipients(cwc);
+ check_send_commands_state(cwc, false);
+
+ // Bug 1296535
+ // Try some other invalid and valid recipient strings:
+ // - random string that is no email.
+ setup_msg_contents(cwc, " recipient@", "", "");
+ check_send_commands_state(cwc, false);
+
+ let ccShow = cwc.window.document.getElementById(
+ "addr_ccShowAddressRowButton"
+ );
+ EventUtils.synthesizeMouseAtCenter(ccShow, {}, ccShow.ownerGlobal);
+ await new Promise(resolve => setTimeout(resolve));
+ check_send_commands_state(cwc, false);
+
+ // Select the newly generated pill.
+ EventUtils.synthesizeMouseAtCenter(
+ get_first_pill(cwc),
+ {},
+ get_first_pill(cwc).ownerGlobal
+ );
+ await new Promise(resolve => setTimeout(resolve));
+ // Delete the selected pill.
+ EventUtils.synthesizeKey("VK_DELETE", {}, cwc.window);
+ // Confirm the address row is now empty.
+ Assert.ok(!get_first_pill(cwc));
+ // Confirm the send button is disabled.
+ check_send_commands_state(cwc, false);
+ // Add multiple recipients.
+ setup_msg_contents(
+ cwc,
+ "recipient@domain.invalid, info@somedomain.extension, name@incomplete",
+ "",
+ ""
+ );
+ check_send_commands_state(cwc, true);
+
+ clear_recipients(cwc);
+ check_send_commands_state(cwc, false);
+
+ // - a mailinglist in addressbook
+ // Button is enabled without checking whether it contains valid addresses.
+ let defaultAB = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ let ml = create_mailing_list("emptyList");
+ defaultAB.addMailList(ml);
+
+ setup_msg_contents(cwc, " emptyList", "", "");
+ check_send_commands_state(cwc, true);
+
+ clear_recipients(cwc);
+ check_send_commands_state(cwc, false);
+
+ setup_msg_contents(cwc, "emptyList <list> ", "", "");
+ check_send_commands_state(cwc, true);
+
+ clear_recipients(cwc);
+ check_send_commands_state(cwc, false);
+
+ // Hack to reveal the newsgroup button.
+ let newsgroupsButton = cwc.window.document.getElementById(
+ "addr_newsgroupsShowAddressRowButton"
+ );
+ newsgroupsButton.hidden = false;
+ EventUtils.synthesizeMouseAtCenter(
+ newsgroupsButton,
+ {},
+ newsgroupsButton.ownerGlobal
+ );
+ await new Promise(resolve => setTimeout(resolve));
+
+ // - some string as a newsgroup
+ setup_msg_contents(cwc, "newsgroup ", "", "", "newsgroupsAddrInput");
+ check_send_commands_state(cwc, true);
+
+ close_compose_window(cwc);
+});
+
+/**
+ * Bug 431217
+ * Test that the Send buttons are properly enabled if an addressee is prefilled
+ * automatically via account prefs.
+ */
+add_task(function test_send_enabled_prefilled_address() {
+ // Set the prefs to prefill a default CC address when Compose is opened.
+ let identity = account.defaultIdentity;
+ identity.doCc = true;
+ identity.doCcList = "Auto@recipient.invalid";
+
+ // In that case the recipient is input, enabled Send.
+ let cwc = open_compose_new_mail(); // compose controller
+ check_send_commands_state(cwc, true);
+
+ // Clear the CC list.
+ clear_recipients(cwc);
+ // No other pill is there. Send should become disabled.
+ check_send_commands_state(cwc, false);
+
+ close_compose_window(cwc);
+ identity.doCcList = "";
+ identity.doCc = false;
+});
+
+/**
+ * Bug 933101
+ * Similar to test_send_enabled_prefilled_address but switched between an identity
+ * that has a CC list and one that doesn't directly in the compose window.
+ */
+add_task(async function test_send_enabled_prefilled_address_from_identity() {
+ // The first identity will have an automatic CC enabled.
+ let identityWithCC = account.defaultIdentity;
+ identityWithCC.doCc = true;
+ identityWithCC.doCcList = "Auto@recipient.invalid";
+
+ // CC is prefilled, Send enabled.
+ let cwc = open_compose_new_mail();
+ check_send_commands_state(cwc, true);
+
+ let identityPicker = cwc.window.document.getElementById("msgIdentity");
+ Assert.equal(identityPicker.selectedIndex, 0);
+
+ // Switch to the second identity that has no CC. Send should be disabled.
+ Assert.ok(account.identities.length >= 2);
+ let identityWithoutCC = account.identities[1];
+ Assert.ok(!identityWithoutCC.doCc);
+ await chooseIdentity(cwc.window, identityWithoutCC.key);
+ check_send_commands_state(cwc, false);
+
+ // Check the first identity again.
+ await chooseIdentity(cwc.window, identityWithCC.key);
+ check_send_commands_state(cwc, true);
+
+ close_compose_window(cwc);
+ identityWithCC.doCcList = "";
+ identityWithCC.doCc = false;
+});
+
+/**
+ * Bug 863231
+ * Test that the Send buttons are properly enabled if an addressee is populated
+ * via the Contacts sidebar.
+ */
+add_task(function test_send_enabled_address_contacts_sidebar() {
+ // Create some contact address book card in the Personal addressbook.
+ let defaultAB = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite");
+ let contact = create_contact("test@example.com", "Sammy Jenkis", true);
+ load_contacts_into_address_book(defaultAB, [contact]);
+
+ let cwc = open_compose_new_mail(); // compose controller
+ // On an empty window, Send must be disabled.
+ check_send_commands_state(cwc, false);
+
+ // Open Contacts sidebar and use our contact.
+ // FIXME: Use UI to open contacts sidebar.
+ cwc.window.toggleContactsSidebar();
+
+ let contactsBrowser = cwc.window.document.getElementById("contactsBrowser");
+ wait_for_frame_load(
+ contactsBrowser,
+ "chrome://messenger/content/addressbook/abContactsPanel.xhtml?focus"
+ );
+
+ let abTree = contactsBrowser.contentDocument.getElementById("abResultsTree");
+ // The results are loaded async so wait for the population of the tree.
+ utils.waitFor(
+ () => abTree.view.rowCount > 0,
+ "Addressbook cards didn't load"
+ );
+ click_tree_row(abTree, 0, cwc);
+
+ contactsBrowser.contentDocument.getElementById("ccButton").click();
+
+ // The recipient is filled in, Send must be enabled.
+ check_send_commands_state(cwc, true);
+
+ // FIXME: Use UI to close contacts sidebar.
+ cwc.window.toggleContactsSidebar();
+ close_compose_window(cwc);
+});
+
+/**
+ * Tests that when editing a pill and clicking send while the edit is active
+ * the pill gets updated before the send of the email.
+ */
+add_task(async function test_update_pill_before_send() {
+ let cwc = open_compose_new_mail();
+
+ setup_msg_contents(cwc, "recipient@fake.invalid", "Subject", "");
+
+ let pill = get_first_pill(cwc);
+
+ // Edit the first pill.
+ // First, we need to get into the edit mode by clicking the pill twice.
+ EventUtils.synthesizeMouseAtCenter(pill, { clickCount: 1 }, cwc.window);
+ let clickPromise = BrowserTestUtils.waitForEvent(pill, "click");
+ // We do not want a double click, but two separate clicks.
+ EventUtils.synthesizeMouseAtCenter(pill, { clickCount: 1 }, cwc.window);
+ await clickPromise;
+
+ Assert.ok(!pill.querySelector("input").hidden);
+
+ // Set the pill which is in edit mode to an invalid email.
+ EventUtils.synthesizeKey("KEY_Home", { shiftKey: true }, cwc.window);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {}, cwc.window);
+ EventUtils.sendString("invalidEmail", cwc.window);
+
+ // Click send while the pill is in the edit mode and check the dialog title
+ // if the pill is updated we get an invalid recipient error. Otherwise the
+ // error would be an imap error because the email would still be sent to
+ // `recipient@fake.invalid`.
+ let dialogTitle;
+ plan_for_modal_dialog("commonDialogWindow", cwc => {
+ dialogTitle = cwc.window.document.getElementById("infoTitle").textContent;
+ cwc.window.document.querySelector("dialog").getButton("accept").click();
+ });
+ // Click the send button.
+ EventUtils.synthesizeMouseAtCenter(
+ cwc.window.document.getElementById("button-send"),
+ {},
+ cwc.window
+ );
+ wait_for_modal_dialog("commonDialogWindow");
+
+ Assert.ok(
+ dialogTitle.includes("Invalid Recipient Address"),
+ "The pill edit has been updated before sending the email"
+ );
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_sendFormat.js b/comm/mail/test/browser/composition/browser_sendFormat.js
new file mode 100644
index 0000000000..8877b4d8a5
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_sendFormat.js
@@ -0,0 +1,565 @@
+/* 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/. */
+
+/**
+ * Tests resulting send format of a message dependent on using HTML features
+ * in the composition.
+ */
+
+"use strict";
+
+requestLongerTimeout(4);
+
+var {
+ open_compose_from_draft,
+ open_compose_new_mail,
+ open_compose_with_reply,
+ FormatHelper,
+} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+var {
+ be_in_folder,
+ empty_folder,
+ get_special_folder,
+ get_about_message,
+ open_message_from_file,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var sendFormatPreference;
+var htmlAsPreference;
+var draftsFolder;
+var outboxFolder;
+
+add_setup(async () => {
+ sendFormatPreference = Services.prefs.getIntPref("mail.default_send_format");
+ htmlAsPreference = Services.prefs.getIntPref("mailnews.display.html_as");
+ // Show all parts to a message in the message display.
+ // This allows us to see if a message contains both a plain text and a HTML
+ // part.
+ Services.prefs.setIntPref("mailnews.display.html_as", 4);
+ draftsFolder = await get_special_folder(Ci.nsMsgFolderFlags.Drafts, true);
+ outboxFolder = await get_special_folder(Ci.nsMsgFolderFlags.Queue, true);
+});
+
+registerCleanupFunction(async function () {
+ Services.prefs.setIntPref("mail.default_send_format", sendFormatPreference);
+ Services.prefs.setIntPref("mailnews.display.html_as", htmlAsPreference);
+ await empty_folder(draftsFolder);
+ await empty_folder(outboxFolder);
+});
+
+async function checkMsgFile(aFilePath, aConvertibility) {
+ let file = new FileUtils.File(getTestFilePath(`data/${aFilePath}`));
+ let messageController = await open_message_from_file(file);
+
+ // Creating a reply should not affect convertibility.
+ let composeWindow = open_compose_with_reply(messageController).window;
+
+ Assert.equal(composeWindow.gMsgCompose.bodyConvertible(), aConvertibility);
+
+ await BrowserTestUtils.closeWindow(composeWindow);
+ await BrowserTestUtils.closeWindow(messageController.window);
+}
+
+/**
+ * Tests nodeTreeConvertible() can be called from JavaScript.
+ */
+add_task(async function test_msg_nodeTreeConvertible() {
+ let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance(
+ Ci.nsIMsgCompose
+ );
+
+ let textDoc = new DOMParser().parseFromString(
+ "<p>Simple Text</p>",
+ "text/html"
+ );
+ Assert.equal(
+ msgCompose.nodeTreeConvertible(textDoc.documentElement),
+ Ci.nsIMsgCompConvertible.Plain
+ );
+
+ let htmlDoc = new DOMParser().parseFromString(
+ '<p>Complex <span style="font-weight: bold">Text</span></p>',
+ "text/html"
+ );
+ Assert.equal(
+ msgCompose.nodeTreeConvertible(htmlDoc.documentElement),
+ Ci.nsIMsgCompConvertible.No
+ );
+});
+
+/**
+ * Tests that we only open one compose window for one instance of a draft.
+ */
+add_task(async function test_msg_convertibility() {
+ await checkMsgFile("./format1-plain.eml", Ci.nsIMsgCompConvertible.Plain);
+
+ // Bug 1385636
+ await checkMsgFile(
+ "./format1-altering.eml",
+ Ci.nsIMsgCompConvertible.Altering
+ );
+
+ // Bug 584313
+ await checkMsgFile("./format2-style-attr.eml", Ci.nsIMsgCompConvertible.No);
+ await checkMsgFile("./format3-style-tag.eml", Ci.nsIMsgCompConvertible.No);
+});
+
+/**
+ * Map from a nsIMsgCompSendFormat to the id of the corresponding menuitem in
+ * the Options, Send Format menu.
+ *
+ * @type {Map<nsIMsgCompSendFormat, string>}
+ */
+var sendFormatToMenuitem = new Map([
+ [Ci.nsIMsgCompSendFormat.PlainText, "format_plain"],
+ [Ci.nsIMsgCompSendFormat.HTML, "format_html"],
+ [Ci.nsIMsgCompSendFormat.Both, "format_both"],
+ [Ci.nsIMsgCompSendFormat.Auto, "format_auto"],
+]);
+
+/**
+ * Verify that the correct send format menu item is checked.
+ *
+ * @param {Window} composeWindow - The compose window.
+ * @param {nsIMsgCompSendFormat} expectFormat - The expected checked format
+ * option. Either Auto, PlainText, HTML, or Both.
+ * @param {string} msg - A message to use in assertions.
+ */
+function assertSendFormatInMenu(composeWindow, expectFormat, msg) {
+ for (let [format, menuitemId] of sendFormatToMenuitem.entries()) {
+ let menuitem = composeWindow.document.getElementById(menuitemId);
+ let checked = expectFormat == format;
+ Assert.equal(
+ menuitem.getAttribute("checked") == "true",
+ checked,
+ `${menuitemId} should ${checked ? "not " : ""}be checked: ${msg}`
+ );
+ }
+}
+
+const PLAIN_MESSAGE_BODY = "Plain message body";
+const BOLD_MESSAGE_BODY = "Bold message body";
+const BOLD_MESSAGE_BODY_AS_PLAIN = `*${BOLD_MESSAGE_BODY}*`;
+
+/**
+ * Set the default send format and create a new message in the compose window.
+ *
+ * @param {nsIMsgCompSendFormat} preference - The default send format to set via
+ * a preference before opening the window.
+ * @param {boolean} useBold - Whether to use bold text in the message's body.
+ *
+ * @returns {Window} - The opened compose window, pre-filled with a message.
+ */
+async function newMessage(preference, useBold) {
+ Services.prefs.setIntPref("mail.default_send_format", preference);
+
+ let composeWindow = open_compose_new_mail().window;
+ assertSendFormatInMenu(
+ composeWindow,
+ preference,
+ "Send format should initially match preference"
+ );
+
+ // Focus should be on "To" field.
+ EventUtils.sendString("recipient@server.net", composeWindow);
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeKey("KEY_Enter", {}, composeWindow);
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeKey("KEY_Enter", {}, composeWindow);
+ await TestUtils.waitForTick();
+ // Focus should be in the "Subject" field.
+ EventUtils.sendString(
+ `${useBold ? "rich" : "plain"} message with preference ${preference}`,
+ composeWindow
+ );
+ await TestUtils.waitForTick();
+ EventUtils.synthesizeKey("KEY_Enter", {}, composeWindow);
+ await TestUtils.waitForTick();
+
+ // Focus should be in the body.
+ let formatHelper = new FormatHelper(composeWindow);
+ if (useBold) {
+ EventUtils.synthesizeMouseAtCenter(
+ formatHelper.boldButton,
+ {},
+ composeWindow
+ );
+ await TestUtils.waitForTick();
+ await formatHelper.typeInMessage(BOLD_MESSAGE_BODY);
+ } else {
+ await formatHelper.typeInMessage(PLAIN_MESSAGE_BODY);
+ }
+
+ return composeWindow;
+}
+
+/**
+ * Set the send format to something else via the application menu.
+ *
+ * @param {Window} composeWindow - The compose window to set the format in.
+ * @param {nsIMsgCompSendFormat} sendFormat - The send format to set. Either
+ * Auto, PlainText, HTML, or Both.
+ */
+async function setSendFormat(composeWindow, sendFormat) {
+ async function openMenu(menu) {
+ let openPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ menu.openMenu(true);
+ await openPromise;
+ }
+ let optionsMenu = composeWindow.document.getElementById("optionsMenu");
+ let sendFormatMenu =
+ composeWindow.document.getElementById("outputFormatMenu");
+ let menuitem = composeWindow.document.getElementById(
+ sendFormatToMenuitem.get(sendFormat)
+ );
+
+ await openMenu(optionsMenu);
+ await openMenu(sendFormatMenu);
+
+ let closePromise = BrowserTestUtils.waitForEvent(optionsMenu, "popuphidden");
+ sendFormatMenu.menupopup.activateItem(menuitem);
+ await closePromise;
+ assertSendFormatInMenu(
+ composeWindow,
+ sendFormat,
+ "Send format should change to the selected format"
+ );
+}
+
+/**
+ * Verify the actual sent message of a composed message.
+ *
+ * @param {Window} composeWindow - The compose window that contains the message
+ * we want to send.
+ * @param {object} expectMessage - The expected sent message.
+ * @param {boolean} expectMessage.isBold - Whether the message uses a bold
+ * message, rather than the plain message.
+ * @param {boolean} expectMessage.plain - Whether the message has a plain part.
+ * @param {boolean} expectMessage.html - Whether the message has a html part.
+ * @param {string} msg - A message to use in assertions.
+ */
+async function assertSentMessage(composeWindow, expectMessage, msg) {
+ let { isBold, plain, html } = expectMessage;
+
+ // Send later.
+ let closePromise = BrowserTestUtils.windowClosed(composeWindow);
+ EventUtils.synthesizeKey(
+ "KEY_Enter",
+ { accelKey: true, shiftKey: true },
+ composeWindow
+ );
+ await closePromise;
+
+ // Open the "sent" message.
+ await be_in_folder(outboxFolder);
+ // Should be the last message in the tree.
+ select_click_row(-1);
+
+ // Test that the sent content type is either text/plain, text/html or
+ // multipart/alternative.
+ // TODO: Is there a better way to expose the content-type of the displayed
+ // message?
+ let contentType =
+ get_about_message().currentHeaderData["content-type"].headerValue;
+ if (plain && html) {
+ Assert.ok(
+ contentType.startsWith("multipart/alternative"),
+ `Sent contentType "${contentType}" should be multipart: ${msg}`
+ );
+ } else if (plain) {
+ Assert.ok(
+ contentType.startsWith("text/plain"),
+ `Sent contentType "${contentType}" should be plain text only: ${msg}`
+ );
+ } else if (html) {
+ Assert.ok(
+ contentType.startsWith("text/html"),
+ `Sent contentType "${contentType}" should be html only: ${msg}`
+ );
+ } else {
+ throw new Error("Expected message is missing either plain or html parts");
+ }
+
+ // Assert the html and plain text parts are either hidden or shown.
+ // NOTE: We have set the mailnews.display.html_as preference to show all parts
+ // of the message, which means it will show both the plain text and html parts
+ // if both were sent.
+ let messageBody =
+ get_about_message().document.getElementById("messagepane").contentDocument
+ .body;
+ let plainBody = messageBody.querySelector(".moz-text-flowed");
+ let htmlBody = messageBody.querySelector(".moz-text-html");
+ Assert.equal(
+ !!plain,
+ !!plainBody,
+ `Message should ${plain ? "" : "not "}have a Plain part: ${msg}`
+ );
+ Assert.equal(
+ !!html,
+ !!htmlBody,
+ `Message should ${html ? "" : "not "}have a HTML part: ${msg}`
+ );
+
+ if (plain) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(plainBody),
+ `Plain part should be visible: ${msg}`
+ );
+ Assert.equal(
+ plainBody.textContent.trim(),
+ isBold ? BOLD_MESSAGE_BODY_AS_PLAIN : PLAIN_MESSAGE_BODY,
+ `Plain text content should match: ${msg}`
+ );
+ }
+
+ if (html) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(htmlBody),
+ `HTML part should be visible: ${msg}`
+ );
+ Assert.equal(
+ htmlBody.textContent.trim(),
+ isBold ? BOLD_MESSAGE_BODY : PLAIN_MESSAGE_BODY,
+ `HTML text content should match: ${msg}`
+ );
+ }
+}
+
+async function saveDraft(composeWindow) {
+ let oldDraftsCounts = draftsFolder.getTotalMessages(false);
+ // Save as draft.
+ EventUtils.synthesizeKey("s", { accelKey: true }, composeWindow);
+ await TestUtils.waitForCondition(
+ () => composeWindow.gSaveOperationInProgress,
+ "Should start save operation"
+ );
+ await TestUtils.waitForCondition(
+ () => !composeWindow.gSaveOperationInProgress && !composeWindow.gWindowLock,
+ "Waiting for the save operation to complete"
+ );
+ await TestUtils.waitForCondition(
+ () => draftsFolder.getTotalMessages(false) > oldDraftsCounts,
+ "message saved to drafts folder"
+ );
+ await BrowserTestUtils.closeWindow(composeWindow);
+}
+
+async function assertDraftFormat(expectSavedFormat) {
+ await be_in_folder(draftsFolder);
+ select_click_row(0);
+
+ let newComposeWindow = open_compose_from_draft().window;
+ assertSendFormatInMenu(
+ newComposeWindow,
+ expectSavedFormat,
+ "Send format of the opened draft should match the saved format"
+ );
+ return newComposeWindow;
+}
+
+add_task(async function test_preference_send_format() {
+ // Sending a plain message.
+ for (let { preference, sendsPlain, sendsHtml } of [
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.PlainText,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Both,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ ]) {
+ info(`Testing preference ${preference} with a plain message`);
+ let composeWindow = await newMessage(preference, false);
+ await assertSentMessage(
+ composeWindow,
+ { plain: sendsPlain, html: sendsHtml, isBold: false },
+ `Plain message with preference ${preference}`
+ );
+ }
+ // Sending a bold message.
+ for (let { preference, sendsPlain, sendsHtml } of [
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.PlainText,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Both,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ ]) {
+ info(`Testing preference ${preference} with a bold message`);
+ let composeWindow = await newMessage(preference, true);
+ await assertSentMessage(
+ composeWindow,
+ { plain: sendsPlain, html: sendsHtml, isBold: true },
+ `Bold message with preference ${preference}`
+ );
+ }
+});
+
+add_task(async function test_setting_send_format() {
+ for (let { preference, sendFormat, boldMessage, sendsPlain, sendsHtml } of [
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ boldMessage: true,
+ sendFormat: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ boldMessage: true,
+ sendFormat: Ci.nsIMsgCompSendFormat.PlainText,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.PlainText,
+ boldMessage: false,
+ sendFormat: Ci.nsIMsgCompSendFormat.Both,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.HTML,
+ boldMessage: false,
+ sendFormat: Ci.nsIMsgCompSendFormat.Auto,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Both,
+ boldMessage: false,
+ sendFormat: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ ]) {
+ info(
+ `Testing changing format from preference ${preference} to ${sendFormat}`
+ );
+ let composeWindow = await newMessage(preference, boldMessage);
+ await setSendFormat(composeWindow, sendFormat);
+ await assertSentMessage(
+ composeWindow,
+ { isBold: boldMessage, plain: sendsPlain, html: sendsHtml },
+ `${boldMessage ? "Bold" : "Plain"} message set as ${sendFormat}`
+ );
+ }
+}).__skipMe = AppConstants.platform == "macosx";
+// Can't click menu bar on Mac to change the send format.
+
+add_task(async function test_saving_draft_with_set_format() {
+ for (let { preference, sendFormat, sendsPlain, sendsHtml } of [
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ sendFormat: Ci.nsIMsgCompSendFormat.PlainText,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.PlainText,
+ sendFormat: Ci.nsIMsgCompSendFormat.Auto,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Both,
+ sendFormat: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.HTML,
+ sendFormat: Ci.nsIMsgCompSendFormat.Both,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ ]) {
+ info(`Testing draft saved as ${sendFormat}`);
+ let composeWindow = await newMessage(preference, true);
+ await setSendFormat(composeWindow, sendFormat);
+ await saveDraft(composeWindow);
+ // Draft keeps the set format when opened.
+ composeWindow = await assertDraftFormat(sendFormat);
+ await assertSentMessage(
+ composeWindow,
+ { isBold: true, plain: sendsPlain, html: sendsHtml },
+ `Bold draft message set as ${sendFormat}`
+ );
+ }
+}).__skipMe = AppConstants.platform == "macosx";
+// Can't click menu bar on Mac to change the send format.
+
+add_task(async function test_saving_draft_with_new_preference() {
+ for (let { preference, newPreference, sendsPlain, sendsHtml } of [
+ {
+ preference: Ci.nsIMsgCompSendFormat.Auto,
+ newPreference: Ci.nsIMsgCompSendFormat.HTML,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.PlainText,
+ newPreference: Ci.nsIMsgCompSendFormat.Both,
+ sendsPlain: true,
+ sendsHtml: false,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.Both,
+ newPreference: Ci.nsIMsgCompSendFormat.Auto,
+ sendsPlain: true,
+ sendsHtml: true,
+ },
+ {
+ preference: Ci.nsIMsgCompSendFormat.HTML,
+ newPreference: Ci.nsIMsgCompSendFormat.PlainText,
+ sendsPlain: false,
+ sendsHtml: true,
+ },
+ ]) {
+ info(`Testing changing preference from ${preference} to ${newPreference}`);
+ let composeWindow = await newMessage(preference, false);
+ await saveDraft(composeWindow);
+ // Re-open, with a new default preference set, to make sure the draft has
+ // the send format set earlier saved in its headers.
+ Services.prefs.setIntPref("mail.default_send_format", newPreference);
+ // Draft keeps the old preference.
+ composeWindow = await assertDraftFormat(preference);
+ await assertSentMessage(
+ composeWindow,
+ { isBold: false, plain: sendsPlain, html: sendsHtml },
+ `Plain draft message with preference ${preference}`
+ );
+ }
+});
diff --git a/comm/mail/test/browser/composition/browser_signatureInit.js b/comm/mail/test/browser/composition/browser_signatureInit.js
new file mode 100644
index 0000000000..f4206696b7
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_signatureInit.js
@@ -0,0 +1,50 @@
+/* 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/. */
+
+/**
+ * Tests that the compose window initializes with the signature correctly
+ * under various circumstances.
+ */
+
+"use strict";
+
+var { close_compose_window, get_compose_body, open_compose_new_mail } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+var kHtmlPref = "mail.identity.default.compose_html";
+var kReplyOnTopPref = "mail.identity.default.reply_on_top";
+var kReplyOnTop = 1;
+var kSigBottomPref = "mail.identity.default.sig_bottom";
+
+/**
+ * Regression test for bug 762413 - tests that when we're set to reply above,
+ * with the signature below the reply, we initialize the compose window such
+ * that there is a <br> node above the signature. This allows the user to
+ * insert text before the signature.
+ */
+add_task(function test_on_reply_above_signature_below_reply() {
+ let origHtml = Services.prefs.getBoolPref(kHtmlPref);
+ let origReplyOnTop = Services.prefs.getIntPref(kReplyOnTopPref);
+ let origSigBottom = Services.prefs.getBoolPref(kSigBottomPref);
+
+ Services.prefs.setBoolPref(kHtmlPref, false);
+ Services.prefs.setIntPref(kReplyOnTopPref, kReplyOnTop);
+ Services.prefs.setBoolPref(kSigBottomPref, false);
+
+ let cw = open_compose_new_mail();
+ let mailBody = get_compose_body(cw);
+
+ let node = mailBody.firstChild;
+ Assert.equal(
+ node.localName,
+ "br",
+ "Expected a BR node to start the compose body."
+ );
+
+ Services.prefs.setBoolPref(kHtmlPref, origHtml);
+ Services.prefs.setIntPref(kReplyOnTopPref, origReplyOnTop);
+ Services.prefs.setBoolPref(kSigBottomPref, origSigBottom);
+
+ close_compose_window(cw);
+});
diff --git a/comm/mail/test/browser/composition/browser_signatureUpdating.js b/comm/mail/test/browser/composition/browser_signatureUpdating.js
new file mode 100644
index 0000000000..1555703e27
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_signatureUpdating.js
@@ -0,0 +1,276 @@
+/* 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/. */
+
+/**
+ * Tests that the signature updates properly when switching identities.
+ */
+
+// mail.identity.id1.htmlSigFormat = false
+// mail.identity.id1.htmlSigText = "Tinderbox is soo 90ies"
+
+// mail.identity.id2.htmlSigFormat = true
+// mail.identity.id2.htmlSigText = "Tinderboxpushlog is the new <b>hotness!</b>"
+
+"use strict";
+
+var { close_compose_window, open_compose_new_mail, setup_msg_contents } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+var { be_in_folder, FAKE_SERVER_HOSTNAME, get_special_folder } =
+ ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+ );
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var cwc = null; // compose window controller
+
+add_setup(async function () {
+ requestLongerTimeout(2);
+
+ // These prefs can't be set in the manifest as they contain white-space.
+ Services.prefs.setStringPref(
+ "mail.identity.id1.htmlSigText",
+ "Tinderbox is soo 90ies"
+ );
+ Services.prefs.setStringPref(
+ "mail.identity.id2.htmlSigText",
+ "Tinderboxpushlog is the new <b>hotness!</b>"
+ );
+
+ // Ensure we're in the tinderbox account as that has the right identities set
+ // up for this test.
+ let server = MailServices.accounts.findServer(
+ "tinderbox",
+ FAKE_SERVER_HOSTNAME,
+ "pop3"
+ );
+ let inbox = await get_special_folder(
+ Ci.nsMsgFolderFlags.Inbox,
+ false,
+ server
+ );
+ await be_in_folder(inbox);
+});
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("mail.compose.default_to_paragraph");
+ Services.prefs.clearUserPref("mail.identity.id1.compose_html");
+ Services.prefs.clearUserPref("mail.identity.id1.htmlSigText");
+ Services.prefs.clearUserPref("mail.identity.id2.htmlSigText");
+ Services.prefs.clearUserPref(
+ "mail.identity.id1.suppress_signature_separator"
+ );
+ Services.prefs.clearUserPref(
+ "mail.identity.id2.suppress_signature_separator"
+ );
+});
+
+/**
+ * Test that the plaintext compose window has a signature initially,
+ * and has the correct signature after switching to another identity.
+ */
+async function plaintextComposeWindowSwitchSignatures(suppressSigSep) {
+ Services.prefs.setBoolPref("mail.identity.id1.compose_html", false);
+ Services.prefs.setBoolPref(
+ "mail.identity.id1.suppress_signature_separator",
+ suppressSigSep
+ );
+ Services.prefs.setBoolPref(
+ "mail.identity.id2.suppress_signature_separator",
+ suppressSigSep
+ );
+ cwc = open_compose_new_mail();
+
+ let contentFrame = cwc.window.document.getElementById("messageEditor");
+ let mailBody = contentFrame.contentDocument.body;
+
+ // The first node in the body should be a BR node, which allows the user
+ // to insert text before / outside of the signature.
+ Assert.equal(mailBody.firstChild.localName, "br");
+
+ setup_msg_contents(cwc, "", "Plaintext compose window", "Body, first line.");
+
+ let node = mailBody.lastChild;
+
+ // The last node is a BR - this allows users to put text after the
+ // signature without it being styled like the signature.
+ Assert.equal(node.localName, "br");
+ node = node.previousSibling;
+
+ // Now we should have the DIV node that contains the signature, with
+ // the class moz-signature.
+ Assert.equal(node.localName, "div");
+
+ const kSeparator = "-- ";
+ const kSigClass = "moz-signature";
+ Assert.equal(node.className, kSigClass);
+
+ let sigNode = node.firstChild;
+
+ if (!suppressSigSep) {
+ Assert.equal(sigNode.textContent, kSeparator);
+ let brNode = sigNode.nextSibling;
+ Assert.equal(brNode.localName, "br");
+ sigNode = brNode.nextSibling;
+ }
+
+ let expectedText = "Tinderbox is soo 90ies";
+ Assert.equal(sigNode.textContent, expectedText);
+
+ // Now switch identities!
+ await chooseIdentity(cwc.window, "id2");
+
+ node = contentFrame.contentDocument.body.lastChild;
+
+ // The last node is a BR - this allows users to put text after the
+ // signature without it being styled like the signature.
+ Assert.equal(node.localName, "br");
+ node = node.previousSibling;
+
+ Assert.equal(node.localName, "div");
+ Assert.equal(node.className, kSigClass);
+
+ sigNode = node.firstChild;
+
+ if (!suppressSigSep) {
+ expectedText = "-- ";
+ Assert.equal(sigNode.textContent, kSeparator);
+ let brNode = sigNode.nextSibling;
+ Assert.equal(brNode.localName, "br");
+ sigNode = brNode.nextSibling;
+ }
+
+ expectedText = "Tinderboxpushlog is the new *hotness!*";
+ Assert.equal(sigNode.textContent, expectedText);
+
+ // Now check that the original signature has been removed by ensuring
+ // that there's only one node with class moz-signature.
+ let sigs = contentFrame.contentDocument.querySelectorAll("." + kSigClass);
+ Assert.equal(sigs.length, 1);
+
+ // And ensure that the text we wrote wasn't altered
+ let bodyFirstChild = contentFrame.contentDocument.body.firstChild;
+
+ while (node != bodyFirstChild) {
+ node = node.previousSibling;
+ }
+
+ Assert.equal(node.nodeValue, "Body, first line.");
+
+ close_compose_window(cwc);
+}
+
+add_task(async function testPlaintextComposeWindowSwitchSignatures() {
+ await plaintextComposeWindowSwitchSignatures(false);
+});
+
+add_task(
+ async function testPlaintextComposeWindowSwitchSignaturesWithSuppressedSeparator() {
+ await plaintextComposeWindowSwitchSignatures(true);
+ }
+);
+
+/**
+ * Same test, but with an HTML compose window
+ */
+async function HTMLComposeWindowSwitchSignatures(
+ suppressSigSep,
+ paragraphFormat
+) {
+ Services.prefs.setBoolPref(
+ "mail.compose.default_to_paragraph",
+ paragraphFormat
+ );
+
+ Services.prefs.setBoolPref("mail.identity.id1.compose_html", true);
+ Services.prefs.setBoolPref(
+ "mail.identity.id1.suppress_signature_separator",
+ suppressSigSep
+ );
+ Services.prefs.setBoolPref(
+ "mail.identity.id2.suppress_signature_separator",
+ suppressSigSep
+ );
+ cwc = open_compose_new_mail();
+
+ setup_msg_contents(cwc, "", "HTML compose window", "Body, first line.");
+
+ let contentFrame = cwc.window.document.getElementById("messageEditor");
+ let node = contentFrame.contentDocument.body.lastChild;
+
+ // In html compose, the signature is inside the last node, which has a
+ // class="moz-signature".
+ Assert.equal(node.className, "moz-signature");
+ node = node.firstChild; // text node containing the signature divider
+ if (suppressSigSep) {
+ Assert.equal(node.nodeValue, "Tinderbox is soo 90ies");
+ } else {
+ Assert.equal(node.nodeValue, "-- \nTinderbox is soo 90ies");
+ }
+
+ // Now switch identities!
+ await chooseIdentity(cwc.window, "id2");
+
+ node = contentFrame.contentDocument.body.lastChild;
+
+ // In html compose, the signature is inside the last node
+ // with class="moz-signature".
+ Assert.equal(node.className, "moz-signature");
+ node = node.firstChild; // text node containing the signature divider
+ if (!suppressSigSep) {
+ Assert.equal(node.nodeValue, "-- ");
+ node = node.nextSibling;
+ Assert.equal(node.localName, "br");
+ node = node.nextSibling;
+ }
+ Assert.equal(node.nodeValue, "Tinderboxpushlog is the new ");
+ node = node.nextSibling;
+ Assert.equal(node.localName, "b");
+ node = node.firstChild;
+ Assert.equal(node.nodeValue, "hotness!");
+
+ // Now check that the original signature has been removed,
+ // and no blank lines got added!
+ node = contentFrame.contentDocument.body.firstChild;
+ let textNode;
+ if (paragraphFormat) {
+ textNode = node.firstChild;
+ } else {
+ textNode = node;
+ }
+ Assert.equal(textNode.nodeValue, "Body, first line.");
+ if (!paragraphFormat) {
+ node = node.nextSibling;
+ Assert.equal(node.localName, "br");
+ }
+ node = node.nextSibling;
+ // check that the signature is immediately after the message text.
+ Assert.equal(node.className, "moz-signature");
+ // check that that the signature is the last node.
+ Assert.equal(node, contentFrame.contentDocument.body.lastChild);
+
+ close_compose_window(cwc);
+}
+
+add_task(async function testHTMLComposeWindowSwitchSignatures() {
+ await HTMLComposeWindowSwitchSignatures(false, false);
+});
+
+add_task(
+ async function testHTMLComposeWindowSwitchSignaturesWithSuppressedSeparator() {
+ await HTMLComposeWindowSwitchSignatures(true, false);
+ }
+);
+
+add_task(async function testHTMLComposeWindowSwitchSignaturesParagraphFormat() {
+ await HTMLComposeWindowSwitchSignatures(false, true);
+});
+
+add_task(
+ async function testHTMLComposeWindowSwitchSignaturesWithSuppressedSeparatorParagraphFormat() {
+ await HTMLComposeWindowSwitchSignatures(true, true);
+ }
+);
diff --git a/comm/mail/test/browser/composition/browser_spelling.js b/comm/mail/test/browser/composition/browser_spelling.js
new file mode 100644
index 0000000000..86a1ba9c84
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_spelling.js
@@ -0,0 +1,311 @@
+/* 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 { close_compose_window, open_compose_new_mail } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var { maybeOnSpellCheck } = ChromeUtils.importESModule(
+ "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
+);
+
+async function checkMisspelledWords(editor, ...words) {
+ await new Promise(resolve => maybeOnSpellCheck({ editor }, resolve));
+
+ let selection = editor.selectionController.getSelection(
+ Ci.nsISelectionController.SELECTION_SPELLCHECK
+ );
+ Assert.equal(
+ selection.rangeCount,
+ words.length,
+ "correct number of misspellings"
+ );
+ for (let i = 0; i < words.length; i++) {
+ Assert.equal(selection.getRangeAt(i).toString(), words[i]);
+ }
+ return selection;
+}
+
+add_task(async function () {
+ // Install en-NZ dictionary.
+
+ let dictionary = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dictionary.initWithPath(getTestFilePath("data/en_NZ"));
+
+ let hunspell = Cc["@mozilla.org/spellchecker/engine;1"].getService(
+ Ci.mozISpellCheckingEngine
+ );
+ hunspell.addDirectory(dictionary);
+
+ // Open a compose window and write a message.
+
+ let cwc = open_compose_new_mail();
+ let composeWindow = cwc.window;
+ let composeDocument = composeWindow.document;
+
+ cwc.window.document.getElementById("msgSubject").focus();
+ EventUtils.sendString(
+ "I went to the harbor in an aluminium boat",
+ cwc.window
+ );
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString("I maneuvered to the center.\n", cwc.window);
+ cwc.window.document.getElementById("messageEditor").focus();
+ EventUtils.sendString(
+ "The sky was the colour of ochre and the stars shone like jewelry.\n",
+ cwc.window
+ );
+
+ // Check initial spelling.
+
+ let subjectEditor = composeDocument.getElementById("msgSubject").editor;
+ let editorBrowser = composeWindow.GetCurrentEditorElement();
+ let bodyEditor = composeWindow.GetCurrentEditor();
+ let saveButton = composeDocument.getElementById("button-save");
+
+ await checkMisspelledWords(subjectEditor, "aluminium");
+ await checkMisspelledWords(bodyEditor, "colour", "ochre");
+
+ // Check menu items are displayed correctly.
+
+ let shownPromise, hiddenPromise;
+ let contextMenu = composeDocument.getElementById("msgComposeContext");
+ let contextMenuEnabled = composeDocument.getElementById("spellCheckEnable");
+ let optionsMenu = composeDocument.getElementById("optionsMenu");
+ let optionsMenuEnabled = composeDocument.getElementById(
+ "menu_inlineSpellCheck"
+ );
+
+ if (AppConstants.platform != "macosx") {
+ shownPromise = BrowserTestUtils.waitForEvent(optionsMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(optionsMenu, {}, composeWindow);
+ await shownPromise;
+ Assert.equal(
+ optionsMenuEnabled.getAttribute("checked"),
+ "true",
+ "options menu item is checked"
+ );
+ hiddenPromise = BrowserTestUtils.waitForEvent(optionsMenu, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, composeWindow);
+ await hiddenPromise;
+ }
+
+ shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu" },
+ editorBrowser
+ );
+ await shownPromise;
+ Assert.equal(
+ contextMenuEnabled.getAttribute("checked"),
+ "true",
+ "context menu item is checked"
+ );
+
+ // Disable the spell checker.
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(contextMenuEnabled);
+ await hiddenPromise;
+
+ await checkMisspelledWords(subjectEditor);
+ await checkMisspelledWords(bodyEditor);
+
+ // Save the message. The spell checking state shouldn't change.
+
+ EventUtils.synthesizeMouseAtCenter(saveButton, {}, composeWindow);
+ // Clicking the button sets gWindowLocked to true synchronously, so if
+ // gWindowLocked is false, we know that saving has completed.
+ await TestUtils.waitForCondition(
+ () => !composeWindow.gWindowLocked,
+ "window unlocked after saving"
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ await checkMisspelledWords(subjectEditor);
+ await checkMisspelledWords(bodyEditor);
+
+ // Check menu items are displayed correctly.
+
+ if (AppConstants.platform != "macosx") {
+ shownPromise = BrowserTestUtils.waitForEvent(optionsMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(optionsMenu, {}, composeWindow);
+ await shownPromise;
+ Assert.ok(
+ !optionsMenuEnabled.hasAttribute("checked"),
+ "options menu item is not checked"
+ );
+ hiddenPromise = BrowserTestUtils.waitForEvent(optionsMenu, "popuphidden");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, composeWindow);
+ await hiddenPromise;
+ }
+
+ shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu" },
+ editorBrowser
+ );
+ await shownPromise;
+ Assert.equal(
+ contextMenuEnabled.getAttribute("checked"),
+ "false",
+ "context menu item is not checked"
+ );
+
+ // Enable the spell checker.
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(contextMenuEnabled);
+ await hiddenPromise;
+
+ await checkMisspelledWords(subjectEditor, "aluminium");
+ await checkMisspelledWords(bodyEditor, "colour", "ochre");
+
+ // Save the message. The spell checking state shouldn't change.
+
+ EventUtils.synthesizeMouseAtCenter(saveButton, {}, composeWindow);
+ // Clicking the button sets gWindowLocked to true synchronously, so if
+ // gWindowLocked is false, we know that saving has completed.
+ await TestUtils.waitForCondition(
+ () => !composeWindow.gWindowLocked,
+ "window unlocked after saving"
+ );
+
+ await checkMisspelledWords(subjectEditor, "aluminium");
+ await checkMisspelledWords(bodyEditor, "colour", "ochre");
+
+ // Add language.
+
+ let statusButton = composeDocument.getElementById("languageStatusButton");
+ let languageList = composeDocument.getElementById("languageMenuList");
+
+ shownPromise = BrowserTestUtils.waitForEvent(languageList, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(statusButton, {}, composeWindow);
+ await shownPromise;
+
+ Assert.equal(languageList.childElementCount, 4);
+ Assert.equal(languageList.children[0].value, "en-NZ");
+ Assert.equal(languageList.children[0].getAttribute("checked"), "false");
+ Assert.equal(languageList.children[1].value, "en-US");
+ Assert.equal(languageList.children[1].getAttribute("checked"), "true");
+ Assert.equal(languageList.children[2].localName, "menuseparator");
+ Assert.equal(
+ languageList.children[3].dataset.l10nId,
+ "spell-add-dictionaries"
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(languageList, "popuphidden");
+ languageList.activateItem(languageList.children[0]);
+ await TestUtils.waitForCondition(
+ () => languageList.children[0].getAttribute("checked") == "true",
+ "en-NZ menu item checked"
+ );
+ await TestUtils.waitForCondition(
+ () => composeWindow.gActiveDictionaries.has("en-NZ"),
+ "en-NZ added to dictionaries"
+ );
+ languageList.hidePopup();
+ await hiddenPromise;
+
+ Assert.deepEqual(
+ [...composeWindow.gActiveDictionaries],
+ ["en-US", "en-NZ"],
+ "correct dictionaries active"
+ );
+ await checkMisspelledWords(subjectEditor);
+ await checkMisspelledWords(bodyEditor);
+
+ // Remove language.
+
+ shownPromise = BrowserTestUtils.waitForEvent(languageList, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(statusButton, {}, composeWindow);
+ await shownPromise;
+
+ Assert.equal(languageList.childElementCount, 4);
+ Assert.equal(languageList.children[0].value, "en-NZ");
+ Assert.equal(languageList.children[0].getAttribute("checked"), "true");
+ Assert.equal(languageList.children[1].value, "en-US");
+ Assert.equal(languageList.children[1].getAttribute("checked"), "true");
+ Assert.equal(languageList.children[2].localName, "menuseparator");
+ Assert.equal(
+ languageList.children[3].dataset.l10nId,
+ "spell-add-dictionaries"
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(languageList, "popuphidden");
+ languageList.activateItem(languageList.children[1]);
+ await TestUtils.waitForCondition(
+ () => !languageList.children[1].hasAttribute("checked"),
+ "en-US menu item unchecked"
+ );
+ await TestUtils.waitForCondition(
+ () => !composeWindow.gActiveDictionaries.has("en-US"),
+ "en-US removed from dictionaries"
+ );
+ languageList.hidePopup();
+ await hiddenPromise;
+
+ Assert.deepEqual(
+ [...composeWindow.gActiveDictionaries],
+ ["en-NZ"],
+ "correct dictionaries active"
+ );
+ await checkMisspelledWords(subjectEditor, "harbor");
+ let words = await checkMisspelledWords(
+ bodyEditor,
+ "maneuvered",
+ "center",
+ "jewelry"
+ );
+
+ // Check that opening the context menu on a spelling error works as expected.
+
+ let box = words.getRangeAt(1).getBoundingClientRect();
+ shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtPoint(
+ box.left + box.width / 2,
+ box.top + box.height / 2,
+ { type: "contextmenu" },
+ editorBrowser
+ );
+ await shownPromise;
+
+ let menuItem = composeDocument.getElementById("spellCheckNoSuggestions");
+ Assert.ok(BrowserTestUtils.is_hidden(menuItem));
+
+ let suggestions = contextMenu.querySelectorAll(".spell-suggestion");
+ Assert.greater(suggestions.length, 0);
+ Assert.equal(suggestions[0].value, "centre");
+
+ for (let id of [
+ "spellCheckAddSep",
+ "spellCheckAddToDictionary",
+ "spellCheckIgnoreWord",
+ "spellCheckSuggestionsSeparator",
+ ]) {
+ menuItem = composeDocument.getElementById(id);
+ Assert.ok(BrowserTestUtils.is_visible(menuItem));
+ }
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(suggestions[0]);
+ await hiddenPromise;
+
+ await checkMisspelledWords(bodyEditor, "maneuvered", "jewelry");
+ await SpecialPowers.spawn(editorBrowser, [], () => {
+ Assert.ok(
+ content.document.body.textContent.startsWith(
+ "I maneuvered to the centre."
+ )
+ );
+ });
+
+ // Clean up.
+
+ close_compose_window(cwc);
+ hunspell.removeDirectory(dictionary);
+});
diff --git a/comm/mail/test/browser/composition/browser_subjectWas.js b/comm/mail/test/browser/composition/browser_subjectWas.js
new file mode 100644
index 0000000000..c76a719ce5
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_subjectWas.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+/**
+ * Tests that replying in to mail with subject change (was: old) style will
+ * do the right thing.
+ */
+
+"use strict";
+
+var { close_compose_window, open_compose_with_reply } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ComposeHelpers.jsm"
+);
+var {
+ add_message_to_folder,
+ assert_selected_and_displayed,
+ be_in_folder,
+ create_folder,
+ create_message,
+ mc,
+ select_click_row,
+} = ChromeUtils.import(
+ "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
+);
+
+var folder = null;
+
+add_setup(async function () {
+ folder = await create_folder("SubjectWas");
+ await add_message_to_folder(
+ [folder],
+ create_message({
+ subject: "New subject (was: Old subject)",
+ body: { body: "Testing thread subject switch reply." },
+ clobberHeaders: {
+ References: "<97010db3-bd55-34e0-b08b-841b2a9ff0ec@test>",
+ },
+ })
+ );
+ registerCleanupFunction(() => folder.deleteSelf(null));
+});
+
+/**
+ * Test that the subject is set properly in the replied message.
+ */
+add_task(async function test_was_reply_subj() {
+ await be_in_folder(folder);
+
+ let msg = select_click_row(0);
+ assert_selected_and_displayed(mc, msg);
+
+ let cwc = open_compose_with_reply();
+
+ let msgSubject = cwc.window.document.getElementById("msgSubject").value;
+
+ // Subject should be Re: <the original subject stripped of the was: part>
+ Assert.equal(
+ msgSubject,
+ "Re: New subject",
+ "was: part of subject should have been removed"
+ );
+
+ close_compose_window(cwc);
+});
diff --git a/comm/mail/test/browser/composition/browser_text_styling.js b/comm/mail/test/browser/composition/browser_text_styling.js
new file mode 100644
index 0000000000..88c31c3040
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_text_styling.js
@@ -0,0 +1,609 @@
+/* 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/. */
+
+/**
+ * Test styling messages.
+ */
+
+requestLongerTimeout(3);
+
+var { close_compose_window, open_compose_new_mail, FormatHelper } =
+ ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
+
+add_task(async function test_style_buttons() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let buttonSet = [
+ { name: "bold", tag: "B", node: formatHelper.boldButton },
+ { name: "italic", tag: "I", node: formatHelper.italicButton },
+ { name: "underline", tag: "U", node: formatHelper.underlineButton },
+ ];
+
+ // Without focus on message.
+ for (let button of buttonSet) {
+ Assert.ok(
+ button.node.disabled,
+ `${button.name} button should be disabled with no focus`
+ );
+ }
+
+ formatHelper.focusMessage();
+
+ // With focus on message.
+ for (let button of buttonSet) {
+ Assert.ok(
+ !button.node.disabled,
+ `${button.name} button should be enabled with focus`
+ );
+ }
+
+ async function selectTextAndToggleButton(
+ start,
+ end,
+ button,
+ enables,
+ message
+ ) {
+ await formatHelper.selectTextRange(start, end);
+ await formatHelper.assertShownStyles(
+ enables ? null : [button.name],
+ `${message}: Before toggle`
+ );
+ button.node.click();
+ await formatHelper.assertShownStyles(
+ enables ? [button.name] : null,
+ `${message}: Before toggle`
+ );
+ }
+
+ for (let button of buttonSet) {
+ let name = button.name;
+ let tags = new Set();
+ tags.add(button.tag);
+
+ await formatHelper.assertShownStyles(
+ null,
+ `No shown styles at the start (${name})`
+ );
+ button.node.click();
+ await formatHelper.assertShownStyles(
+ [name],
+ `${name} is shown after clicking`
+ );
+ let text = `test-${button.name}`;
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownStyles(
+ [name],
+ `${name} is shown after clicking and typing`
+ );
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }],
+ `Clicking ${name} button and typing`
+ );
+
+ // Stop styling on click.
+ let addedText = "not-styled";
+ button.node.click();
+ await formatHelper.assertShownStyles(
+ null,
+ `No longer ${name} after re-clicking`
+ );
+ await formatHelper.typeInMessage(addedText);
+ await formatHelper.assertShownStyles(
+ null,
+ `No longer ${name} after re-clicking and typing`
+ );
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }, addedText],
+ `Unclicking ${name} button and typing`
+ );
+
+ await formatHelper.deleteTextRange(
+ text.length,
+ text.length + addedText.length
+ );
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }],
+ `Removed non-${name} region`
+ );
+
+ // Undo in region.
+ await selectTextAndToggleButton(
+ 1,
+ 3,
+ button,
+ false,
+ `Unchecking ${button.name} button for some region`
+ );
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: "t" }, "es", { tags, text: `t-${button.name}` }],
+ `After unchecking ${button.name} button for some region`
+ );
+
+ // Redo over region.
+ await selectTextAndToggleButton(
+ 1,
+ 3,
+ button,
+ true,
+ `Rechecking ${button.name} button for some region`
+ );
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }],
+ `After rechecking ${button.name} button for some region`
+ );
+
+ // Undo over whole text
+ await selectTextAndToggleButton(
+ 0,
+ text.length,
+ button,
+ false,
+ `Unchecking ${button.name} button for whole text`
+ );
+ formatHelper.assertMessageParagraph(
+ [text],
+ `After unchecking ${button.name} button for whole text`
+ );
+
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_multi_style_with_buttons() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ let boldButton = formatHelper.boldButton;
+ let italicButton = formatHelper.italicButton;
+ let underlineButton = formatHelper.underlineButton;
+
+ formatHelper.focusMessage();
+
+ let parts = ["bold", " and italic", " and underline"];
+
+ boldButton.click();
+ await formatHelper.typeInMessage(parts[0]);
+ formatHelper.assertMessageParagraph(
+ [{ tags: ["B"], text: parts[0] }],
+ "Added bold"
+ );
+ await formatHelper.assertShownStyles(
+ ["bold"],
+ "After clicking bold and typing"
+ );
+
+ italicButton.click();
+ await formatHelper.typeInMessage(parts[1]);
+ formatHelper.assertMessageParagraph(
+ [
+ { tags: ["B"], text: parts[0] },
+ { tags: ["B", "I"], text: parts[1] },
+ ],
+ "Added italic"
+ );
+ await formatHelper.assertShownStyles(
+ ["bold", "italic"],
+ "After clicking italic and typing"
+ );
+
+ underlineButton.click();
+ await formatHelper.typeInMessage(parts[2]);
+ formatHelper.assertMessageParagraph(
+ [
+ { tags: ["B"], text: parts[0] },
+ { tags: ["B", "I"], text: parts[1] },
+ { tags: ["B", "I", "U"], text: parts[2] },
+ ],
+ "Added underline"
+ );
+ await formatHelper.assertShownStyles(
+ ["bold", "italic", "underline"],
+ "After clicking underline and typing"
+ );
+
+ await formatHelper.selectTextRange(2, parts[0].length + parts[1].length + 2);
+ await formatHelper.assertShownStyles(
+ ["bold"],
+ "Only bold when selecting all bold, mixed italic and mixed underline"
+ );
+
+ // Remove bold over selection.
+ boldButton.click();
+ formatHelper.assertMessageParagraph(
+ [
+ { tags: ["B"], text: parts[0].slice(0, 2) },
+ parts[0].slice(2),
+ { tags: ["I"], text: parts[1] },
+ { tags: ["I", "U"], text: parts[2].slice(0, 2) },
+ { tags: ["B", "I", "U"], text: parts[2].slice(2) },
+ ],
+ "Removed bold in middle"
+ );
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_text_styling_whilst_typing() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ await formatHelper.assertShownStyles(null, "None checked");
+
+ for (let style of formatHelper.styleDataMap.values()) {
+ let tags = new Set();
+ tags.add(style.tag);
+ let name = style.name;
+
+ // Start styling.
+ await formatHelper.selectStyle(style);
+
+ // See Bug 1716840.
+ // await formatHelper.assertShownStyles(style, `${name} selected`);
+ let text = `test-${name}`;
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownStyles(style, `${name} selected and typing`);
+ formatHelper.assertMessageParagraph([{ tags, text }], `Selecting ${name}`);
+
+ // Stop styling.
+ await formatHelper.selectStyle(style);
+ // See Bug 1716840.
+ // await formatHelper.assertShownStyles(null, `${name} unselected`);
+ let addedText = "not-styled";
+ await formatHelper.typeInMessage(addedText);
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }, addedText],
+ `Unselecting ${name}`
+ );
+ await formatHelper.assertShownStyles(null, `${name} unselected and typing`);
+
+ // Select these again to unselect for next loop cycle. Needs to be done
+ // before empty paragraph since they happen only after "typing".
+ if (style.linked) {
+ await formatHelper.selectStyle(style.linked);
+ }
+ if (style.implies) {
+ await formatHelper.selectStyle(style.implies);
+ }
+ await formatHelper.emptyParagraph();
+
+ // Select again to unselect for next loop cycle.
+ await formatHelper.selectStyle(style);
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_text_styling_update_on_selection_change() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ for (let style of formatHelper.styleDataMap.values()) {
+ let tags = new Set();
+ tags.add(style.tag);
+ let name = style.name;
+
+ // Start styling.
+ await formatHelper.selectStyle(style);
+ let text = `test-${name}`;
+ await formatHelper.typeInMessage(text);
+ // Stop styling.
+ await formatHelper.selectStyle(style);
+ let addedText = "not-styled";
+ await formatHelper.typeInMessage("not-styled");
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }, addedText],
+ `Unselecting ${name} and typing`
+ );
+
+ await formatHelper.assertShownStyles(null, `${name} unselected at end`);
+
+ // Test selections.
+ for (let [start, end, forward, expect] of [
+ // Make sure we toggle, so the test does not capture the previous state.
+ [0, null, true, true], // At start.
+ [0, text.length + 1, true, false], // Mixed is unchecked.
+ [text.length, null, true, true], // On boundary travelling forward.
+ [text.length, null, false, false], // On boundary travelling backward.
+ [2, 4, true, true], // In the styled region.
+ [text.length, text.length + 1, true, false], // In the unstyled region.
+ ]) {
+ await formatHelper.selectTextRange(start, end, forward);
+ if (expect) {
+ expect = style;
+ } else {
+ expect = null;
+ }
+ await formatHelper.assertShownStyles(
+ expect,
+ `Selecting with ${name} style, from ${start} to ${end} ` +
+ `${forward ? "forwards" : "backwards"}`
+ );
+ }
+ if (style.linked) {
+ await formatHelper.selectStyle(style.linked);
+ }
+ if (style.implies) {
+ await formatHelper.selectStyle(style.implies);
+ }
+ await formatHelper.emptyParagraph();
+ // Select again to unselect for next loop cycle.
+ await formatHelper.selectStyle(style);
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_text_styling_on_selections() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ let start;
+ let end = 0;
+ let parts = [];
+ let fullText = "";
+ for (let text of ["test for ", "styling some", " selections"]) {
+ start = end;
+ end += text.length;
+ parts.push({ text, start, end });
+ fullText += text;
+ }
+ await formatHelper.typeInMessage(fullText);
+ formatHelper.assertMessageParagraph([fullText], "No styling at start");
+
+ for (let style of formatHelper.styleDataMap.values()) {
+ let tags = new Set();
+ tags.add(style.tag);
+ let name = style.name;
+
+ formatHelper.assertMessageParagraph(
+ [fullText],
+ `No ${name} style at start`
+ );
+
+ await formatHelper.selectTextRange(parts[1].start, parts[1].end);
+ await formatHelper.selectStyle(style);
+ formatHelper.assertMessageParagraph(
+ [parts[0].text, { tags, text: parts[1].text }, parts[2].text],
+ `${name} in the middle`
+ );
+
+ await formatHelper.selectTextRange(parts[0].start, parts[2].end);
+ await formatHelper.selectStyle(style);
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: fullText }],
+ `${name} on all`
+ );
+
+ // Undo in region.
+ await formatHelper.selectTextRange(parts[1].start, parts[1].end);
+ await formatHelper.selectStyle(style);
+ formatHelper.assertMessageParagraph(
+ [
+ { tags, text: parts[0].text },
+ parts[1].text,
+ { tags, text: parts[2].text },
+ ],
+ `${name} not in the middle`
+ );
+
+ // Redo over region.
+ await formatHelper.selectTextRange(parts[1].start, parts[1].end);
+ await formatHelper.selectStyle(style);
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: fullText }],
+ `${name} on all again`
+ );
+
+ // Reset by unselecting all again.
+ await formatHelper.selectTextRange(parts[0].start, parts[2].end);
+ await formatHelper.selectStyle(style);
+ }
+
+ formatHelper.assertMessageParagraph([fullText], "No style at end");
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_induced_text_styling() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ for (let style of formatHelper.styleDataMap.values()) {
+ if (!style.implies && !style.linked) {
+ continue;
+ }
+ let tags = new Set();
+ tags.add(style.tag);
+ let name = style.name;
+
+ // Start styling.
+ await formatHelper.selectStyle(style);
+ let text = `test-${name}`;
+ await formatHelper.typeInMessage(text);
+ await formatHelper.assertShownStyles(style, `${name} initial text`);
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }],
+ `${name} initial text`
+ );
+
+ if (style.implies) {
+ // Unselecting implied styles will be ignored.
+ let desc = `${style.implies.name} implied by ${name}`;
+ await formatHelper.selectTextRange(0, text.length);
+ await formatHelper.assertShownStyles(
+ style,
+ `Before trying to deselect ${desc}`
+ );
+
+ await formatHelper.selectStyle(style.implies);
+ formatHelper.assertMessageParagraph(
+ [{ tags, text }],
+ `After trying to deselect ${desc}`
+ );
+ await formatHelper.assertShownStyles(
+ style,
+ `After trying to deselect ${desc}`
+ );
+ }
+ if (style.linked) {
+ // Unselecting the linked style also unselects the current one.
+ let desc = `${style.linked.name} linked from ${name}`;
+ await formatHelper.selectTextRange(0, text.length);
+ await formatHelper.assertShownStyles(style, `Before unselecting ${desc}`);
+ await formatHelper.selectStyle(style.linked);
+
+ formatHelper.assertMessageParagraph([text], `After unselecting ${desc}`);
+ await formatHelper.assertShownStyles(null, `After unselecting ${desc}`);
+ }
+
+ await formatHelper.emptyParagraph();
+ // Select again to unselect for next loop cycle.
+ if (style.linked) {
+ await formatHelper.selectStyle(style.linked);
+ }
+ if (style.implies) {
+ await formatHelper.selectStyle(style.implies);
+ }
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
+
+add_task(async function test_fixed_width_text_styling_font_change() {
+ let controller = open_compose_new_mail();
+ let formatHelper = new FormatHelper(controller.window);
+
+ formatHelper.focusMessage();
+
+ await formatHelper.assertShownFont("", "Variable width to start");
+ for (let style of formatHelper.styleDataMap.values()) {
+ if (
+ style.name !== "tt" &&
+ style.linked?.name !== "tt" &&
+ style.implies?.name !== "tt"
+ ) {
+ continue;
+ }
+
+ let tags = new Set();
+ tags.add(style.tag);
+ let name = style.name;
+
+ // Start styling.
+ await formatHelper.selectStyle(style);
+ // See Bug 1716840.
+ // await formatHelper.assertShownFont(
+ // "monospace",
+ // `monospace when ${name} selected`
+ // );
+
+ await formatHelper.typeInMessage(`test-${name}`);
+ await formatHelper.assertShownFont(
+ "monospace",
+ `monospace when ${name} selected and typing`
+ );
+
+ // Stop styling.
+ await formatHelper.selectStyle(style);
+ // See Bug 1716840.
+ // await formatHelper.assertShownFont(
+ // "",
+ // `Variable Width when ${name} unselected`
+ // );
+
+ await formatHelper.typeInMessage("test-none");
+ await formatHelper.assertShownFont(
+ "",
+ `Variable Width when ${name} unselected and typing`
+ );
+
+ await formatHelper.selectTextRange(1, 3);
+ await formatHelper.assertShownFont(
+ "monospace",
+ `monospace when ${name} region highlighted`
+ );
+ // Select the same font does nothing
+ await formatHelper.selectFont("monospace");
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: `test-${name}` }, "test-none"],
+ `No change when ${name} region has monospace selected`
+ );
+
+ // Try to change the font selection to variable width.
+ await formatHelper.selectFont("");
+ if (name === "tt") {
+ // "tt" style is removed.
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: "t" }, "es", { tags, text: `t-${name}` }, "test-none"],
+ `variable width when ${name} region has font unset`
+ );
+ await formatHelper.assertShownFont(
+ "",
+ `Variable Width when ${name} region has font unset`
+ );
+ // Reset by selecting the style.
+ // Note: Reselecting the "monospace" font will not add the tt style back.
+ await formatHelper.selectStyle(style);
+ }
+ // Otherwise, the style is unchanged.
+ formatHelper.assertMessageParagraph(
+ [{ tags, text: `test-${name}` }, "test-none"],
+ `Still ${name} style in region`
+ );
+ await formatHelper.assertShownFont(
+ "monospace",
+ `Still monospace for ${name} region`
+ );
+
+ // Change the font to something else.
+ let font = formatHelper.commonFonts[0];
+ await formatHelper.selectFont(font);
+ // Doesn't remove the style, but adds the font.
+ formatHelper.assertMessageParagraph(
+ [
+ { tags, text: "t" },
+ // See Bug 1718779
+ // Whilst the font covers this region, it is actually suppressed by the
+ // styling tags. In this case, the ordering of the <font> and, e.g.,
+ // <tt> element matters.
+ { tags, font, text: "es" },
+ { tags, text: `t-${name}` },
+ "test-none",
+ ],
+ `"${font}" when ${name} region has font set`
+ );
+ // See Bug 1718779
+ // The desired font is shown at first, but then switches to Fixed Width
+ // again.
+ // await formatHelper.assertShownFont(
+ // font,
+ // `"${font}" when ${name} region has font set`
+ //);
+
+ await formatHelper.emptyParagraph();
+ // Select again to unselect for next loop cycle.
+ if (style.linked) {
+ await formatHelper.selectStyle(style.linked);
+ }
+ if (style.implies) {
+ await formatHelper.selectStyle(style.implies);
+ }
+ await formatHelper.emptyParagraph();
+ }
+
+ close_compose_window(controller);
+});
diff --git a/comm/mail/test/browser/composition/browser_xUnsent.js b/comm/mail/test/browser/composition/browser_xUnsent.js
new file mode 100644
index 0000000000..dd3be86294
--- /dev/null
+++ b/comm/mail/test/browser/composition/browser_xUnsent.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/**
+ * Tests that X-Unsent .eml messages are correctly opened for composition.
+ */
+
+"use strict";
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+function waitForComposeWindow() {
+ return BrowserTestUtils.domWindowOpened(null, async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ await BrowserTestUtils.waitForEvent(win, "focus", true);
+ return (
+ win.document.documentURI ===
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml"
+ );
+ });
+}
+
+/**
+ * Tests that opening an .eml with X-Unsent: 1 opens composition correctly.
+ */
+add_task(async function openXUnsent() {
+ let compWinReady = waitForComposeWindow();
+ let file = new FileUtils.File(getTestFilePath(`data/xunsent.eml`));
+ let fileURL = Services.io
+ .newFileURI(file)
+ .QueryInterface(Ci.nsIFileURL)
+ .mutate()
+ .setQuery("type=application/x-message-display")
+ .finalize();
+ MailUtils.openEMLFile(window, file, fileURL);
+ let compWin = await compWinReady;
+
+ Assert.equal(
+ compWin.document.getElementById("msgSubject").value,
+ "xx unsent",
+ "Should open as draft with correct subject"
+ );
+ compWin.close();
+});
diff --git a/comm/mail/test/browser/composition/data/attachment.txt b/comm/mail/test/browser/composition/data/attachment.txt
new file mode 100644
index 0000000000..e5dde9a9f2
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/attachment.txt
@@ -0,0 +1,4 @@
+"Attachment is the great fabricator of illusions; reality can be attained only
+ by someone who is detached."
+
+ -- Simone Weil
diff --git a/comm/mail/test/browser/composition/data/base64-bug1586890.eml b/comm/mail/test/browser/composition/data/base64-bug1586890.eml
new file mode 100644
index 0000000000..b79957b6c0
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/base64-bug1586890.eml
@@ -0,0 +1,25 @@
+Date: Tue, 31 Aug 2018 16:33:00 +0200
+From: From <from@example.com>
+To: To <to@example.com>
+Subject: Bug 1586890 - BASE64 MIME body and attachment (UTF-16BE) with invalid charset
+MIME-Version: 1.0
+Message-ID: <1dcZe4@example.com>
+Content-Type: multipart/mixed;
+ boundary="------------DA562B250842CC7332F16476"
+
+This is a multi-part message in MIME format.
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=BadCharset
+Content-Transfer-Encoding: base64
+
+AGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0ADQAKAG4AbwBwAHEAcgBzAHQAdQB2AHcAeAB5
+AHo=
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=BadCharset
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="test.txt"
+
+AGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0ADQAKAG4AbwBwAHEAcgBzAHQAdQB2AHcAeAB5
+AHo=
+--------------DA562B250842CC7332F16476--
diff --git a/comm/mail/test/browser/composition/data/base64-encoded-msg.eml b/comm/mail/test/browser/composition/data/base64-encoded-msg.eml
new file mode 100644
index 0000000000..e2a37fbe85
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/base64-encoded-msg.eml
@@ -0,0 +1,11 @@
+Message-ID: <4877F4BB.3060507@example.org>
+Date: Fri, 11 Jul 2008 20:03:07 -0400
+From: Joe <joe@example.org>
+MIME-Version: 1.0
+To: Jane <jane@example.org>
+Subject: base64 content
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: base64
+
+WW91IGhhdmUgZGVjb2RlZCB0aGlzIHRleHQgZnJvbSBiYXNlNjQu
+
diff --git a/comm/mail/test/browser/composition/data/base64-with-whitespace.eml b/comm/mail/test/browser/composition/data/base64-with-whitespace.eml
new file mode 100644
index 0000000000..f610203558
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/base64-with-whitespace.eml
@@ -0,0 +1,46 @@
+Date: Tue, 31 Aug 2018 16:33:00 +0200
+From: From <from@example.com>
+To: To <to@example.com>
+Subject: Bug 1487421 - BASE64 MIME body and attachment with empty lines in between
+MIME-Version: 1.0
+Message-ID: <1dcZe4@example.com>
+Content-Type: multipart/mixed;
+ boundary="------------DA562B250842CC7332F16476"
+
+This is a multi-part message in MIME format.
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+YWJj
+
+ZG
+
+V
+
+mZ2hpamtsbW
+
+5vcHFyc3R1dnd4
+
+eXo=
+
+--------------DA562B250842CC7332F16476
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="test.txt"
+
+YWJj
+
+ZG
+
+V
+
+mZ2hpamtsbW
+
+5vcHFyc3R1dnd4
+
+eXo=
+
+--------------DA562B250842CC7332F16476--
+
diff --git a/comm/mail/test/browser/composition/data/body-greek.eml b/comm/mail/test/browser/composition/data/body-greek.eml
new file mode 100644
index 0000000000..d2b1764e9b
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/body-greek.eml
@@ -0,0 +1,9 @@
+From: test <test@example.com>
+Subject: test reply to ISO-8859-7 encoded message
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-7
+Content-Transfer-Encoding: quoted-printable
+
+Here comes some Greek text: =CA=E1=EB=E7=F3=F0=DD=F1=E1
diff --git a/comm/mail/test/browser/composition/data/body-utf16.eml b/comm/mail/test/browser/composition/data/body-utf16.eml
new file mode 100644
index 0000000000..2d72a8dc71
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/body-utf16.eml
@@ -0,0 +1,10 @@
+From: test <test@example.com>
+Subject: test reply to UTF-16 encoded message
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-16LE;
+Content-Transfer-Encoding: base64
+
+//5IAGkAIABNAGEAZwBuAHUAcwAsACAAaABlAHIAZQAgAGEAIABiAGUAdAB0AGUAcgAgAFUA
+VABGAC0AMQA2ACAAZQBuAGMAbwBkAGUAZAAgAG0AYQBpAGwALgA=
diff --git a/comm/mail/test/browser/composition/data/charset-cp932.eml b/comm/mail/test/browser/composition/data/charset-cp932.eml
new file mode 100644
index 0000000000..1e8fc33123
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/charset-cp932.eml
@@ -0,0 +1,11 @@
+Date: Tue, 4 Dec 2018 15:23:22 +0900
+From: from@example.org
+Subject: =?cp932?Q?=82=b1=82=b1=82=c9=96=7b=95=b6=82=aa=82=ab=82=dc=82=b7=81=42?=
+To: to@example.org
+Message-Id: <424F4F74-05B4-4575-8B0D-473334183C69@example.org>
+Mime-Version: 1.0 (1.0)
+Content-Type: text/plain; charset=cp932
+Content-Transfer-Encoding: 8bit
+
+ここに本文がきます。
+
diff --git a/comm/mail/test/browser/composition/data/content-utf8-alt-rel.eml b/comm/mail/test/browser/composition/data/content-utf8-alt-rel.eml
new file mode 100644
index 0000000000..ccf457ad2b
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/content-utf8-alt-rel.eml
@@ -0,0 +1,46 @@
+From: test <test@example.com>
+Subject: test multipart, alternative first
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------alternative"
+
+This is a multi-part message in MIME format.
+--------------alternative
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+テ。テウテコテ、テカテシテ
+
+--------------alternative
+Content-Type: multipart/related;
+ boundary="------------related"
+
+
+--------------related
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+<html attr>
+ <!-- This also needs to work when the html tag has an attribute -->
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ テ。テウテコテ、テカテシテ<br>
+ <img src="cid:part1" alt=""><br>
+ </body>
+</html>
+
+--------------related
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-ID: <part1>
+
+iVBORw0KGgoAAAANSUhEUgAAAAYAAAALCAIAAADTMGvBAAAAEUlEQVQImWPgsi5DQwxDWggA
+lCEwN+YGfiYAAAAASUVORK5CYII=
+--------------related--
+
+--------------alternative--
+
diff --git a/comm/mail/test/browser/composition/data/content-utf8-alt-rel2.eml b/comm/mail/test/browser/composition/data/content-utf8-alt-rel2.eml
new file mode 100644
index 0000000000..0a2df0cd9e
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/content-utf8-alt-rel2.eml
@@ -0,0 +1,46 @@
+From: test <test@example.com>
+Subject: test multipart, alternative first
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="------------alternative"
+
+This is a multi-part message in MIME format.
+--------------alternative
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+テ。テウテコテ、テカテシテ
+
+--------------alternative
+Content-Type: multipart/related;
+ boundary="------------related"
+
+
+--------------related
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+テ。テウテコテ、テカテシテ<br>
+<html>
+ <!-- This also needs to work when there is content before the html tag :-( -->
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ <img src="cid:part1" alt=""><br>
+ </body>
+</html>
+
+--------------related
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-ID: <part1>
+
+iVBORw0KGgoAAAANSUhEUgAAAAYAAAALCAIAAADTMGvBAAAAEUlEQVQImWPgsi5DQwxDWggA
+lCEwN+YGfiYAAAAASUVORK5CYII=
+--------------related--
+
+--------------alternative--
+
diff --git a/comm/mail/test/browser/composition/data/content-utf8-rel-alt.eml b/comm/mail/test/browser/composition/data/content-utf8-rel-alt.eml
new file mode 100644
index 0000000000..3b43154516
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/content-utf8-rel-alt.eml
@@ -0,0 +1,40 @@
+From: test <test@example.com>
+Subject: test multipart, related first
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: multipart/related;
+ boundary="------------related"
+
+--------------related
+Content-Type: multipart/alternative;
+ boundary="------------alternative"
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+テ。テウテコテ、テカテシテ
+
+--------------alternative
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+ <!-- This also needs to work when there is no html tag -->
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ テ。テウテコテ、テカテシテ<br>
+ <img src="cid:part1" alt=""><br>
+ </body>
+
+--------------alternative
+
+--------------related
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-ID: <part1>
+
+iVBORw0KGgoAAAANSUhEUgAAAAYAAAALCAIAAADTMGvBAAAAEUlEQVQImWPgsi5DQwxDWggA
+lCEwN+YGfiYAAAAASUVORK5CYII=
+
+--------------related--
diff --git a/comm/mail/test/browser/composition/data/content-utf8-rel-only.eml b/comm/mail/test/browser/composition/data/content-utf8-rel-only.eml
new file mode 100644
index 0000000000..17e773859e
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/content-utf8-rel-only.eml
@@ -0,0 +1,32 @@
+From: test <test@example.com>
+Subject: test HTML related only
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: multipart/related;
+ boundary="------------related"
+
+This is a multi-part message in MIME format.
+--------------related
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ テ。テウテコテ、テカテシテ<br>
+ <img src="cid:part1" id="cidImage" width="10" height="10" alt="">
+ <img src="cid:part1" crossorigin="anonymous" id="cidImageOrigin" width="20" height="20" alt="">
+ </body>
+</html>
+
+--------------related
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+Content-ID: <part1>
+
+iVBORw0KGgoAAAANSUhEUgAAAAYAAAALCAIAAADTMGvBAAAAEUlEQVQImWPgsi5DQwxDWggA
+lCEwN+YGfiYAAAAASUVORK5CYII=
+--------------related--
diff --git a/comm/mail/test/browser/composition/data/defective-charset.eml b/comm/mail/test/browser/composition/data/defective-charset.eml
new file mode 100644
index 0000000000..7b5c431294
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/defective-charset.eml
@@ -0,0 +1,28 @@
+To: me@example.com
+From: you@example.com
+Subject: Test message with Spanish text, no encoding, is windows-1250 (as detected)
+Date: Tue, 20 Apr 2021 15:20:34 -0500
+MIME-Version: 1.0
+Content-Type: text/plain;
+Content-Transfer-Encoding: 8bit
+Content-Language: en-US
+
+A lo largo de mi vida he sido testigo de muchas historias, experiencias
+e interpretaciones de la vida. Y me doy cuenta de que lo que m疽 aprecio
+es la honestidad. En estos ltimos meses siento que se ha polarizado an
+m疽 esta gran diferencias: las personas que son honestas consigo mismas,
+incluso en su deshonestidad, y las que no. Las personas honestas son
+aquellas dueas y responsables de su vida. Que no buscan una autoridad
+externa que les gue, que les proteja, que les cuide. Las personas
+deshonestas son aquellas que se creen vctimas de la vida, que culpan al
+otro o a los otros de sus 電esgracias. Que buscan fuera lo que solo
+pueden encontrar dentro. A lo largo de mi vida he sido testigo de muchas
+historias, experiencias e interpretaciones de la vida. Y me doy cuenta
+de que lo que m疽 aprecio es la honestidad. En estos ltimos meses
+siento que se ha polarizado an m疽 esta gran diferencias: las personas
+que son honestas consigo mismas, incluso en su deshonestidad, y las que
+no. Las personas honestas son aquellas dueas y responsables de su vida.
+Que no buscan una autoridad externa que les gue, que les proteja, que
+les cuide. Las personas deshonestas son aquellas que se creen vctimas
+de la vida, que culpan al otro o a los otros de sus 電esgracias. Que
+buscan fuera lo que solo pueden encontrar dentro.
diff --git a/comm/mail/test/browser/composition/data/en_NZ/en_NZ.aff b/comm/mail/test/browser/composition/data/en_NZ/en_NZ.aff
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/en_NZ/en_NZ.aff
diff --git a/comm/mail/test/browser/composition/data/en_NZ/en_NZ.dic b/comm/mail/test/browser/composition/data/en_NZ/en_NZ.dic
new file mode 100644
index 0000000000..9c4f5d1758
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/en_NZ/en_NZ.dic
@@ -0,0 +1,23 @@
+23
+aluminium
+an
+and
+boat
+centre
+colour
+harbour
+I
+in
+jewellery
+like
+manoeuvred
+ochre
+of
+shone
+sky
+stars
+the
+The
+to
+was
+went
diff --git a/comm/mail/test/browser/composition/data/evil-meta-msg.eml b/comm/mail/test/browser/composition/data/evil-meta-msg.eml
new file mode 100644
index 0000000000..82de0bac65
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/evil-meta-msg.eml
@@ -0,0 +1,11 @@
+From: Fuzz <fizz@example.org>
+To: contact@example.org
+Subject: test case 17
+Date: Wed, 11 May 2024 14:31:59 +0000
+Content-Type: text/html
+
+<html>
+<meta http-equiv="refresh" content="0;URL='http://localhost:8090'" />
+<meta http-equiv="refresh" content="1;URL='http://localhost:8091'" />
+<div id="demo">KABOOM!</demo>
+<object onerror="alert(1); document.getElementById('demo').innerHTML=parent.JSON.stringify(Object.getOwnPropertyNames(parent));" data="notarealaddress" width="400" height="300"></object>
diff --git a/comm/mail/test/browser/composition/data/feed-message.eml b/comm/mail/test/browser/composition/data/feed-message.eml
new file mode 100644
index 0000000000..66f81a65ff
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/feed-message.eml
@@ -0,0 +1,26 @@
+From - Fri, 27 Jan 2017 00:19:08 GMT
+X-Mozilla-Status: 0041
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Received: by localhost; Fri, 27 Jan 2017 07:52:26 +0100
+Date: Fri, 27 Jan 2017 00:19:08 GMT
+Message-Id: <http://www.selenic.com/mercurial/#changeset-c11fe7c58837ebf4266357d1f31896c7cc4a49b9@localhost.localdomain>
+From: john@example.com
+MIME-Version: 1.0
+Subject: Changeset c11fe7c58837ebf4266357d1f31896c7cc4a49b9
+Content-Transfer-Encoding: 8bit
+Content-Base: http://hg.mozilla.org/mozilla-central/rev/c11fe7c58837ebf4266357d1f31896c7cc4a49b9
+Content-Type: text/html; charset=UTF-8
+
+<html>
+ <head>
+ <title>Changeset c11fe7c58837ebf4266357d1f31896c7cc4a49b9</title>
+ <base href="http://hg.mozilla.org/mozilla-central/pushlog">
+ </head>
+ <body id="msgFeedSummaryBody" selected="false">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <ul class="filelist"><li class="file">modules/libpref/init/all.js</li><li class="file">netwerk/base/nsIOService.cpp</li><li class="file">netwerk/base/nsIOService.h</li><li class="file">We like using linefeeds only.</li></ul>
+ </div>
+ </body>
+</html>
+
diff --git a/comm/mail/test/browser/composition/data/format-flowed.eml b/comm/mail/test/browser/composition/data/format-flowed.eml
new file mode 100644
index 0000000000..31e77cb465
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/format-flowed.eml
@@ -0,0 +1,12 @@
+To: test1@test.invalid
+From: test2@test.invalid
+Subject: test format flowed reply
+Date: Tue, 27 Sep 2016 13:00:40 +0200
+MIME-Version: 1.0
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Language: en-US
+Content-Transfer-Encoding: 7bit
+
+first first first first first text text text text text text text text
+text text text text text text text text text text text text text text
+last last last last last last last last last last last last last last
diff --git a/comm/mail/test/browser/composition/data/format1-altering.eml b/comm/mail/test/browser/composition/data/format1-altering.eml
new file mode 100644
index 0000000000..b635ef267b
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/format1-altering.eml
@@ -0,0 +1,21 @@
+Message-ID: <11111.11111@example.invalid>
+Date: Sun, 18 May 2014 22:31:12 +0200
+MIME-Version: 1.0
+To: test@test.invalid
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>title</title>
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ <h1>heading</h1>
+ <hr>
+ <pre>
+ Pre line 1
+ Pre line 2
+ </pre>
+ </body>
+</html>
diff --git a/comm/mail/test/browser/composition/data/format1-plain.eml b/comm/mail/test/browser/composition/data/format1-plain.eml
new file mode 100644
index 0000000000..c39900369c
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/format1-plain.eml
@@ -0,0 +1,22 @@
+Message-ID: <11111.11111@example.invalid>
+Date: Sun, 18 May 2014 22:31:12 +0200
+MIME-Version: 1.0
+To: test@test.invalid
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>title</title>
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ Line 1<br>
+<p>
+ Line 2
+</p>
+ <pre class="moz-signature" cols="72">--
+ Signature block
+ </pre>
+ </body>
+</html>
diff --git a/comm/mail/test/browser/composition/data/format2-style-attr.eml b/comm/mail/test/browser/composition/data/format2-style-attr.eml
new file mode 100644
index 0000000000..5893b72472
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/format2-style-attr.eml
@@ -0,0 +1,37 @@
+Message-ID: <22222222.2222222@example.invalid>
+Date: Sun, 17 Jun 2012 09:42:45 +0200
+From: John Doe
+MIME-Version: 1.0
+Subject: Testcase - style attribute
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html;
+ charset=ISO-8859-1">
+ </head>
+ <body bgcolor="#FFFFFF" text="#000000">
+ *** This text is "Variable Width" ***<br>
+ <br>
+ <a title="Get the best mailer now! (Caveat: neglected bird with lots
+ of bugs)" style="float: right; background-color: blue; font-size:
+ 18px; text-decoration: none; color: white; padding-top: 90px;
+ padding-bottom: 90px; padding-right: 50px; padding-left: 50px;"
+ href="http://www.getthunderbird.com">http://www.getthunderbird.com</a><a
+ title="Get the best browser now!" style="float: right;
+ background-color: orangered; font-size: 18px; text-decoration:
+ none; color: white; padding-top: 90px; padding-bottom: 90px;
+ padding-right: 50px; padding-left: 50px;"
+ href="http://www.getfirefox.com">http://www.getfirefox.com</a>Lorem
+ ipsum
+ dolor sit amet, consectetur adipiscing elit. Vestibulum
+ velit purus, egestas eu commodo ac, imperdiet pretium sem. Nulla
+ pulvinar commodo rutrum. Duis feugiat facilisis libero, id fermentum
+ neque molestie vel. Praesent vel nisi metus, a aliquam tellus. Cras
+ in
+ <pre style="color:blue;background-color:yellow;text-align:center;">
+ Vivamus accumsan bibendum arcu nec egestas. Suspendisse potenti.
+ </pre>
+ </body>
+</html>
diff --git a/comm/mail/test/browser/composition/data/format3-style-tag.eml b/comm/mail/test/browser/composition/data/format3-style-tag.eml
new file mode 100644
index 0000000000..b0b27f7ae8
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/format3-style-tag.eml
@@ -0,0 +1,31 @@
+Message-ID: <33333.33333@example.invalid>
+Date: Sun, 17 Jun 2012 09:42:45 +0200
+MIME-Version: 1.0
+Subject: <style> element
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<html>
+<head>
+
+<style type="text/css">
+<!--
+body {
+font-size:11.0pt;
+font-family:"Calibri","sans-serif";
+}
+ul, ol, blockquote {
+margin: 0px 0px;
+}
+-->
+</style>
+
+</head><body>
+
+Type text here
+
+<div id="imageholder">
+
+</div>
+</body>
+</html>
diff --git a/comm/mail/test/browser/composition/data/iso-2022-jp.eml b/comm/mail/test/browser/composition/data/iso-2022-jp.eml
new file mode 100644
index 0000000000..79f289cc4a
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/iso-2022-jp.eml
@@ -0,0 +1,12 @@
+To: test1@test.invalid
+From: test2@test.invalid
+Subject: test quote ISO-2022-JP encoded message
+Date: Mon, 17 Aug 2020 10:10:47 +0900
+MIME-Version: 1.0
+Content-Type: text/plain; charset=ISO-2022-JP; format=flowed; delsp=yes
+Content-Transfer-Encoding: 7bit
+Content-Language: en-US
+
+hello
+
+$B@$3&(B
diff --git a/comm/mail/test/browser/composition/data/long-html-line.eml b/comm/mail/test/browser/composition/data/long-html-line.eml
new file mode 100644
index 0000000000..0c3adda83b
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/long-html-line.eml
@@ -0,0 +1,16 @@
+From: "Long line writer" <email@longline.invalid>
+To: <user@example.com>
+Subject: Long HTML line
+Date: Wed, 10 Feb 2016 16:45:36 -0600
+MIME-Version: 1.0
+Content-Type: text/html;
+ charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE html>
+<html>
+<body>
+
+We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. We like writing long lines. This is 998 long.
+
+ </body>
diff --git a/comm/mail/test/browser/composition/data/mime-encoded-subject.eml b/comm/mail/test/browser/composition/data/mime-encoded-subject.eml
new file mode 100644
index 0000000000..4b6ff2a263
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/mime-encoded-subject.eml
@@ -0,0 +1,16 @@
+Return-Path: <homer@example.com>
+Received: from smtp.example.com (smtpu [10.0.0.52])
+ by storage (Cyrus v2.3.7-Invoca-RPM-2.3.7-1.1) with LMTPA;
+ Mon, 26 Dec 2011 20:49:16 +0200
+Message-ID: <4EF8C1A5.1060708@example.com>
+Date: Mon, 26 Dec 2011 20:49:09 +0200
+From: Homer <homer@example.com>
+MIME-Version: 1.0
+To: Marge <marge@example.com>
+Subject: =?UTF-8?B?4oiAYeKIikE=?=
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Because they're stupid, that's why. That's why everybody does everything!
+
+ -Homer
diff --git a/comm/mail/test/browser/composition/data/multipart-charset.eml b/comm/mail/test/browser/composition/data/multipart-charset.eml
new file mode 100644
index 0000000000..20827efe54
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/multipart-charset.eml
@@ -0,0 +1,24 @@
+From: test <test@example.com>
+Subject: test multipart mixed, attachment with different charset
+To: test2 <test2@example.com>
+Date: Sat, 27 Feb 2016 17:11:45 +0100
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="boundary"
+
+This is a multi-part message in MIME format.
+--boundary
+Content-Type: text/plain; charset=EUC-KR; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Hi there.
+
+--boundary
+Content-Type: text/plain; charset=KOI8-R;
+ name="attachment.tmx"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="attachment.tmx"
+
+Just some text.
+--boundary--
diff --git a/comm/mail/test/browser/composition/data/nest.png b/comm/mail/test/browser/composition/data/nest.png
new file mode 100644
index 0000000000..5d5c0b2873
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/nest.png
Binary files differ
diff --git a/comm/mail/test/browser/composition/data/non-flowed-plain.eml b/comm/mail/test/browser/composition/data/non-flowed-plain.eml
new file mode 100644
index 0000000000..5f1cd3ebbe
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/non-flowed-plain.eml
@@ -0,0 +1,15 @@
+From: test@example.com
+To: test@example.com
+MIME-Version: 1.0
+Subject:plain text message not using format=flowed
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+line 1
+line 2
+line 3
+line 4
+line 5
+line 6
+line 7
+line 8
diff --git a/comm/mail/test/browser/composition/data/tb-logo.png b/comm/mail/test/browser/composition/data/tb-logo.png
new file mode 100644
index 0000000000..aac56e2546
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/tb-logo.png
Binary files differ
diff --git a/comm/mail/test/browser/composition/data/testmsg.eml b/comm/mail/test/browser/composition/data/testmsg.eml
new file mode 100644
index 0000000000..7aa1127836
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/testmsg.eml
@@ -0,0 +1,16 @@
+Return-Path: <homer@example.com>
+Received: from smtp.example.com (smtpu [10.0.0.52])
+ by storage (Cyrus v2.3.7-Invoca-RPM-2.3.7-1.1) with LMTPA;
+ Mon, 26 Dec 2011 20:49:16 +0200
+Message-ID: <4EF8C1A5.1060708@example.com>
+Date: Mon, 26 Dec 2011 20:49:09 +0200
+From: Homer <homer@example.com>
+MIME-Version: 1.0
+To: Marge <marge@example.com>
+Subject: why
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Because they're stupid, that's why. That's why everybody does everything!
+
+ -Homer
diff --git a/comm/mail/test/browser/composition/data/xunsent.eml b/comm/mail/test/browser/composition/data/xunsent.eml
new file mode 100644
index 0000000000..dab78a0b7f
--- /dev/null
+++ b/comm/mail/test/browser/composition/data/xunsent.eml
@@ -0,0 +1,14 @@
+Message-ID: <b48374f7-d539-4547-b239-7217b8debed9@test>
+Date: Mon, 2 Oct 2023 14:31:45 +0300
+MIME-Version: 1.0
+User-Agent: Thunderbird Daily
+Content-Language: en-US
+X-Unsent: 1
+To: Alice <alice@test>
+From: Carol <carol@test>
+Subject: xx unsent
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
+ attachmentreminder=0; deliveryformat=0
+Content-Type: text/plain
+
+This is a draft.
diff --git a/comm/mail/test/browser/composition/head.js b/comm/mail/test/browser/composition/head.js
new file mode 100644
index 0000000000..7b37c05ca0
--- /dev/null
+++ b/comm/mail/test/browser/composition/head.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm");
+
+registerCleanupFunction(() => {
+ for (let book of MailServices.ab.directories) {
+ if (
+ ["ldap_2.servers.history", "ldap_2.servers.pab"].includes(book.dirPrefId)
+ ) {
+ let cards = book.childCards;
+ if (cards.length > 0) {
+ info(`Cleaning up ${cards.length} card(s) from ${book.dirName}`);
+ for (let card of cards) {
+ if (card.isMailList) {
+ MailServices.ab.deleteAddressBook(card.mailListURI);
+ }
+ }
+ cards = cards.filter(c => !c.isMailList);
+ if (cards.length > 0) {
+ book.deleteCards(cards);
+ }
+ }
+ is(book.childCards.length, 0);
+ } else {
+ Assert.report(true, undefined, undefined, "Unexpected address book!");
+ MailServices.ab.deleteAddressBook(book.URI);
+ }
+ }
+
+ Services.focus.focusedWindow = window;
+ let mailButton = document.getElementById("mailButton");
+ mailButton.focus();
+ mailButton.blur();
+});
+
+/**
+ * Get the body part of an MIME message.
+ *
+ * @param {string} content - The message content.
+ * @returns {string}
+ */
+function getMessageBody(content) {
+ let separatorIndex = content.indexOf("\r\n\r\n");
+ Assert.equal(content.slice(-2), "\r\n", "Should end with a line break.");
+ return content.slice(separatorIndex + 4, -2);
+}
+
+async function chooseIdentity(win, identityKey) {
+ let popup = win.document.getElementById("msgIdentityPopup");
+ let shownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.getElementById("msgIdentity"),
+ {},
+ win
+ );
+ await shownPromise;
+ let hiddenPromise = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.activateItem(popup.querySelector(`[identitykey="${identityKey}"]`));
+ await hiddenPromise;
+}
diff --git a/comm/mail/test/browser/composition/html/linkpreview.html b/comm/mail/test/browser/composition/html/linkpreview.html
new file mode 100644
index 0000000000..5b80bdc59e
--- /dev/null
+++ b/comm/mail/test/browser/composition/html/linkpreview.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html prefix="og: https://ogp.me/ns#">
+<head>
+ <title>OG test</title>
+ <meta charset="UTF-8" />
+ <meta property="og:title" content="Article title" />
+ <meta property="og:url" content="https://www.example.com/?test=true" />
+ <meta property="og:image" content="http://mochi.test:8888/browser/comm/mail/test/browser/content-policy/html/pass.png" />
+ <meta property="og:description" content="Description of test article." />
+</head>
+<body>
+ <p>See <a href="https://ogp.me/">https://ogp.me/</a>
+</body>
+</html>