/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { MailChannel } = ChromeUtils.importESModule( "resource:///modules/MailChannel.sys.mjs" ); class TestPlainChannel { QueryInterface = ChromeUtils.generateQI(["nsIChannel"]); contentType = ""; constructor(uri) { this.URI = Services.io.newURI(uri); } } class TestMailChannel extends MailChannel { QueryInterface = ChromeUtils.generateQI(["nsIChannel", "nsIMailChannel"]); contentType = ""; /** * @param {string} uri */ constructor(uri) { super(); this.URI = Services.io.newURI(uri); } /** * @param {string[][]} expectedHeaders - An array of key/value pairs. */ checkHeaders(expectedHeaders) { const { headerNames, headerValues, mimeHeaders } = this; Assert.equal( headerNames.length, expectedHeaders.length, "number of header names" ); Assert.equal( headerValues.length, expectedHeaders.length, "number of header values" ); for (let i = 0; i < expectedHeaders.length; i++) { Assert.equal(headerNames[i], expectedHeaders[i][0], `header name ${i}`); Assert.equal(headerValues[i], expectedHeaders[i][1], `header value ${i}`); } for (const [name, value] of expectedHeaders) { if (name != "X-Mozilla-LocalizedDate") { Assert.equal( mimeHeaders.extractHeader(name, true).replaceAll("\t", " "), value, `mimeHeaders value for ${name}` ); } } } /** * @param {object[]} expectedAttachments - An array of objects, each * representing the properties of an attachment. */ checkAttachments(expectedAttachments) { const { attachments } = this; Assert.equal( attachments.length, expectedAttachments.length, "number of attachments" ); for (let i = 0; i < expectedAttachments.length; i++) { for (const property of attachments[i].enumerator) { Assert.equal( property.value, expectedAttachments[i][property.name], `attachment ${i} property ${property.name}` ); delete expectedAttachments[i][property.name]; } Assert.equal( Object.entries(expectedAttachments[i]).length, 0, `all attachment ${i} properties checked` ); } } } /** * @param {string} uri * @param {nsIChannel} channel * @param {string} input * @returns {string} */ async function convertStream(uri, channel, input) { const converter = Cc[ "@mozilla.org/streamconv;1?from=message/rfc822&to=*/*" ].createInstance(Ci.nsIStreamConverter); const listener = new PromiseTestUtils.PromiseStreamListener(undefined, true); converter.asyncConvertData("message/rfc822", "text/html", listener, channel); const { buffer } = new TextEncoder().encode(input); const inputStream = Cc[ "@mozilla.org/io/arraybuffer-input-stream;1" ].createInstance(Ci.nsIArrayBufferInputStream); inputStream.setData(buffer, 0, buffer.byteLength); converter.onStartRequest(channel); converter.onDataAvailable(channel, inputStream, 0, buffer.byteLength); converter.onStopRequest(channel, Cr.NS_OK); return await listener.promise; } const sampleEmailFile = do_get_file("sampleContent.eml"); const sampleEmailURI = "mailbox:" + Services.io.newFileURI(sampleEmailFile).spec.slice(5); /** * Test with a mailbox URL for an email file. */ add_task(async function testBodyMailboxURL() { await subtestBody(`${sampleEmailURI}?number=1`); }); /** * Test with an IMAP URL. This doesn't work because the IMAP account doesn't * exist, so the I/O service will refuse to create the URI. */ add_task(async function testBodyImapURL() { await subtestBody("imap://somebody@somewhere.tld/fetch%3EUID%3E/INBOX%3E1"); }).skip(); /** * Test with a file URL, with the query parameters we add. */ add_task(async function testBodyFileURL1() { await subtestBody( "file:///somewhere/out/there.eml?type=application/x-message-display&number=0" ); }); /** * Test with a file URL, without the query parameters we add. */ add_task(async function testBodyFileURL2() { await subtestBody("file:///somewhere/out/there.eml"); }); /** * Test with some completely unknown URL format. */ add_task(async function testBodyNewURL() { await subtestBody("odd:///?p=file%3A%2F%2F%2Fsomewhere%2Fout%2Fthere.eml"); }); /** * Simulates a load of sampleContent.eml from the given URI. The converter * output can vary based on the URI, but should be (almost) exactly the same * for every test that runs this subtest. * * @param {string} uri */ async function subtestBody(uri) { const channel = new TestMailChannel(uri); const input = await IOUtils.readUTF8(sampleEmailFile.path); const output = await convertStream(uri, channel, input); // Test the channel output. Assert.equal( channel.contentType, "text/html", "body channel should have the HTML content type" ); channel.checkHeaders([ ["Content-Type", `multipart/mixed; boundary="--------------CHOPCHOP0"`], ["Subject", "Big Meeting Today"], ["From", `"Andy Anway" `], ["To", `"Bob Bell" `], ["Message-Id", ""], ["Date", "Tue, 01 Feb 2000 00:00:00 +1300"], [ "X-Mozilla-LocalizedDate", new Services.intl.DateTimeFormat(undefined, { dateStyle: "short", timeStyle: "short", }).format(new Date("2000-02-01T00:00:00+1300")), ], ]); let expectedAttachmentURI; if (uri.startsWith("mailbox:")) { expectedAttachmentURI = "mailbox-message:" + uri.slice(8) + "#1"; } else { expectedAttachmentURI = uri; } const expectedAttachment1URL = URL.parse(uri); // Delete first to get the params in order. expectedAttachment1URL.searchParams.delete("type"); expectedAttachment1URL.searchParams.set("part", "1.2"); expectedAttachment1URL.searchParams.set("type", "text/plain"); expectedAttachment1URL.searchParams.set("filename", "attachment.txt"); const expectedAttachment2URL = URL.parse(uri); // Delete first to get the params in order. expectedAttachment2URL.searchParams.delete("type"); expectedAttachment2URL.searchParams.set("part", "1.3"); expectedAttachment2URL.searchParams.set("type", "image/svg+xml"); expectedAttachment2URL.searchParams.set("filename", "attachment.svg"); channel.checkAttachments([ { uri: expectedAttachmentURI, url: expectedAttachment1URL, displayName: "attachment.txt", contentType: "text/plain", "X-Mozilla-PartSize": "51", notDownloaded: false, "X-Mozilla-PartDownloaded": "1", }, { uri: expectedAttachmentURI, url: expectedAttachment2URL, displayName: "attachment.svg", contentType: "image/svg+xml", "X-Mozilla-PartSize": "474", notDownloaded: false, "X-Mozilla-PartDownloaded": "1", }, ]); // Test the HTML output. Assert.equal( output.slice(0, 28), "\xEF\xBB\xBF\r\n\r\n", "output should begin with UTF-8 BOM and HTML doctype" ); Assert.equal( output.slice(-18), "\r\n\r\n", "output should end with closing HTML tag" ); // Test the tag in the HTML output. const expectedImgURL = URL.parse(uri); expectedImgURL.searchParams.delete("type"); expectedImgURL.searchParams.set("part", "1.1.2"); expectedImgURL.searchParams.set("type", "image/png"); expectedImgURL.searchParams.set("filename", "tb-logo.png"); const document = new DOMParser().parseFromString(output, "text/html"); const img = document.querySelector("img"); Assert.equal( img.src, expectedImgURL.toString(), "inline image URL should be rewritten relative to the message URL" ); // Test a request for the image. const imgChannel = new TestPlainChannel(expectedImgURL); const imgOutput = await convertStream(expectedImgURL, imgChannel, input); Assert.equal( imgChannel.contentType, "image/png", "image channel should have the image content type" ); Assert.equal( imgOutput.slice(0, 8), "\x89PNG\x0D\x0A\x1A\x0A", "output should start at the start of the PNG image" ); Assert.equal( imgOutput.slice(-8), "IEND\xAE\x42\x60\x82", "output should end at the end of the PNG image" ); // Test a request for the first attachment. const attachment1Channel = new TestPlainChannel(expectedAttachment1URL); const attachment1Output = await convertStream( expectedAttachment1URL, attachment1Channel, input ); Assert.equal( attachment1Channel.contentType, "text/plain", "attachment channel should have the attachment content type" ); Assert.equal( attachment1Output, "I'm a text attachment! I won't be displayed inline\n", "output should contain the attachment" ); // Test a request for the second attachment. const attachment2Channel = new TestPlainChannel(expectedAttachment2URL); const attachment2Output = await convertStream( expectedAttachment2URL, attachment2Channel, input ); Assert.equal( attachment2Channel.contentType, "image/svg+xml", "attachment channel should have the attachment content type" ); Assert.equal( attachment2Output.slice(0, 5), "\n", "output should end at the end of the SVG image" ); } /** * Test the inline attachments when enabled. */ add_task(async function testInlineAttachments() { const uri = `${sampleEmailURI}?number=2`; const channel = new TestMailChannel(uri); const input = await IOUtils.readUTF8(sampleEmailFile.path); const output = await convertStream(uri, channel, input); // Test the HTML output. Assert.equal( output.slice(0, 28), "\xEF\xBB\xBF\r\n\r\n", "output should begin with UTF-8 BOM and HTML doctype" ); Assert.equal( output.slice(-18), "\r\n\r\n", "output should end with closing HTML tag" ); const document = new DOMParser().parseFromString(output, "text/html"); const fieldsets = document.querySelectorAll( "fieldset.moz-mime-attachment-header" ); Assert.equal( fieldsets.length, 2, "there should be fieldsets for the inline attachment and printed list" ); Assert.ok(!fieldsets[0].classList.contains("moz-print-only")); Assert.equal(fieldsets[0].textContent, "attachment.svg"); const inlineContainer = fieldsets[0].nextElementSibling; Assert.equal(inlineContainer.localName, "div"); Assert.equal(inlineContainer.className, "moz-attached-image-container"); const img = inlineContainer.querySelector("img"); Assert.equal(img.className, "moz-attached-image"); Assert.equal( img.src, `${sampleEmailURI}?number=2&part=1.3&type=image/svg+xml&filename=attachment.svg` ); subtestPrintedAttachmentList(fieldsets[1]); }); /** * Test the inline attachments when inline text attachments are enabled. */ add_task(async function testInlineTextAttachments() { Services.prefs.setBoolPref("mail.inline_attachments.text", true); const uri = `${sampleEmailURI}?number=3`; const channel = new TestMailChannel(uri); const input = await IOUtils.readUTF8(sampleEmailFile.path); const output = await convertStream(uri, channel, input); // Test the HTML output. Assert.equal( output.slice(0, 28), "\xEF\xBB\xBF\r\n\r\n", "output should begin with UTF-8 BOM and HTML doctype" ); Assert.equal( output.slice(-18), "\r\n\r\n", "output should end with closing HTML tag" ); const document = new DOMParser().parseFromString(output, "text/html"); const fieldsets = document.querySelectorAll( "fieldset.moz-mime-attachment-header" ); Assert.equal( fieldsets.length, 3, "there should be fieldsets for both the inline attachments and printed list" ); Assert.ok(!fieldsets[0].classList.contains("moz-print-only")); Assert.equal(fieldsets[0].textContent, "attachment.txt"); const inlineContainer0 = fieldsets[0].nextElementSibling; Assert.equal(inlineContainer0.localName, "div"); Assert.equal(inlineContainer0.className, "moz-text-plain"); const pre0 = inlineContainer0.querySelector("pre.moz-quote-pre[wrap]"); Assert.equal( pre0.textContent, "I'm a text attachment! I won't be displayed inline\n" ); Assert.ok(!fieldsets[1].classList.contains("moz-print-only")); Assert.equal(fieldsets[1].textContent, "attachment.svg"); const inlineContainer1 = fieldsets[1].nextElementSibling; Assert.equal(inlineContainer1.localName, "div"); Assert.equal(inlineContainer1.className, "moz-attached-image-container"); const img1 = inlineContainer1.querySelector("img.moz-attached-image"); Assert.equal( img1.src, `${sampleEmailURI}?number=3&part=1.3&type=image/svg+xml&filename=attachment.svg` ); subtestPrintedAttachmentList(fieldsets[2]); Services.prefs.clearUserPref("mail.inline_attachments.text"); }); /** * Test the inline attachments when disabled. */ add_task(async function testNoInlineAttachments() { Services.prefs.setBoolPref("mail.inline_attachments", false); const uri = `${sampleEmailURI}?number=4`; const channel = new TestMailChannel(uri); const input = await IOUtils.readUTF8(sampleEmailFile.path); const output = await convertStream(uri, channel, input); // Test the HTML output. Assert.equal( output.slice(0, 28), "\xEF\xBB\xBF\r\n\r\n", "output should begin with UTF-8 BOM and HTML doctype" ); Assert.equal( output.slice(-18), "\r\n\r\n", "output should end with closing HTML tag" ); const document = new DOMParser().parseFromString(output, "text/html"); const fieldsets = document.querySelectorAll( "fieldset.moz-mime-attachment-header" ); Assert.equal( fieldsets.length, 1, "there should only be a fieldset for the printed list" ); subtestPrintedAttachmentList(fieldsets[0]); Services.prefs.clearUserPref("mail.inline_attachments"); }); /** * @param {HTMLFieldSetElement} fieldset */ function subtestPrintedAttachmentList(fieldset) { Assert.ok(fieldset.classList.contains("moz-print-only")); Assert.equal(fieldset.textContent, "Attachments:"); const listContainer = fieldset.nextElementSibling; Assert.equal(listContainer.localName, "div"); Assert.equal( listContainer.className, "moz-mime-attachment-wrap moz-print-only" ); const table = listContainer.querySelector("table"); Assert.equal(table.tBodies[0].rows.length, 2); Assert.equal(table.tBodies[0].rows[0].cells[0].textContent, "attachment.txt"); Assert.equal(table.tBodies[0].rows[0].cells[1].textContent, "51 bytes"); Assert.equal(table.tBodies[0].rows[1].cells[0].textContent, "attachment.svg"); Assert.equal(table.tBodies[0].rows[1].cells[1].textContent, "474 bytes"); } /** * Test the case where there aren't any attachments. */ add_task(async function testNoAttachments() { const uri = `${sampleEmailURI}?number=5`; const channel = new TestMailChannel(uri); const input = `From andy@anway.invalid Subject: Big Meeting Today From: "Andy Anway" To: "Bob Bell" Message-Id: Date: Tue, 01 Feb 2000 00:00:00 +1300 Content-Type: text/html; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit

I'll be your message today.

`.replaceAll(/^ {2,4}/gm, ""); const output = await convertStream(uri, channel, input); // Test the channel output. Assert.equal( channel.contentType, "text/html", "body channel should have the HTML content type" ); channel.checkHeaders([ ["Subject", "Big Meeting Today"], ["From", `"Andy Anway" `], ["To", `"Bob Bell" `], ["Message-Id", ""], ["Date", "Tue, 01 Feb 2000 00:00:00 +1300"], [ "X-Mozilla-LocalizedDate", new Services.intl.DateTimeFormat(undefined, { dateStyle: "short", timeStyle: "short", }).format(new Date("2000-02-01T00:00:00+1300")), ], ["Content-Type", "text/html; charset=ISO-8859-1; format=flowed"], ]); channel.checkAttachments([]); // Test the HTML output. Assert.equal( output.slice(0, 28), "\xEF\xBB\xBF\r\n\r\n", "output should begin with UTF-8 BOM and HTML doctype" ); Assert.equal( output.slice(-18), "\r\n\r\n", "output should end with closing HTML tag" ); const document = new DOMParser().parseFromString(output, "text/html"); const fieldsets = document.querySelectorAll( "fieldset.moz-mime-attachment-header" ); Assert.equal( fieldsets.length, 0, "there should no fieldsets for attachments" ); });