diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/mime/test | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
29 files changed, 4900 insertions, 0 deletions
diff --git a/comm/mailnews/mime/test/TestMimeCrash.cpp b/comm/mailnews/mime/test/TestMimeCrash.cpp new file mode 100644 index 0000000000..f041d8f910 --- /dev/null +++ b/comm/mailnews/mime/test/TestMimeCrash.cpp @@ -0,0 +1,63 @@ +// This is a crash test for Bug 556351 + +#include "nsCOMPtr.h" +#include "nsIMimeConverter.h" +#include "nsServiceManagerUtils.h" + +#include "prshma.h" +#include "prsystem.h" + +#include "TestHarness.h" + +nsresult mime_encoder_output_fn(const char* buf, int32_t size, void* closure) { + return NS_OK; +} + +nsresult do_test(const char* aBuffer, const uint32_t aSize) { + nsresult rv; + MimeEncoderData* encodeData = nullptr; + int32_t written = 0; + + nsCOMPtr<nsIMimeConverter> converter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->QPEncoderInit(mime_encoder_output_fn, nullptr, &encodeData); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->EncoderWrite(encodeData, aBuffer, aSize, &written); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->EncoderDestroy(encodeData, false); + return rv; +} + +int main(int argc, char** argv) { + ScopedXPCOM xpcom("TestMimeCrash"); + if (xpcom.failed()) return 1; + + // We cannot use malloc() since this crashes depends on memory allocation. + // By using mmap()/PR_MemMap(), end of buffer that is last in the page + // sets LF. + + uint32_t bufsize = PR_GetPageSize(); + PRFileMap* fm = PR_OpenAnonFileMap(".", bufsize, PR_PROT_READWRITE); + if (!fm) return 1; + char* addr = (char*)PR_MemMap(fm, 0, bufsize); + if (!addr) return 1; + memset(addr, '\r', bufsize); + + nsresult rv = do_test(addr, bufsize); + + PR_MemUnmap(addr, bufsize); + PR_CloseFileMap(fm); + + if (NS_FAILED(rv)) { + fail("cannot use nsIMimeConverter error=%08x\n", rv); + return -1; + } + + passed("no crash"); + + return 0; +} diff --git a/comm/mailnews/mime/test/moz.build b/comm/mailnews/mime/test/moz.build new file mode 100644 index 0000000000..6b37fdbe09 --- /dev/null +++ b/comm/mailnews/mime/test/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] diff --git a/comm/mailnews/mime/test/unit/custom_header.js b/comm/mailnews/mime/test/unit/custom_header.js new file mode 100644 index 0000000000..af2af97780 --- /dev/null +++ b/comm/mailnews/mime/test/unit/custom_header.js @@ -0,0 +1,11 @@ +// Custom header support for testing structured_headers.js + +/* globals jsmime */ + +jsmime.headerparser.addStructuredDecoder("X-Unusual", function (hdrs) { + return Number.parseInt(hdrs[hdrs.length - 1], 16); +}); + +jsmime.headeremitter.addStructuredEncoder("X-Unusual", function (val) { + this.addUnstructured(val.toString(16)); +}); diff --git a/comm/mailnews/mime/test/unit/head_mime.js b/comm/mailnews/mime/test/unit/head_mime.js new file mode 100644 index 0000000000..868a7ceace --- /dev/null +++ b/comm/mailnews/mime/test/unit/head_mime.js @@ -0,0 +1,62 @@ +/* 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/. */ + +/** + * Utility code for converting encoded MIME data. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +var CC = Components.Constructor; + +// Ensure the profile directory is set up +do_get_profile(); + +var gDEPTH = "../../../../"; + +registerCleanupFunction(function () { + load(gDEPTH + "mailnews/resources/mailShutdown.js"); +}); + +function apply_mime_conversion(msgUri, smimeHeaderSink) { + let service = MailServices.messageServiceFromURI(msgUri); + + // This is what we listen on in the end. + let listener = new PromiseTestUtils.PromiseStreamListener(); + + // Make the underlying channel--we need this for the converter parameter. + let url = service.getUrlForUri(msgUri); + + let channel = Services.io.newChannelFromURI( + url, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + channel.QueryInterface(Ci.nsIMailChannel).smimeHeaderSink = smimeHeaderSink; + + // Make the MIME converter, using the listener we first set up. + let converter = Cc["@mozilla.org/streamConverters;1"] + .getService(Ci.nsIStreamConverterService) + .asyncConvertData("message/rfc822", "text/html", listener, channel); + + // Now load the message, run it through the converter, and wait for all the + // data to stream through. + channel.asyncOpen(converter); + return listener; +} diff --git a/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js b/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.js new file mode 100644 index 0000000000..71d31da9ac --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_EncodeMimePartIIStr_UTF8.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/. */ + +// This tests minimal mime encoding fixed in bug 458685 + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + var i; + + var checks = [ + ["", false, ""], + ["\u0436", false, "=?UTF-8?B?0LY=?="], // CYRILLIC SMALL LETTER ZHE + ["IamASCII", false, "IamASCII"], + // Although an invalid email, we shouldn't crash on it (bug 479206) + ["crash test@foo.invalid>", true, '"crash test"@foo.invalid'], + [ + "MXR now displays links to Github log & Blame for\r\n Gaia/Rust/Servo", + false, + "MXR now displays links to Github log & Blame for\r\n Gaia/Rust/Servo", + ], + ["-----------------------:", false, "-----------------------:"], + ]; + + for (i = 0; i < checks.length; ++i) { + Assert.equal( + MailServices.mimeConverter.encodeMimePartIIStr_UTF8( + checks[i][0], + checks[i][1], + "Subject".length, + 72 + ), + checks[i][2] + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js b/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js new file mode 100644 index 0000000000..2dfeabf7ff --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_alternate_p7m_handling.js @@ -0,0 +1,58 @@ +/* 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 { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { MsgHdrToMimeMessage } = ChromeUtils.import( + "resource:///modules/gloda/MimeMessage.jsm" +); + +const P7M_ATTACHMENT = "dGhpcyBpcyBub3QgYSByZWFsIHMvbWltZSBwN20gZW50aXR5"; +var messageGenerator = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); +var msgHdr; + +add_setup(async function () { + // Create a message with a p7m attachment. + let synMsg = messageGenerator.makeMessage({ + attachments: [ + { + body: P7M_ATTACHMENT, + filename: "test.txt.p7m", + contentType: "application/pkcs7-mime", + format: "", + encoding: "base64", + }, + ], + }); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + msgHdr = synSet.getMsgHdr(0); +}); + +add_task(async function test_mime_p7m_external_foo_pref() { + Services.prefs.setBoolPref("mailnews.p7m_external", true); + + await new Promise(resolve => { + MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) { + Assert.ok(aMimeMsg.allUserAttachments.length == 1); + resolve(); + }); + }); +}); +add_task(async function test_mime_p7m_external_all_external_pref() { + Services.prefs.setBoolPref("mailnews.p7m_external", false); + + await new Promise(resolve => { + MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) { + Assert.ok(aMimeMsg.allUserAttachments.length == 1); + resolve(); + }); + }); +}); diff --git a/comm/mailnews/mime/test/unit/test_attachment_size.js b/comm/mailnews/mime/test/unit/test_attachment_size.js new file mode 100644 index 0000000000..cf6e68f0bf --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_attachment_size.js @@ -0,0 +1,322 @@ +/* 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 test creates some messages with attachments of different types and + * checks that libmime reports the expected size for each of them. + */ + +var { + MessageGenerator, + SyntheticPartLeaf, + SyntheticPartMultiMixed, + SyntheticMessageSet, +} = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// Somehow we hit the blocklist service, and that needs appInfo defined +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo(); + +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); + +// Create a message generator +var msgGen = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); + +/* 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. + */ +const EPSILON = "@mozilla.org/windows-registry-key;1" in Cc ? 4 : 2; + +const TEXT_ATTACHMENT = + "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."; + +const BINARY_ATTACHMENT = TEXT_ATTACHMENT; + +const IMAGE_ATTACHMENT = + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" + + "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" + + "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" + + "SNQAAlmAY+71EgFoAAAAASUVORK5CYII="; +const IMAGE_SIZE = 188; + +const UU_ATTACHMENT = + "begin 644 /home/jvporter/Desktop/out.txt\n" + + 'M0V%N)W0@;6%K92!T:&4@9G)U9R!C;VYT97-T+"!(96QE;CL@<W1O;6%C:"=S\n' + + "M('5P<V5T+B!))VQL(&9I>\"!Y;W4L(%5B:6LA(%5B:6L@9')O<',@>6]U(&)A\n" + + "M8VL@:6X@=&AE('1H:6-K(&]F('1H:6YG<R!F87-T+B!486ME;B!A<R!D:7)E\n" + + "M8W1E9\"P@56)I:R!S<&5E9',@<F5L:65F('1O(&AE860@86YD('-T;VUA8V@N\n" + + "M(%)E;65M8F5R.B!58FEK(&ES(&]N;'D@<V5C;VYD<R!A=V%Y+B!!=F]I9\"!P\n" + + ".<F]L;VYG960@=7-E+@H`\n" + + "`\n" + + "end"; + +const YENC_TEXT = + "Hello there --\n" + + "=ybegin line=128 size=174 name=jane\n" + + "\x76\x99\x98\x91\x9e\x8f\x97\x9a\x9d\x56\x4a\x94\x8f\x4a\x97\x8f" + + "\x4a\x9d\x9f\x93\x9d\x4a\x8d\x99\x9f\x8d\x92\xed\xd3\x4a\x8e\x8f" + + "\x4a\x8c\x99\x98\x98\x8f\x4a\x92\x8f\x9f\x9c\x8f\x58\x4a\x7a\x8b" + + "\x9c\x90\x99\x93\x9d\x56\x4a\xed\xca\x4a\x9a\x8f\x93\x98\x8f\x4a" + + "\x97\x8b\x4a\x8c\x99\x9f\x91\x93\x8f\x4a\xed\xd3\x9e\x8f\x93\x98" + + "\x9e\x8f\x56\x4a\x97\x8f\x9d\x4a\xa3\x8f\x9f\xa2\x4a\x9d\x8f\x4a" + + "\x90\x8f\x9c\x97\x8b\x93\x8f\x98\x9e\x4a\x9d\x93\x4a\xa0\x93\x9e" + + "\x8f\x4a\x9b\x9f\x8f\x4a\x94\x8f\x4a\x98\x51\x8b\xa0\x8b\x93\x9d" + + "\x0d\x0a\x4a\x9a\x8b\x9d\x4a\x96\x8f\x4a\x9e\x8f\x97\x9a\x9d\x4a" + + "\x8e\x8f\x4a\x97\x8f\x4a\x8e\x93\x9c\x8f\x4a\x64\x4a\xec\xd5\x4a" + + "\x74\x8f\x4a\x97\x51\x8f\x98\x8e\x99\x9c\x9d\x58\x4a\xec\xe5\x34" + + "\x0d\x0a" + + "=yend size=174 crc32=7efccd8e\n"; +const YENC_SIZE = 174; + +const PART_HTML = new SyntheticPartLeaf( + "<html><head></head><body>I am HTML! Woo! </body></html>", + { + contentType: "text/html", + } +); + +var attachedMessage1 = msgGen.makeMessage({ body: { body: TEXT_ATTACHMENT } }); +var attachedMessage2 = msgGen.makeMessage({ + body: { body: TEXT_ATTACHMENT }, + attachments: [ + { + body: IMAGE_ATTACHMENT, + contentType: "application/x-ubik", + filename: "ubik", + encoding: "base64", + format: "", + }, + ], +}); + +add_task(async function test_text_attachment() { + await test_message_attachments({ + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "ubik.txt", + format: "", + }, + ], + size: TEXT_ATTACHMENT.length, + }); +}); + +// (inline) image attachment +add_task(async function test_inline_image_attachment() { + await test_message_attachments({ + attachments: [ + { + body: IMAGE_ATTACHMENT, + contentType: "image/png", + filename: "lines.png", + encoding: "base64", + format: "", + }, + ], + size: IMAGE_SIZE, + }); +}); + +// binary attachment, no encoding +add_task(async function test_binary_attachment_no_encoding() { + await test_message_attachments({ + attachments: [ + { + body: BINARY_ATTACHMENT, + contentType: "application/x-ubik", + filename: "ubik", + format: "", + }, + ], + size: BINARY_ATTACHMENT.length, + }); +}); + +// binary attachment, b64 encoding +add_task(async function test_binary_attachment_b64_encoding() { + await test_message_attachments({ + attachments: [ + { + body: IMAGE_ATTACHMENT, + contentType: "application/x-ubik", + filename: "ubik", + encoding: "base64", + format: "", + }, + ], + size: IMAGE_SIZE, + }); +}); + +// uuencoded attachment +add_task(async function test_uuencoded_attachment() { + await test_message_attachments({ + attachments: [ + { + body: UU_ATTACHMENT, + contentType: "application/x-uuencode", + filename: "ubik", + format: "", + encoding: "uuencode", + }, + ], + size: TEXT_ATTACHMENT.length, + }); +}); + +// yencoded attachment +add_task(async function test_yencoded_attachment() { + await test_message_attachments({ + bodyPart: new SyntheticPartLeaf("I am text! Woo!\n\n" + YENC_TEXT, { + contentType: "", + }), + subject: 'yEnc-Prefix: "jane" 174 yEnc bytes - yEnc test (1)', + size: YENC_SIZE, + }); +}); + +// an attached eml that used to return a size that's -1 +add_task(async function test_incorrect_attached_eml() { + await test_message_attachments({ + bodyPart: new SyntheticPartMultiMixed([PART_HTML, attachedMessage1]), + size: get_message_size(attachedMessage1), + }); +}); + +// this is an attached message that itself has an attachment +add_task(async function test_recursive_attachment() { + await test_message_attachments({ + bodyPart: new SyntheticPartMultiMixed([PART_HTML, attachedMessage2]), + size: get_message_size(attachedMessage2), + }); +}); + +// an "attachment" that's really the body of the message +add_task(async function test_body_attachment() { + await test_message_attachments({ + body: { + body: TEXT_ATTACHMENT, + contentType: "application/x-ubik; name=attachment.ubik", + }, + size: TEXT_ATTACHMENT.length, + }); +}); + +// a message/rfc822 "attachment" that's really the body of the message +add_task(async function test_rfc822_attachment() { + await test_message_attachments({ + bodyPart: attachedMessage1, + size: get_message_size(attachedMessage1), + }); +}); + +// an external http link attachment (as constructed for feed enclosures) - no 'size' parm. +add_task(async function test_external_http_link_without_size() { + await test_message_attachments({ + attachments: [ + { + body: "This MIME attachment is stored separately from the message.", + contentType: 'application/unknown; name="somefile"', + extraHeaders: { + "X-Mozilla-External-Attachment-URL": "http://myblog.com/somefile", + }, + disposition: 'attachment; filename="somefile"', + }, + ], + size: -1, + }); +}); + +// an external http link attachment (as constructed for feed enclosures) - file with 'size' parm. +add_task(async function test_external_http_link_wit_file_size() { + await test_message_attachments({ + attachments: [ + { + body: "This MIME attachment is stored separately from the message.", + contentType: 'audio/mpeg; name="file.mp3"; size=123456789', + extraHeaders: { + "X-Mozilla-External-Attachment-URL": "https://myblog.com/file.mp3", + }, + disposition: 'attachment; name="file.mp3"', + }, + ], + size: 123456789, + }); +}); + +add_task(function endTest() { + messageInjection.teardownMessageInjection(); +}); + +async function test_message_attachments(info) { + let synMsg = msgGen.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + await PromiseTestUtils.promiseDelay(200); + let streamListener = new PromiseTestUtils.PromiseStreamListener({ + onStopRequest(request) { + request.QueryInterface(Ci.nsIMailChannel); + for (let attachment of request.attachments) { + let attachmentSize = parseInt(attachment.get("X-Mozilla-PartSize")); + dump( + "*** Size is " + attachmentSize + " (expecting " + info.size + ")\n" + ); + Assert.ok(Math.abs(attachmentSize - info.size) <= EPSILON); + break; + } + }, + }); + msgService.streamMessage( + msgURI, + streamListener, + null, + null, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + false + ); + + await streamListener.promise; +} + +/** + * Return the size of a synthetic message. Much like the above comment, libmime + * counts bytes differently on Windows, where it counts newlines (\r\n) as 2 + * bytes. Mac and Linux treats them as 1 byte. + * + * @param message a synthetic message from makeMessage() + * @returns the message's size in bytes + */ +function get_message_size(message) { + let messageString = message.toMessageString(); + if (EPSILON == 4) { + // Windows + return messageString.length; + } + return messageString.replace(/\r\n/g, "\n").length; +} diff --git a/comm/mailnews/mime/test/unit/test_badContentType.js b/comm/mailnews/mime/test/unit/test_badContentType.js new file mode 100644 index 0000000000..1202f3319b --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_badContentType.js @@ -0,0 +1,115 @@ +/* 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 test checks handling of bad content type of the + * type reported in bug 659355. + * Adapted from test_attachment_size.js + */ + +var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); +var messageGenerator = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); + +const IMAGE_ATTACHMENT = + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwS" + + "FlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA" + + "A5SURBVCiRY/z//z8DKYCJJNXkaGBgYGD4D8NQ5zUgiTVAxeBqSLaBkVRPM0KtIhrQ3km0jwe" + + "SNQAAlmAY+71EgFoAAAAASUVORK5CYII="; + +add_task(async function test_image_attachment_normal_content_type() { + await test_message_attachments({ + attachments: [ + { + body: IMAGE_ATTACHMENT, + contentType: "image/png", + filename: "lines.png", + encoding: "base64", + format: "", + }, + ], + testContentType: "image/png", + }); +}); + +add_task(async function test_image_attachment_bad_content_type() { + await test_message_attachments({ + attachments: [ + { + body: IMAGE_ATTACHMENT, + contentType: "=?windows-1252?q?application/pdf", + filename: "lines.pdf", + encoding: "base64", + format: "", + }, + ], + testContentType: "application/pdf", + }); +}); + +add_task(function endTest() { + messageInjection.teardownMessageInjection(); +}); + +async function test_message_attachments(info) { + let synMsg = messageGenerator.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + + let streamListener = new PromiseTestUtils.PromiseStreamListener({ + onStopRequest(request, statusCode) { + request.QueryInterface(Ci.nsIMailChannel); + let msgHdrSinkContentType = + request.attachments[0].getProperty("contentType"); + Assert.equal(msgHdrSinkContentType, info.testContentType); + }, + }); + msgService.streamMessage( + msgURI, + streamListener, + null, + null, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + false + ); + await streamListener.promise; +} + +function MsgHeaderSinkHandleAttachments() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} + +MsgHeaderSinkHandleAttachments.prototype = { + handleAttachment( + aContentType, + aUrl, + aDisplayName, + aUri, + aIsExternalAttachment + ) { + this._resolve(aContentType); + }, + + get promise() { + return this._promise; + }, +}; diff --git a/comm/mailnews/mime/test/unit/test_bug493544.js b/comm/mailnews/mime/test/unit/test_bug493544.js new file mode 100644 index 0000000000..f0da7ef167 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_bug493544.js @@ -0,0 +1,106 @@ +// +// Tests if a multi-line MIME header is parsed even if it violates RFC 2047 +// + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + const headers = [ + { + // Bug 833028 + encoded: + "Subject: AAA =?UTF-8?Q?bbb?= CCC =?UTF-8?Q?ddd?= EEE =?UTF-8?Q?fff?= GGG", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: AAA bbb CCC ddd EEE fff GGG", + }, + { + // Bug 493544 + encoded: + "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiCAg?=\n" + + " =?UTF-8?B?4oiJICDiiIogIOKIiyAg4oiMICDiiI0gIOKIjiAg4oiP?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Bug 476741 + encoded: + "Subject: =?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__=E2?=\n" + + " =?utf-8?Q?=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__=E2=88?=\n" + + " =?utf-8?Q?=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__=E2=88=8F?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Normal case + encoded: + "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA==?=\n" + + " =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Normal case with the encoding character in lower case + encoded: + "Subject: =?UTF-8?b?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA==?=\n" + + " =?UTF-8?b?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Normal case + encoded: + "Subject: =?utf-8?Q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" + + " =?utf-8?Q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" + + " =?utf-8?Q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" + + " =?utf-8?Q?=E2=88=8F?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Normal case with the encoding character in lower case + encoded: + "Subject: =?utf-8?q?=E2=88=80__=E2=88=81__=E2=88=82__=E2=88=83__=E2=88=84__?=\n" + + " =?utf-8?q?=E2=88=85__=E2=88=86__=E2=88=87__=E2=88=88__=E2=88=89__?=\n" + + " =?utf-8?q?=E2=88=8A__=E2=88=8B__=E2=88=8C__=E2=88=8D__=E2=88=8E__?=\n" + + " =?utf-8?q?=E2=88=8F?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + { + // Regression test for bug 227290 + encoded: + "Subject: =?UTF-8?B?4oiAICDiiIEgIOKIgiAg4oiDICDiiIQgIOKIhSAg4oiGICDiiIcgIOKIiA===?=\n" + + " =?UTF-8?B?ICDiiIkgIOKIiiAg4oiLICDiiIwgIOKIjSAg4oiOICDiiI8=?=", + defaultCharset: "UTF-8", + overrideCharset: false, + eatContinuation: false, + decoded: "Subject: ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏", + }, + ]; + + for (let i = 0; i < headers.length; ++i) { + let decoded = MailServices.mimeConverter.decodeMimeHeader( + headers[i].encoded, + headers[i].defaultCharset, + headers[i].overrideCharset, + headers[i].eatContinuation + ); + Assert.equal(decoded, headers[i].decoded); + } +} diff --git a/comm/mailnews/mime/test/unit/test_handlerRegistration.js b/comm/mailnews/mime/test/unit/test_handlerRegistration.js new file mode 100644 index 0000000000..5a2aef8799 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_handlerRegistration.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 https://mozilla.org/MPL/2.0/. */ + +var { EnigmailVerify } = ChromeUtils.import( + "chrome://openpgp/content/modules/mimeVerify.jsm" +); + +/** + * Tests switching content-type handlers on demand. + */ +add_task(function () { + const CONTRACT_ID = "@mozilla.org/mimecth;1?type=multipart/signed"; + const INTERFACE = Ci.nsIMimeContentTypeHandler; + + Assert.ok( + !Components.manager.isContractIDRegistered(CONTRACT_ID), + "no factory is registered initially" + ); + + EnigmailVerify.registerPGPMimeHandler(); + Assert.equal( + Cc[CONTRACT_ID].number, + EnigmailVerify.pgpMimeFactory.classID.number, + "pgpMimeFactory is the registered factory" + ); + Assert.ok( + Cc[CONTRACT_ID].createInstance(INTERFACE), + "pgpMimeFactory successfully created an instance" + ); + + EnigmailVerify.unregisterPGPMimeHandler(); + Assert.ok( + !Components.manager.isContractIDRegistered(CONTRACT_ID), + "pgpMimeFactory has been unregistered" + ); + Assert.throws( + () => Cc[CONTRACT_ID].createInstance(INTERFACE), + /NS_ERROR_XPC_CI_RETURNED_FAILURE/, + "exception correctly thrown" + ); + + EnigmailVerify.registerPGPMimeHandler(); + Assert.equal( + Cc[CONTRACT_ID].number, + EnigmailVerify.pgpMimeFactory.classID.number, + "pgpMimeFactory is the registered factory" + ); + Assert.ok( + Cc[CONTRACT_ID].createInstance(INTERFACE), + "pgpMimeFactory successfully created an instance" + ); + + EnigmailVerify.unregisterPGPMimeHandler(); + Assert.ok( + !Components.manager.isContractIDRegistered(CONTRACT_ID), + "pgpMimeFactory has been unregistered" + ); + Assert.throws( + () => Cc[CONTRACT_ID].createInstance(INTERFACE), + /NS_ERROR_XPC_CI_RETURNED_FAILURE/, + "exception correctly thrown" + ); +}); diff --git a/comm/mailnews/mime/test/unit/test_hidden_attachments.js b/comm/mailnews/mime/test/unit/test_hidden_attachments.js new file mode 100644 index 0000000000..1203c9a8ea --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_hidden_attachments.js @@ -0,0 +1,221 @@ +/* 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 test creates some messages with attachments of different types and + * checks that libmime emits (or doesn't emit) the attachments as appropriate. + */ + +var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); +var messageGenerator = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); + +add_task(async function test_without_attachment() { + await test_message_attachments({}); +}); + +/* Attachments with Content-Disposition: attachment */ +// inline-able attachment with a name +add_task( + async function test_content_disposition_attachment_inlineable_attachment_with_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "ubik.txt", + disposition: "attachment", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: attachment */ +// inline-able attachment with no name +add_task( + async function test_content_disposition_attachment_inlineable_attachment_no_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "", + disposition: "attachment", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: attachment */ +// non-inline-able attachment with a name +add_task( + async function test_content_disposition_attachment_non_inlineable_attachment_with_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "ubik.ubk", + disposition: "attachment", + contentType: "application/x-ubik", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: attachment */ +// non-inline-able attachment with no name +add_task( + async function test_content_disposition_attachment_non_inlineable_attachment_no_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "", + disposition: "attachment", + contentType: "application/x-ubik", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: inline */ +// inline-able attachment with a name +add_task( + async function test_content_disposition_inline_inlineable_attachment_with_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "ubik.txt", + disposition: "inline", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: inline */ +// inline-able attachment with no name +add_task( + async function test_content_disposition_inline_inlineable_attachment_no_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "", + disposition: "inline", + format: "", + shouldShow: false, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: inline */ +// non-inline-able attachment with a name +add_task( + async function test_content_disposition_inline_non_inlineable_attachment_with_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "ubik.ubk", + disposition: "inline", + contentType: "application/x-ubik", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +/* Attachments with Content-Disposition: inline */ +// non-inline-able attachment with no name +add_task( + async function test_content_disposition_inline_non_inlineable_attachment_no_name() { + await test_message_attachments({ + attachments: [ + { + body: "attachment", + filename: "", + disposition: "inline", + contentType: "application/x-ubik", + format: "", + shouldShow: true, + }, + ], + }); + } +); + +async function test_message_attachments(info) { + let synMsg = messageGenerator.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + + let streamListener = new PromiseTestUtils.PromiseStreamListener({ + onStopRequest(request, status) { + request.QueryInterface(Ci.nsIMailChannel); + let expectedAttachments = (info.attachments || []) + .filter(i => i.shouldShow) + .map(i => i.filename); + Assert.equal(request.attachments.length, expectedAttachments.length); + + for (let i = 0; i < request.attachments.length; i++) { + // If the expected attachment's name is empty, we probably generated a + // name like "Part 1.2", so don't bother checking that the names match + // (they won't). + if (expectedAttachments[i]) { + Assert.equal( + request.attachments[i].getProperty("displayName"), + expectedAttachments[i] + ); + } + } + }, + }); + msgService.streamMessage( + msgURI, + streamListener, + null, + null, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + false + ); + + await streamListener.promise; +} diff --git a/comm/mailnews/mime/test/unit/test_jsmime_charset.js b/comm/mailnews/mime/test/unit/test_jsmime_charset.js new file mode 100644 index 0000000000..865c8ae02f --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_jsmime_charset.js @@ -0,0 +1,43 @@ +/* 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 tests that the charset decoding uses nsICharsetDecoder instead of +// TextDecoder, to get some extra charsets. + +const { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm"); + +var tests = [ + ["=?UTF-7?Q?+AKM-1?=", "\u00A31"], + ["=?UTF-7?Q?+AK?= =?UTF-7?Q?M-1?=", "\u00A31"], + ["=?UTF-8?Q?=C2?=", "\uFFFD"], // Replacement character for invalid input. + ["=?NotARealCharset?Q?text?=", "=?NotARealCharset?Q?text?="], + ["\xC2\xA31", "\u00A31", "ISO-8859-2"], + ["\xA31", "\u01411", "ISO-8859-2"], + ["\xC21", "\u00C21", "ISO-8859-1"], + // "Here comes the text." in Japanese encoded in Shift_JIS, also using Thunderbird's alias cp932. + [ + "=?shift_jis?Q?=82=b1=82=b1=82=c9=96=7b=95=b6=82=aa=82=ab=82=dc=82=b7=81=42?=", + "ここに本文がきます。", + ], + ["=?shift_jis?B?grGCsYLJlnuVtoKqgquC3IK3gUI=?=", "ここに本文がきます。"], + [ + "=?cp932?Q?=82=b1=82=b1=82=c9=96=7b=95=b6=82=aa=82=ab=82=dc=82=b7=81=42?=", + "ここに本文がきます。", + ], + ["=?cp932?B?grGCsYLJlnuVtoKqgquC3IK3gUI=?=", "ここに本文がきます。"], +]; + +function run_test() { + for (let test of tests) { + dump("Testing message " + test[0]); + let value = test[0]; + if (test.length > 2) { + value = jsmime.headerparser.convert8BitHeader(value, test[2]); + } + Assert.equal( + jsmime.headerparser.parseStructuredHeader("Subject", value), + test[1] + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_message_attachment.js b/comm/mailnews/mime/test/unit/test_message_attachment.js new file mode 100644 index 0000000000..653a7078af --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_message_attachment.js @@ -0,0 +1,168 @@ +/* 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 test verifies that we generate proper attachment filenames. + */ + +var { + MessageGenerator, + SyntheticMessageSet, + SyntheticPartMultiMixed, + SyntheticPartLeaf, +} = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); + +// Create a message generator +var msgGen = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); +var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +// The attachments need to have some content or the stream converter won't +// display them inline. In the case of the email attachment it must have +// trailing CRLFs or it will fail to parse. +const TEXT_ATTACHMENT = "inline text attachment"; +const EMAIL_ATTACHMENT = "Subject: fake email\r\n\r\n"; +const HTML_ATTACHMENT = "<html><body></body></html>"; + +add_setup(function () { + Services.prefs.setBoolPref("mail.inline_attachments.text", true); +}); + +// Unnamed email attachment. +add_task(async function test_unnamed_email_attachment() { + await test_message_attachments({ + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "test.txt", + format: "", + }, + { + body: EMAIL_ATTACHMENT, + expectedFilename: "ForwardedMessage.eml", + contentType: "message/rfc822", + }, + ], + }); +}); + +// Named email attachment. +add_task(async function test_named_email_attachment() { + await test_message_attachments({ + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "test.txt", + format: "", + }, + { + body: EMAIL_ATTACHMENT, + filename: "Attached Message", + contentType: "message/rfc822", + }, + ], + }); +}); + +// Escaped html attachment. +add_task(async function test_foo() { + await test_message_attachments({ + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "test.html", + format: "", + }, + { + body: HTML_ATTACHMENT, + filename: + "<iframe src="e;http://www.example.com"e></iframe>.htm", + expectedFilename: + "<iframe src=&quote;http://www.example.com&quote></iframe>.htm", + contentType: "text/html;", + }, + ], + }); +}); + +// No named email attachment with subject header. +add_task(async function test_no_named_email_attachment_with_subject_header() { + await test_message_attachments({ + attachments: [ + { + body: "", + expectedFilename: "testSubject.eml", + }, + ], + bodyPart: new SyntheticPartMultiMixed([ + new SyntheticPartLeaf("plain body text"), + msgGen.makeMessage({ + subject: "=?UTF-8?B?dGVzdFN1YmplY3Q=?=", // This string is 'testSubject'. + charset: "UTF-8", + }), + ]), + }); +}); + +async function test_message_attachments(info) { + let synMsg = msgGen.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + + msgService.streamMessage( + msgURI, + streamListener, + msgWindow, + null, + true, // Have them create the converter. + "header=filter", + false + ); + + let streamedData = await streamListener.promise; + + // Check that the attachments' filenames are as expected. Just use a regex + // here because it's simple. + let regex1 = + /<legend class="moz-mime-attachment-header-name">(.*?)<\/legend>/gi; + + for (let attachment of info.attachments) { + let match = regex1.exec(streamedData); + Assert.notEqual(match, null); + Assert.equal(match[1], attachment.expectedFilename || attachment.filename); + } + Assert.equal(regex1.exec(streamedData), null); + + // Check the attachments' filenames are listed for printing. + let regex2 = /<td class="moz-mime-attachment-file">(.*?)<\/td>/gi; + + for (let attachment of info.attachments) { + let match = regex2.exec(streamedData); + Assert.notEqual(match, null); + Assert.equal(match[1], attachment.expectedFilename || attachment.filename); + } + Assert.equal(regex2.exec(streamedData), null); +} + +add_task(function endTest() { + messageInjection.teardownMessageInjection(); +}); diff --git a/comm/mailnews/mime/test/unit/test_mimeContentType.js b/comm/mailnews/mime/test/unit/test_mimeContentType.js new file mode 100644 index 0000000000..fb40549f35 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_mimeContentType.js @@ -0,0 +1,82 @@ +/* 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/. */ + +function run_test() { + const headers = [ + { + header: + "Content-Type: text/plain\r\n" + + "Content-Disposition: inline\r\n" + + "\r\n", + result: "text/plain", + }, + { + header: + "Content-Type:\r\n" + + "\tapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + 'Content-Disposition: attachment; filename="List.xlsx"\r\n' + + "\r\n", + result: + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + { + header: + "Content-Type: \r\n" + + " application/vnd.openxmlformats-officedocument.presentationml.presentation;\r\n" + + ' name="Presentation.pptx"\r\n' + + "\r\n", + result: + "application/vnd.openxmlformats-officedocument.presentationml.presentation;" + + ' name="Presentation.pptx"', + }, + { + header: + "Content-Type:\r\n" + + "text/plain; charset=utf-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "Content-Disposition: inline\r\n" + + "\r\n", + result: "", + }, + { + header: "Content-Type:\r\n\r\n", + result: "", + }, + { + /* possible crash case for Bug 574961 */ + header: + "Content-Type: \r\n" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \r\n", + result: "", + }, + ]; + + let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + + for (let i = 0; i < headers.length; i++) { + mimeHdr.initialize(headers[i].header); + let receivedHeader = mimeHdr.extractHeader("Content-Type", false); + + dump( + "\nTesting Content-Type: " + + receivedHeader + + " == " + + headers[i].result + + "\n" + ); + + Assert.equal(receivedHeader, headers[i].result); + } +} diff --git a/comm/mailnews/mime/test/unit/test_mimeStreaming.js b/comm/mailnews/mime/test/unit/test_mimeStreaming.js new file mode 100644 index 0000000000..3e82fe51db --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_mimeStreaming.js @@ -0,0 +1,88 @@ +/* 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 test iterates over the test files in gTestFiles, and streams + * each as a message and makes sure the streaming doesn't assert or crash. + */ +const { localAccountUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/LocalAccountUtils.jsm" +); + +var gTestFiles = ["../../../data/bug505221", "../../../data/bug513543"]; + +var gMessages; + +var gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); + +var gUrlListener = { + OnStartRunningUrl(aUrl) {}, + OnStopRunningUrl(aUrl, aExitCode) { + do_test_finished(); + }, +}; + +localAccountUtils.loadLocalMailAccount(); + +add_task(async function run_the_test() { + do_test_pending(); + localAccountUtils.inboxFolder.QueryInterface(Ci.nsIMsgLocalMailFolder); + for (let fileName of gTestFiles) { + localAccountUtils.inboxFolder.addMessage( + await IOUtils.readUTF8(do_get_file(fileName).path) + ); + } + gMessages = [ + ...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages(), + ]; + doNextTest(); +}); + +function streamMsg(msgHdr) { + let msgURI = localAccountUtils.inboxFolder.getUriForMsg(msgHdr); + let msgService = MailServices.messageServiceFromURI(msgURI); + msgService.streamMessage( + msgURI, + gStreamListener, + null, + gUrlListener, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + true + ); +} + +var gStreamListener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + _stream: null, + // nsIRequestObserver part + onStartRequest(aRequest) {}, + onStopRequest(aRequest, aStatusCode) { + doNextTest(); + }, + + /* okay, our onDataAvailable should actually never be called. the stream + converter is actually eating everything except the start and stop + notification. */ + // nsIStreamListener part + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + if (this._stream === null) { + this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + this._stream.init(aInputStream); + } + this._stream.read(aCount); + }, +}; + +function doNextTest() { + if (gMessages.length > 0) { + let msgHdr = gMessages.shift(); + streamMsg(msgHdr); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js new file mode 100644 index 0000000000..4636136f17 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser1.js @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsIMsgHeaderParser functions. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + var checks = [ + ["", "test@foo.invalid", "test@foo.invalid"], + ["Test", "test@foo.invalid", "Test <test@foo.invalid>"], + ["Test", '"abc!x.yz"@foo.invalid', 'Test <"abc!x.yz"@foo.invalid>'], + ["Test", "test.user@foo.invalid", "Test <test.user@foo.invalid>"], + ["Test", "test@[xyz!]", "Test <test@[xyz!]>"], + // Based on RFC 2822 A.1.1 + ["John Doe", "jdoe@machine.example", "John Doe <jdoe@machine.example>"], + // Next 2 tests Based on RFC 2822 A.1.2 + [ + "Joe Q. Public", + "john.q.public@example.com", + '"Joe Q. Public" <john.q.public@example.com>', + ], + [ + 'Giant; "Big" Box', + "sysservices@example.net", + '"Giant; \\"Big\\" Box" <sysservices@example.net>', + ], + ["trailing", "t1@example.com ", "trailing <t1@example.com>"], + ["leading", " t2@example.com", "leading <t2@example.com>"], + [ + "leading trailing", + " t3@example.com ", + "leading trailing <t3@example.com>", + ], + ["", " t4@example.com ", "t4@example.com"], + ]; + + // Test - empty strings + + Assert.equal(MailServices.headerParser.makeMimeAddress("", ""), ""); + + // Test - makeMimeAddress + + for (let i = 0; i < checks.length; ++i) { + Assert.equal( + MailServices.headerParser.makeMimeAddress(checks[i][0], checks[i][1]), + checks[i][2] + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js new file mode 100644 index 0000000000..5d1df70330 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser2.js @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsIMsgHeaderParser functions: + * extractHeaderAddressMailboxes + * extractFirstName + * parseDecodedHeader + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + // In this array, the sub arrays consist of the following elements: + // 0: input string (a comma separated list of recipients) + // 1: expected output from extractHeaderAddressMailboxes + // 2: list of recipient names in the string + // 3: first recipient name in the string + const checks = [ + [ + "abc@foo.invalid", + "abc@foo.invalid", + "abc@foo.invalid", + "abc@foo.invalid", + ], + ["foo <ghj@foo.invalid>", "ghj@foo.invalid", "foo", "foo"], + [ + "abc@foo.invalid, foo <ghj@foo.invalid>", + "abc@foo.invalid, ghj@foo.invalid", + "abc@foo.invalid, foo", + "abc@foo.invalid", + ], + ["foo bar <foo@bar.invalid>", "foo@bar.invalid", "foo bar", "foo bar"], + [ + "foo bar <foo@bar.invalid>, abc@foo.invalid, foo <ghj@foo.invalid>", + "foo@bar.invalid, abc@foo.invalid, ghj@foo.invalid", + "foo bar, abc@foo.invalid, foo", + "foo bar", + ], + // UTF-8 names + [ + "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>", + "foo@bar.invalid, ghj@foo.invalid", + "foo\u00D0 bar, \u00F6foo", + "foo\u00D0 bar", + ], + // More complicated examples drawn from RFC 2822 + [ + '"Joe Q. Public" <john.q.public@example.com>,Test <"abc!x.yz"@foo.invalid>, Test <test@[xyz!]>,"Giant; \\"Big\\" Box" <sysservices@example.net>', + 'john.q.public@example.com, "abc!x.yz"@foo.invalid, test@[xyz!], sysservices@example.net', + 'Joe Q. Public, Test, Test, Giant; "Big" Box', + // extractFirstName returns unquoted names, hence the difference. + "Joe Q. Public", + ], + // Bug 549931 + [ + "Undisclosed recipients:;", + "", // Mailboxes + "", // Address Names + "", + ], // Address Name + ]; + + // Test - empty strings + + Assert.equal(MailServices.headerParser.extractHeaderAddressMailboxes(""), ""); + Assert.equal(MailServices.headerParser.extractFirstName(""), ""); + + // Test - extractHeaderAddressMailboxes + + for (let i = 0; i < checks.length; ++i) { + Assert.equal( + MailServices.headerParser.extractHeaderAddressMailboxes(checks[i][0]), + checks[i][1] + ); + let _names = MailServices.headerParser + .parseDecodedHeader(checks[i][0]) + .map(addr => addr.name || addr.email) + .join(", "); + Assert.equal(_names, checks[i][2]); + Assert.equal( + MailServices.headerParser.extractFirstName(checks[i][0]), + checks[i][3] + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js new file mode 100644 index 0000000000..06be599d93 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser3.js @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsIMsgHeaderParser function removeDuplicateAddresses: + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + const checks = [ + { + addrs: "test@foo.invalid", + otherAddrs: "", + expectedResult: "test@foo.invalid", + }, + { + addrs: "foo bar <test@foo.invalid>", + otherAddrs: "", + expectedResult: "foo bar <test@foo.invalid>", + }, + { + addrs: "foo bar <test@foo.invalid>, abc@foo.invalid", + otherAddrs: "", + expectedResult: "foo bar <test@foo.invalid>, abc@foo.invalid", + }, + { + addrs: + "foo bar <test@foo.invalid>, abc@foo.invalid, test <test@foo.invalid>", + otherAddrs: "", + expectedResult: "foo bar <test@foo.invalid>, abc@foo.invalid", + }, + { + addrs: "foo bar <test@foo.invalid>", + otherAddrs: "abc@foo.invalid", + expectedResult: "foo bar <test@foo.invalid>", + }, + { + addrs: "foo bar <test@foo.invalid>", + otherAddrs: "foo bar <test@foo.invalid>", + expectedResult: "", + }, + { + addrs: "foo bar <test@foo.invalid>, abc@foo.invalid", + otherAddrs: "foo bar <test@foo.invalid>", + expectedResult: "abc@foo.invalid", + }, + { + addrs: "foo bar <test@foo.invalid>, abc@foo.invalid", + otherAddrs: "abc@foo.invalid", + expectedResult: "foo bar <test@foo.invalid>", + }, + { + addrs: + "foo bar <test@foo.invalid>, abc@foo.invalid, test <test@foo.invalid>", + otherAddrs: "abc@foo.invalid", + expectedResult: "foo bar <test@foo.invalid>", + }, + // UTF-8 names + { + addrs: "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>", + otherAddrs: "", + expectedResult: + "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>", + }, + { + addrs: "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>", + otherAddrs: "foo\u00D0 bar <foo@bar.invalid>", + expectedResult: "\u00F6foo <ghj@foo.invalid>", + }, + { + addrs: + "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>, foo\u00D0 bar <foo@bar.invalid>", + otherAddrs: "\u00F6foo <ghj@foo.invalid>", + expectedResult: "foo\u00D0 bar <foo@bar.invalid>", + }, + // Test email groups + { + addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;", + otherAddrs: "foo <ghj@foo.invalid>", + expectedResult: "A group: foo bar <foo@bar.invalid>;", + }, + { + addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;", + otherAddrs: "foo bar <ghj@foo.invalid>", + expectedResult: "A group: foo bar <foo@bar.invalid>;", + }, + { + addrs: "A group: foo bar <foo@bar.invalid>;, foo <ghj@foo.invalid>", + otherAddrs: "foo <foo@bar.invalid>", + expectedResult: "A group: ; , foo <ghj@foo.invalid>", + }, + ]; + + // Test - empty strings + + Assert.equal(MailServices.headerParser.removeDuplicateAddresses("", ""), ""); + Assert.equal( + MailServices.headerParser.removeDuplicateAddresses("", "test@foo.invalid"), + "" + ); + + // Test - removeDuplicateAddresses + + for (let i = 0; i < checks.length; ++i) { + dump("Test " + i + "\n"); + Assert.equal( + MailServices.headerParser.removeDuplicateAddresses( + checks[i].addrs, + checks[i].otherAddrs + ), + checks[i].expectedResult + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js new file mode 100644 index 0000000000..e4573ae37a --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser4.js @@ -0,0 +1,199 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/** + * Test suite for nsIMsgHeaderParser::makeFromDisplayAddress. + * This is what is used to parse in the user input from addressing fields. + */ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function run_test() { + const checks = [ + { displayString: "", addresses: [] }, + { + displayString: "test@foo.invalid", + addresses: [["", "test@foo.invalid"]], + }, + { + displayString: "test@foo.invalid, test2@foo.invalid", + addresses: [ + ["", "test@foo.invalid"], + ["", "test2@foo.invalid"], + ], + }, + { + displayString: "John Doe <test@foo.invalid>", + addresses: [["John Doe", "test@foo.invalid"]], + }, + // Trim spaces. + { + displayString: " John Doe <test@foo.invalid>", + addresses: [["John Doe", "test@foo.invalid"]], + }, + // No space before the email address. + { + displayString: " John Doe<test@foo.invalid>", + addresses: [["John Doe", "test@foo.invalid"]], + }, + // Additional text after the email address to be ignored. + { + displayString: " John Doe<test@foo.invalid> Junior", + addresses: [["John Doe", "test@foo.invalid"]], + }, + { + displayString: "Doe, John <test@foo.invalid>", + addresses: [["Doe, John", "test@foo.invalid"]], + }, + { + displayString: + "Doe, John <test@foo.invalid>, Bond, James <test2@foo.invalid>", + addresses: [ + ["Doe, John", "test@foo.invalid"], + ["Bond, James", "test2@foo.invalid"], + ], + }, + // Additional text after the email address to be ignored, multiple addresses. + { + displayString: + "Doe, John <test@foo.invalid>Junior, Bond, James <test2@foo.invalid>007", + addresses: [ + ["Doe, John", "test@foo.invalid"], + ["Bond, James", "test2@foo.invalid"], + ], + }, + // Multiple commas + { + displayString: + "Doe,, John <test@foo.invalid>,, Bond, James <test2@foo.invalid>, , Gold Finger <goldfinger@example.com> ,, ", + addresses: [ + ["Doe,, John", "test@foo.invalid"], + ["Bond, James", "test2@foo.invalid"], + ["Gold Finger", "goldfinger@example.com"], + ], + }, + // More tests where the user forgot to close the quote or added extra quotes. + { + displayString: '"Yatter King1 <a@a.a.a>', + addresses: [['"Yatter King1', "a@a.a.a"]], + }, + { + displayString: 'Yatter King2" <a@a.a.a>', + addresses: [['Yatter King2"', "a@a.a.a"]], + }, + { + displayString: '"Yatter King3" <a@a.a.a>', + addresses: [['"Yatter King3"', "a@a.a.a"]], + }, + { + displayString: 'Yatter "XXX" King4 <a@a.a.a>', + addresses: [['Yatter "XXX" King4', "a@a.a.a"]], + }, + { + displayString: '"Yatter "XXX" King5" <a@a.a.a>', + addresses: [['"Yatter "XXX" King5"', "a@a.a.a"]], + }, + { + displayString: '"Yatter King6 <a@a.a.a>"', + addresses: [["Yatter King6", "a@a.a.a"]], + }, + { + displayString: '"Yatter King7 <a@a.a.a>" <b@b.b.b>', + addresses: [['"Yatter King7 <a@a.a.a>"', "b@b.b.b"]], + }, + // Handle invalid mailbox separation with semicolons gracefully. + { + displayString: + 'Bart <bart@example.com> ; lisa@example.com; "Homer, J; President" <pres@example.com>, Marge <marge@example.com>; ', + addresses: [ + ["Bart", "bart@example.com"], + ["", "lisa@example.com"], + ['"Homer, J; President"', "pres@example.com"], + ["Marge", "marge@example.com"], + ], + }, + // Junk after a bracketed email address to be ignored. + { + displayString: "<attacker@example.com>friend@example.com", + addresses: [["", "attacker@example.com"]], + }, + { + displayString: + "<attacker2@example.com><friend2@example.com>,foo <attacker3@example.com><friend3@example.com>", + addresses: [ + ["", "attacker2@example.com"], + ["foo", "attacker3@example.com"], + ], + }, + { + displayString: + 'jay "bad" ass <name@evil.com> <someone-else@bad.com> <name@evil.commercial.org>', + addresses: [['jay "bad" ass', "name@evil.com"]], + }, + + { + displayString: + 'me "you" (via foo@example.com) <attacker2@example.com> friend@example.com,', + addresses: [['me "you" (via foo@example.com)', "attacker2@example.com"]], + }, + + // An uncompleted autocomplete... + { + displayString: "me >> test <joe@examp.com>", + addresses: [["me >> test", "joe@examp.com"]], + }, + + // A mail list. + { + displayString: "Holmes and Watson <Tenants221B>, foo@example.com", + addresses: [ + ["Holmes and Watson", "Tenants221B"], + ["", "foo@example.com"], + ], + }, + + // A mail list with a space in the name. + { + displayString: 'Watson and Holmes <"Quoted Tenants221B">', + addresses: [["Watson and Holmes", '"Quoted Tenants221B"']], + }, + + // Mail Merge template + { + displayString: "{{PrimaryEmail}} <>", + addresses: [["{{PrimaryEmail}}", ""]], + }, + + // Quoted heart. + { + displayString: 'Marge "<3" S <qheart@example.com>', + addresses: [['Marge "<3" S', "qheart@example.com"]], + }, + + // Heart. + { + displayString: "Maggie <3 S <heart@example.com>", + addresses: [["Maggie <3 S", "heart@example.com"]], + }, + + // Unbalanced quotes. + { + displayString: 'Homer <3 "B>" "J <unb@example.com>', + addresses: [['Homer <3 "B>" "J', "unb@example.com"]], + }, + ]; + + // Test - strings + + for (let i = 0; i < checks.length; ++i) { + let addrs = MailServices.headerParser.makeFromDisplayAddress( + checks[i].displayString + ); + let checkaddrs = checks[i].addresses; + Assert.equal(addrs.length, checkaddrs.length, "Number of parsed addresses"); + for (let j = 0; j < addrs.length; j++) { + Assert.equal(addrs[j].name, checkaddrs[j][0], "Parsed name"); + Assert.equal(addrs[j].email, checkaddrs[j][1], "Parsed email"); + } + } +} diff --git a/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js new file mode 100644 index 0000000000..a118d44641 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_nsIMsgHeaderParser5.js @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsIMsgHeaderParser functions: + * parseDecodedHeader + * parseEncodedHeader + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function equalArrays(arr1, arr2) { + Assert.equal(arr1.length, arr2.length); + for (let i = 0; i < arr1.length; i++) { + Assert.equal(arr1[i].name, arr2[i].name); + Assert.equal(arr1[i].email, arr2[i].email); + } +} + +function run_test() { + // In this array, the sub arrays consist of the following elements: + // 0: input string + // 1: expected output from parseDecodedHeader + // 2: expected output from parseEncodedHeader + const checks = [ + [ + "abc@foo.invalid", + [{ name: "", email: "abc@foo.invalid" }], + [{ name: "", email: "abc@foo.invalid" }], + ], + [ + "foo <ghj@foo.invalid>", + [{ name: "foo", email: "ghj@foo.invalid" }], + [{ name: "foo", email: "ghj@foo.invalid" }], + ], + [ + "abc@foo.invalid, foo <ghj@foo.invalid>", + [ + { name: "", email: "abc@foo.invalid" }, + { name: "foo", email: "ghj@foo.invalid" }, + ], + [ + { name: "", email: "abc@foo.invalid" }, + { name: "foo", email: "ghj@foo.invalid" }, + ], + ], + // UTF-8 names + [ + "foo\u00D0 bar <foo@bar.invalid>, \u00C3\u00B6foo <ghj@foo.invalid>", + [ + { name: "foo\u00D0 bar", email: "foo@bar.invalid" }, + { name: "\u00C3\u00B6foo", email: "ghj@foo.invalid" }, + ], + [ + { name: "foo\uFFFD bar", email: "foo@bar.invalid" }, + { name: "\u00F6foo", email: "ghj@foo.invalid" }, + ], + ], + // Bug 961564 + [ + "someone <>", + [{ name: "someone", email: "" }], + [{ name: "someone", email: "" }], + ], + // Bug 1423432: Encoded strings with null bytes, + // in base64 a single null byte can be encoded as AA== to AP==. + // parseEncodedHeader will remove the nullbyte. + [ + '"null=?UTF-8?Q?=00?=byte" <nullbyte@example.com>', + [{ name: "null=?UTF-8?Q?=00?=byte", email: "nullbyte@example.com" }], + [{ name: "nullbyte", email: "nullbyte@example.com" }], + ], + [ + '"null=?UTF-8?B?AA==?=byte" <nullbyte@example.com>', + [{ name: "null=?UTF-8?B?AA==?=byte", email: "nullbyte@example.com" }], + [{ name: "nullbyte", email: "nullbyte@example.com" }], + ], + ["", [], []], + [" \r\n\t", [], []], + [ + // This used to cause memory read overruns. + '" "@a a;b', + [ + { name: "", email: '" "@a a' }, + { name: "b", email: "" }, + ], + [ + { name: "", email: "@a a" }, + { name: "b", email: "" }, + ], + ], + ]; + + for (let check of checks) { + equalArrays( + MailServices.headerParser.parseDecodedHeader(check[0]), + check[1] + ); + equalArrays( + MailServices.headerParser.parseEncodedHeader(check[0], "UTF-8"), + check[2] + ); + } +} diff --git a/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js b/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js new file mode 100644 index 0000000000..0e5748fd9c --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_openpgp_decrypt.js @@ -0,0 +1,415 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests to ensure signed and/or encrypted OpenPGP messages are + * processed correctly by mime. + */ + +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +const { OpenPGPTestUtils } = ChromeUtils.import( + "resource://testing-common/mozmill/OpenPGPTestUtils.jsm" +); +const { EnigmailSingletons } = ChromeUtils.import( + "chrome://openpgp/content/modules/singletons.jsm" +); +const { EnigmailVerify } = ChromeUtils.import( + "chrome://openpgp/content/modules/mimeVerify.jsm" +); +const { EnigmailConstants } = ChromeUtils.import( + "chrome://openpgp/content/modules/constants.jsm" +); +const { EnigmailDecryption } = ChromeUtils.import( + "chrome://openpgp/content/modules/decryption.jsm" +); + +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); + +var messageInjection = new MessageInjection({ mode: "local" }); +let gInbox = messageInjection.getInboxFolder(); + +const keyDir = "../../../../mail/test/browser/openpgp/data/keys/"; +const browserEMLDir = "../../../../mail/test/browser/openpgp/data/eml/"; + +const contents = "Sundays are nothing without callaloo."; + +/** + * This implements some of the methods of Enigmail.hdrView.headerPane so we can + * intercept and record the calls to updateSecurityStatus(). + */ +const headerSink = { + expectResults(maxLen) { + this._deferred = PromiseUtils.defer(); + this.expectedCount = maxLen; + this.countReceived = 0; + this.results = []; + EnigmailSingletons.messageReader = this; + return this._deferred.promise; + }, + isCurrentMessage() { + return true; + }, + isMultipartRelated() { + return false; + }, + displaySubPart() { + return true; + }, + hasUnauthenticatedParts() { + return false; + }, + processDecryptionResult() {}, + updateSecurityStatus( + unusedUriSpec, + exitCode, + statusFlags, + extStatusFlags, + keyId, + userId, + sigDetails, + errorMsg, + blockSeparation, + uri, + extraDetails, + mimePartNumber + ) { + if (statusFlags & EnigmailConstants.PGP_MIME_SIGNED) { + this.results.push({ + type: "signed", + status: statusFlags, + keyId, + }); + } else if (statusFlags & EnigmailConstants.PGP_MIME_ENCRYPTED) { + this.results.push({ + type: "encrypted", + status: statusFlags, + keyId, + }); + } + + this.countReceived++; + this.checkFinished(); + }, + modifyMessageHeaders() {}, + + checkFinished() { + if (this.countReceived == this.expectedCount) { + this._deferred.resolve(this.results); + } + }, +}; + +/** + * @name Test + * @property {string} filename - Name of the eml file found in ${browserEMLDir}. + * @property {string} contents - Contents to expect in the file. + * @property {string} from - The email address the message is from. + * @property {string} [keyId] - The key id to expect the message from. + * @property {boolean} sig - If true, indicates the message is signed. + * @property {boolean} enc - If true, indicates the message is encrypted. + * @property {string[]} flags - A list of flags corresponding to those found in + * EnigmailConstants that we should expect the processed message to posses. + * Prefix a flag with "-" to indicate it should not be present. + * @property {boolean} [skip] - If true, the test will be skipped. + */ + +/** + * All the tests we are going to run. + * + * @type Test[] + */ +const tests = [ + { + description: + "signed, unencrypted message, with key attached, from verified sender", + filename: + "signed-by-0xfbfcc82a015e7330-to-0xf231550c4f47e38e-unencrypted-with-key.eml", + contents, + from: "bob@openpgp.example", + keyId: OpenPGPTestUtils.BOB_KEY_ID, + sig: true, + flags: ["GOOD_SIGNATURE", "-DECRYPTION_OKAY"], + }, + { + description: "signed, unencrypted message, from verified sender", + filename: + "signed-by-0xfbfcc82a015e7330-to-0xf231550c4f47e38e-unencrypted.eml", + contents, + from: "bob@openpgp.example", + keyId: OpenPGPTestUtils.BOB_KEY_ID, + sig: true, + flags: ["GOOD_SIGNATURE", "-DECRYPTION_OKAY"], + }, + { + description: + "unsigned, encrypted message, with key attached, from verified sender", + filename: + "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330-with-key.eml", + contents, + from: "bob@openpgp.example", + enc: true, + flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"], + }, + { + description: "unsigned, encrypted message, from verified sender", + filename: + "unsigned-encrypted-to-0xf231550c4f47e38e-from-0xfbfcc82a015e7330.eml", + contents, + from: "bob@openpgp.example", + enc: true, + flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"], + }, + { + description: + "signed, encrypted message, with key attached from verified sender", + filename: + "signed-by-0xfbfcc82a015e7330-encrypted-to-0xf231550c4f47e38e-with-key.eml", + from: "bob@openpgp.example", + keyId: OpenPGPTestUtils.BOB_KEY_ID, + contents, + enc: true, + sig: true, + flags: ["DECRYPTION_OKAY", "GOOD_SIGNATURE"], + }, + { + description: "signed, encrypted message, from verified sender", + filename: + "signed-by-0xfbfcc82a015e7330-encrypted-to-0xf231550c4f47e38e.eml", + from: "bob@openpgp.example", + keyId: OpenPGPTestUtils.BOB_KEY_ID, + contents, + enc: true, + sig: true, + flags: ["DECRYPTION_OKAY", "GOOD_SIGNATURE"], + }, + // Sender with no public key registered or accepted. + { + description: + "signed, unencrypted message, with key attached from sender not in database", + filename: + "signed-by-0x3099ff1238852b9f-to-0xf231550c4f47e38e-unencrypted-with-key.eml", + contents, + from: "carol@openpgp.example", + keyId: OpenPGPTestUtils.CAROL_KEY_ID, + sig: true, + flags: ["-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE", "NO_PUBKEY"], + }, + { + description: "signed, unencrypted message, from sender not in database", + filename: + "signed-by-0x3099ff1238852b9f-to-0xf231550c4f47e38e-unencrypted.eml", + contents, + from: "carol@openpgp.example", + keyId: OpenPGPTestUtils.CAROL_KEY_ID, + sig: true, + flags: ["-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE", "NO_PUBKEY"], + }, + { + description: + "unsigned, encrypted message, with key attached, from sender not in database", + filename: + "unsigned-encrypted-to-0xf231550c4f47e38e-from-0x3099ff1238852b9f-with-key.eml", + contents, + from: "carol@openpgp.example", + enc: true, + flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"], + }, + { + description: "unsigned, encrypted message, from sender not in database", + filename: + "unsigned-encrypted-to-0xf231550c4f47e38e-from-0x3099ff1238852b9f.eml", + contents, + from: "carol@openpgp.example", + enc: true, + flags: ["DECRYPTION_OKAY", "-GOOD_SIGNATURE"], + }, + { + description: + "signed, encrypted message, with key attached, from sender not in database", + filename: + "signed-by-0x3099ff1238852b9f-encrypted-to-0xf231550c4f47e38e-with-key.eml", + contents, + from: "carol@openpgp.example", + keyId: OpenPGPTestUtils.CAROL_KEY_ID, + enc: true, + sig: true, + resultCount: 1, + flags: ["-DECRYPTION_FAILED", "-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE"], + }, + { + description: "signed, encrypted message, from sender not in database", + filename: + "signed-by-0x3099ff1238852b9f-encrypted-to-0xf231550c4f47e38e.eml", + contents, + from: "carol@openpgp.example", + keyId: OpenPGPTestUtils.CAROL_KEY_ID, + enc: true, + sig: true, + resultCount: 1, + flags: ["-DECRYPTION_FAILED", "-GOOD_SIGNATURE", "UNCERTAIN_SIGNATURE"], + }, + // Last two characters of signature swapped. + { + description: "signed message, signature damaged", + filename: "bob-to-alice-signed-damaged-signature.eml", + from: "bob@openpgp.example", + contents, + sig: true, + flags: ["-GOOD_SIGNATURE", "BAD_SIGNATURE"], + }, +]; + +/** + * Initialize OpenPGP, import Alice and Bob's keys, then install the messages + * we are going to test. + */ +add_setup(async function () { + await OpenPGPTestUtils.initOpenPGP(); + + await OpenPGPTestUtils.importPrivateKey( + null, + do_get_file(`${keyDir}alice@openpgp.example-0xf231550c4f47e38e-secret.asc`) + ); + + await OpenPGPTestUtils.importPublicKey( + null, + do_get_file(`${keyDir}bob@openpgp.example-0xfbfcc82a015e7330-pub.asc`) + ); + + for (let test of tests) { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + + MailServices.copy.copyFileMessage( + do_get_file(`${browserEMLDir}${test.filename}`), + gInbox, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + + await promiseCopyListener.promise; + promiseCopyListener = null; + } +}); + +/** + * This executes a test for each entry in the tests array. We test mostly + * that the contents are correct and updateSecurityStatus() repoorts the + * status flags the test specifies. + */ +add_task(async function testMimeDecryptOpenPGPMessages() { + let hdrIndex = 0; + for (let test of tests) { + if (test.skip) { + info(`Skipped test: ${test.description}`); + continue; + } + + info(`Running test: ${test.description}`); + + let testPrefix = `${test.filename}:`; + let expectedResultCount = + test.resultCount || (test.enc && test.sig) ? 2 : 1; + let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex); + let uri = hdr.folder.getUriForMsg(hdr); + let sinkPromise = headerSink.expectResults(expectedResultCount); + + // Set the message window so displayStatus() invokes the hooks we are + // interested in. + EnigmailVerify.lastWindow = {}; + + // Stub this function so verifyDetached() can get the correct email. + EnigmailDecryption.getFromAddr = () => test.from; + + // Trigger the actual mime work. + let conversion = apply_mime_conversion(uri, headerSink); + + await conversion.promise; + + let msgBody = conversion._data; + + if (!test.sig || test.flags.indexOf("GOOD_SIGNATURE")) { + Assert.ok( + msgBody.includes(test.contents), + `${testPrefix} message contents match` + ); + } + + // Check that we're also using the display output. + Assert.ok( + msgBody.includes("<html>"), + `${testPrefix} message displayed as html` + ); + await sinkPromise; + + let idx = 0; + let { results } = headerSink; + + Assert.equal( + results.length, + expectedResultCount, + `${testPrefix} updateSecurityStatus() called ${expectedResultCount} time(s)` + ); + + if (test.enc) { + Assert.equal( + results[idx].type, + "encrypted", + `${testPrefix} message recognized as encrypted` + ); + + if (expectedResultCount > 1) { + idx++; + } + } + + if (test.sig) { + Assert.equal( + results[idx].type, + "signed", + `${testPrefix} message recognized as signed` + ); + } + + if (test.keyId) { + Assert.equal( + results[idx].keyId, + test.keyId, + `${testPrefix}key ids match` + ); + } + + // Test the expected message flags match the message status. + // We combine the signed and encrypted flags via bitwise OR to + // test in one place. + if (test.flags) { + for (let flag of test.flags) { + let flags = results.reduce((prev, curr) => prev | curr.status, 0); + let negative = flag[0] === "-"; + flag = negative ? flag.slice(1) : flag; + + if (negative) { + Assert.ok( + !(flags & EnigmailConstants[flag]), + `${testPrefix} status flag "${flag}" not detected` + ); + } else { + Assert.ok( + flags & EnigmailConstants[flag], + `${testPrefix} status flag "${flag}" detected` + ); + } + } + } + + hdrIndex++; + } +}); diff --git a/comm/mailnews/mime/test/unit/test_parser.js b/comm/mailnews/mime/test/unit/test_parser.js new file mode 100644 index 0000000000..979e50975c --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_parser.js @@ -0,0 +1,322 @@ +/* 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 file is used to test the mime parser implemented in JS, mostly by means +// of creating custom emitters and verifying that the methods on that emitter +// are called in the correct order. This also tests that the various +// HeaderParser methods are run correctly. + +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +// Utility method to compare objects +function compare_objects(real, expected) { + // real is a Map; convert it into an object for uneval purposes + if (typeof real == "object") { + var newreal = {}; + for (let [k, v] of real) { + newreal[k] = v; + } + real = newreal; + } + var a = uneval(real), + b = uneval(expected); + // Very long strings don't get printed out fully (unless they're wrong) + if ((a.length > 100 || b.length > 100) && a == b) { + Assert.ok(a == b); + } else { + Assert.equal(a, b); + } +} + +// Returns and deletes object[field] if present, or undefined if not. +function extract_field(object, field) { + if (field in object) { + var result = object[field]; + delete object[field]; + return result; + } + return undefined; +} + +// A file cache for read_file. +var file_cache = {}; + +/** + * Read a file into a string (all line endings become CRLF). + */ +async function read_file(file, start, end) { + if (!(file in file_cache)) { + var realFile = do_get_file("../../../data/" + file); + file_cache[file] = (await IOUtils.readUTF8(realFile.path)).split( + /\r\n|[\r\n]/ + ); + } + var contents = file_cache[file]; + if (start !== undefined) { + contents = contents.slice(start - 1, end - 1); + } + return contents.join("\r\n"); +} + +/** + * Helper for body tests. + * + * Some extra options are listed too: + * _split: The contents of the file will be passed in packets split by this + * regex. Be sure to include the split delimiter in a group so that they + * are included in the output packets! + * _eol: The CRLFs in the input file will be replaced with the given line + * ending instead. + * + * @param test The name of test + * @param file The name of the file to read (relative to mailnews/data) + * @param opts Options for the mime parser, as well as a few extras detailed + * above. + * @param partspec An array of [partnum, line start, line end] detailing the + * expected parts in the body. It will be expected that the + * accumulated body part data for partnum would be the contents + * of the file from [line start, line end) [1-based lines] + */ +async function make_body_test(test, file, opts, partspec) { + let results = []; + for (let p of partspec) { + results.push([p[0], await read_file(file, p[1], p[2])]); + } + + let msgcontents = await read_file(file); + return [test, msgcontents, opts, results]; +} + +async function make_bodydecode_test(test, file, opts, expected) { + let msgcontents = await read_file(file); + return [test, msgcontents, opts, expected]; +} + +// This is the expected part specifier for the multipart-complex1 test file, +// specified here because it is used in several cases. +var mpart_complex1 = [ + ["1", 8, 10], + ["2", 14, 16], + ["3.1", 22, 24], + ["4", 29, 31], + ["5", 33, 35], +]; + +// Format of tests: +// entry[0] = name of the test +// entry[1] = message (a string or an array of packets) +// entry[2] = options for the MIME parser +// entry[3] = A checker result: +// either a {partnum: header object} (to check headers) +// or a [[partnum body], [partnum body], ...] (to check bodies) +// (the partnums refer to the expected part numbers of the MIME test) +// For body tests, unless you're testing decoding, use make_body_test. +// For decoding tests, use make_bodydecode_test +var parser_tests = [ + // Body tests from data + // (Note: line numbers are 1-based. Also, to capture trailing EOF, add 2 to + // the last line number of the file). + make_body_test("Basic body", "basic1", {}, [["", 3, 5]]), + make_body_test("Basic multipart", "multipart1", {}, [["1", 10, 12]]), + make_body_test("Basic multipart", "multipart2", {}, [["1", 8, 11]]), + make_body_test("Complex multipart", "multipart-complex1", {}, mpart_complex1), + make_body_test("Truncated multipart", "multipart-complex2", {}, [ + ["1.1.1.1", 21, 25], + ["2", 27, 57], + ["3", 60, 62], + ]), + make_body_test("No LF multipart", "multipartmalt-detach", {}, [ + ["1", 20, 21], + ["2.1", 27, 38], + ["2.2", 42, 43], + ["2.3", 47, 48], + ]), + make_body_test("Raw body", "multipart1", { bodyformat: "raw" }, [ + ["", 4, 14], + ]), + make_bodydecode_test( + "Base64 decode 1", + "base64-1", + { bodyformat: "decode" }, + [ + [ + "", + "\r\nHello, world! (Again...)\r\n\r\nLet's see how well base64 text" + + " is handled. Yay, lots of spaces! There" + + "'s even a CRLF at the end and one at the beginning, but the output" + + " shouldn't have it.\r\n", + ], + ] + ), + make_bodydecode_test( + "Base64 decode 2", + "base64-2", + { bodyformat: "decode" }, + [ + [ + "", + "<html><body>This is base64 encoded HTML text, and the tags shouldn" + + "'t be stripped.\r\n<b>Bold text is bold!</b></body></html>\r\n", + ], + ] + ), + make_body_test("Base64 nodecode", "base64-1", {}, [["", 4, 9]]), + make_bodydecode_test( + "QP decode", + "bug505221", + { pruneat: "1", bodyformat: "decode" }, + [ + [ + "1", + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r' + + '\n<HTML><HEAD>\r\n<META HTTP-EQUIV="Content-Type" CONTENT="text/h' + + 'tml; charset=us-ascii">\r\n\r\n\r\n<META content="MSHTML 6.00.600' + + '0.16735" name=GENERATOR></HEAD>\r\n<BODY> bbb\r\n</BODY></HTML>', + ], + ] + ), + + // Comprehensive tests from the torture test + make_body_test("Torture regular body", "mime-torture", {}, [ + ["1", 17, 21], + ["2$.1", 58, 75], + ["2$.2.1", 83, 97], + ["2$.3", 102, 130], + ["3$", 155, 7742], + ["4", 7747, 8213], + ["5", 8218, 8242], + ["6$.1.1", 8284, 8301], + ["6$.1.2", 8306, 8733], + ["6$.2.1", 8742, 9095], + ["6$.2.2", 9100, 9354], + ["6$.2.3", 9357, 11794], + ["6$.2.4", 11797, 12155], + ["6$.3", 12161, 12809], + ["7$.1", 12844, 12845], + ["7$.2", 12852, 13286], + ["7$.3", 13288, 13297], + ["8$.1", 13331, 13358], + ["8$.2", 13364, 13734], + ["9$", 13757, 20179], + ["10", 20184, 21200], + ["11$.1", 21223, 22031], + ["11$.2", 22036, 22586], + ["12$.1", 22607, 23469], + ["12$.2", 23474, 23774], + ["12$.3$.1", 23787, 23795], + ["12$.3$.2.1", 23803, 23820], + ["12$.3$.2.2", 23825, 24633], + ["12$.3$.3", 24640, 24836], + ["12$.3$.4$", 24848, 25872], + ]), + make_body_test("Torture pruneat", "mime-torture", { pruneat: "4" }, [ + ["4", 7747, 8213], + ]), +]; + +function test_parser(message, opts, results) { + var checkingHeaders = !(results instanceof Array); + var calls = 0, + dataCalls = 0; + var fusingParts = extract_field(opts, "_nofuseparts") === undefined; + var emitter = { + stack: [], + startMessage: function emitter_startMsg() { + Assert.equal(this.stack.length, 0, "no stack at start"); + calls++; + this.partData = ""; + }, + endMessage: function emitter_endMsg() { + Assert.equal(this.stack.length, 0, "no stack at end"); + calls++; + }, + startPart: function emitter_startPart(partNum, headers) { + this.stack.push(partNum); + if (checkingHeaders) { + Assert.ok(partNum in results); + compare_objects(headers, results[partNum]); + if (fusingParts) { + Assert.equal(this.partData, ""); + } + } + }, + deliverPartData: function emitter_partData(partNum, data) { + Assert.equal(this.stack[this.stack.length - 1], partNum); + try { + if (!checkingHeaders) { + if (fusingParts) { + this.partData += data; + } else { + Assert.equal(partNum, results[dataCalls][0]); + compare_objects(data, results[dataCalls][1]); + } + } + } finally { + if (!fusingParts) { + dataCalls++; + } + } + }, + endPart: function emitter_endPart(partNum) { + if (this.partData != "") { + Assert.equal(partNum, results[dataCalls][0]); + compare_objects(this.partData, results[dataCalls][1]); + dataCalls++; + this.partData = ""; + } + Assert.equal(this.stack.pop(), partNum); + }, + }; + opts.onerror = function (e) { + throw e; + }; + MimeParser.parseSync(message, emitter, opts); + Assert.equal(calls, 2); + if (!checkingHeaders) { + Assert.equal(dataCalls, results.length); + } +} + +// Format of tests: +// entry[0] = header +// entry[1] = flags +// entry[2] = result to match +var header_tests = [ + // Parameter passing + ["multipart/related", MimeParser.HEADER_PARAMETER, ["multipart/related", {}]], + ["a ; b=v", MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]], + ["a ; b='v'", MimeParser.HEADER_PARAMETER, ["a", { b: "'v'" }]], + ['a; b = "v"', MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]], + ["a;b=1;b=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1" }]], + ["a;b=2;b=1", MimeParser.HEADER_PARAMETER, ["a", { b: "2" }]], + ['a;b="a;b"', MimeParser.HEADER_PARAMETER, ["a", { b: "a;b" }]], + ['a;b="\\\\"', MimeParser.HEADER_PARAMETER, ["a", { b: "\\" }]], + ['a;b="a\\b\\c"', MimeParser.HEADER_PARAMETER, ["a", { b: "abc" }]], + ["a;b=1;c=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1", c: "2" }]], + ['a;b="a\\', MimeParser.HEADER_PARAMETER, ["a", { b: "a" }]], + ["a;b", MimeParser.HEADER_PARAMETER, ["a", {}]], + ['a;b=";";c=d', MimeParser.HEADER_PARAMETER, ["a", { b: ";", c: "d" }]], +]; + +function test_header(headerValue, flags, expected) { + let result = MimeParser.parseHeaderField(headerValue, flags); + Assert.equal(result.preSemi, expected[0]); + compare_objects(result, expected[1]); +} + +add_task(async function testit() { + for (let test of parser_tests) { + test = await test; + dump("Testing message " + test[0]); + if (test[1] instanceof Array) { + dump(" using " + test[1].length + " packets"); + } + dump("\n"); + test_parser(test[1], test[2], test[3]); + } + for (let test of header_tests) { + dump("Testing value ->" + test[0] + "<- with flags " + test[1] + "\n"); + test_header(test[0], test[1], test[2]); + } +}); diff --git a/comm/mailnews/mime/test/unit/test_rfc822_body.js b/comm/mailnews/mime/test/unit/test_rfc822_body.js new file mode 100644 index 0000000000..8a64e68d42 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_rfc822_body.js @@ -0,0 +1,94 @@ +/* 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 test verifies that we emit a message/rfc822 body part as an attachment + * whether or not mail.inline_attachments is true. + */ + +var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); +var msgGen = new MessageGenerator(); +var messageInjection = new MessageInjection({ mode: "local" }); +var inbox = messageInjection.getInboxFolder(); + +add_task(async function test_rfc822_body_display_inline() { + Services.prefs.setBoolPref("mail.inline_attachments", true); + await help_test_rfc822_body({ + // a message whose body is itself a message + bodyPart: msgGen.makeMessage(), + attachmentCount: 1, + }); + await help_test_rfc822_body({ + // a message whose body is itself a message, and which has an attachment + bodyPart: msgGen.makeMessage({ + attachments: [ + { + body: "I'm an attachment!", + filename: "attachment.txt", + format: "", + }, + ], + }), + attachmentCount: 2, + }); +}); + +add_task(async function test_rfc822_body_no_display_inline() { + Services.prefs.setBoolPref("mail.inline_attachments", false); + await help_test_rfc822_body({ + // a message whose body is itself a message + bodyPart: msgGen.makeMessage(), + attachmentCount: 1, + }); + await help_test_rfc822_body({ + // a message whose body is itself a message, and which has an attachment + bodyPart: msgGen.makeMessage({ + attachments: [ + { + body: "I'm an attachment!", + filename: "attachment.txt", + format: "", + }, + ], + }), + attachmentCount: 1, + }); +}); + +async function help_test_rfc822_body(info) { + let synMsg = msgGen.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + + let streamListener = new PromiseTestUtils.PromiseStreamListener({ + onStopRequest(request, statusCode) { + request.QueryInterface(Ci.nsIMailChannel); + Assert.equal(request.attachments.length, info.attachmentCount); + }, + }); + msgService.streamMessage( + msgURI, + streamListener, + null, + null, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + false + ); + + await streamListener.promise; +} diff --git a/comm/mailnews/mime/test/unit/test_smime_decrypt.js b/comm/mailnews/mime/test/unit/test_smime_decrypt.js new file mode 100644 index 0000000000..815f786224 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_smime_decrypt.js @@ -0,0 +1,701 @@ +/* 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 to ensure signed and/or encrypted S/MIME messages are + * processed correctly, and the signature status is treated as good + * or bad as expected. + */ + +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +var { SmimeUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/smimeUtils.jsm" +); + +add_setup(function () { + let messageInjection = new MessageInjection({ mode: "local" }); + gInbox = messageInjection.getInboxFolder(); + SmimeUtils.ensureNSS(); + + SmimeUtils.loadPEMCertificate( + do_get_file(smimeDataDirectory + "TestCA.pem"), + Ci.nsIX509Cert.CA_CERT + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Alice.p12"), + "nss" + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Bob.p12"), + "nss" + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Dave.p12"), + "nss" + ); +}); + +add_task(async function verifyTestCertsStillValid() { + // implementation of nsIDoneFindCertForEmailCallback + var doneFindCertForEmailCallback = { + findCertDone(email, cert) { + Assert.notEqual(cert, null); + if (!cert) { + Assert.ok( + false, + "The S/MIME test certificates are invalid today.\n" + + "Please look at the expiration date in file comm/mailnews/test/data/smime/expiration.txt\n" + + "If that date is in the past, new certificates need to be generated and committed.\n" + + "Follow the instructions in comm/mailnews/test/data/smime/README.md\n" + + "If that date is in the future, the test failure is unrelated to expiration and indicates " + + "an error in certificate validation." + ); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIDoneFindCertForEmailCallback"]), + }; + + let composeSecure = Cc[ + "@mozilla.org/messengercompose/composesecure;1" + ].createInstance(Ci.nsIMsgComposeSecure); + composeSecure.asyncFindCertByEmailAddr( + "Alice@example.com", + doneFindCertForEmailCallback + ); +}); + +var gInbox; + +var smimeDataDirectory = "../../../data/smime/"; + +let smimeHeaderSink = { + expectResults(maxLen) { + // dump("Restarting for next test\n"); + this._deferred = PromiseUtils.defer(); + this._expectedEvents = maxLen; + this.countReceived = 0; + this._results = []; + this.haveSignedBad = false; + this.haveEncryptionBad = false; + this.resultSig = null; + this.resultEnc = null; + this.resultSigFirst = undefined; + return this._deferred.promise; + }, + signedStatus(aNestingLevel, aSignedStatus, aSignerCert) { + console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel); + // dump("Signed message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveSignedBad) { + // override with newer allowed + this.resultSig = { + type: "signed", + status: aSignedStatus, + certificate: aSignerCert, + }; + if (aSignedStatus != 0) { + this.haveSignedBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = true; + } + } + this.countReceived++; + this.checkFinished(); + }, + encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) { + console.log( + "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel + ); + // dump("Encrypted message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveEncryptionBad) { + // override with newer allowed + this.resultEnc = { + type: "encrypted", + status: aEncryptedStatus, + certificate: aRecipientCert, + }; + if (aEncryptedStatus != 0) { + this.haveEncryptionBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = false; + } + } + this.countReceived++; + this.checkFinished(); + }, + checkFinished() { + if (this.countReceived == this._expectedEvents) { + if (this.resultSigFirst) { + this._results.push(this.resultSig); + if (this.resultEnc != null) { + this._results.push(this.resultEnc); + } + } else { + this._results.push(this.resultEnc); + if (this.resultSig != null) { + this._results.push(this.resultSig); + } + } + this._deferred.resolve(this._results); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]), +}; + +/** + * Note on FILENAMES taken from the NSS test suite: + * - env: CMS enveloped (encrypted) + * - dsig: CMS detached signature (with multipart MIME) + * - sig: CMS opaque signature (content embedded inside signature) + * - bad: message text does not match signature + * - mismatch: embedded content is different + * + * Control variables used for checking results: + * - env: If true, we expect a report to encryptionStatus() that message + * is encrypted. + * - sig: If true, we expect a report to signedStatus() that message + * is signed. + * - sig_good: If true, we expect that the reported signature has a + * good status. + * If false, we expect a report of bad status. + * Because of the sequential processing caused by nested + * messages, additional calls to signedStatus() might + * override an earlier decision. + * (An earlier bad status report cannot be overridden by a + * later report of a good status.) + * - extra: If set to a number > 0, we expect that nested processing of + * MIME parts will trigger the given number of additional + * status calls. + * (default is 0.) + * - dave: If true, we expect that the outermost message was done by + * Dave's certificate. + * (default is false, which means we expect Alice's cert.) + */ + +var gMessages = [ + { + filename: "alice.env.eml", + enc: true, + sig: false, + sig_good: false, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA1.multipart.env.eml", + enc: true, + sig: true, + sig_good: false, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA256.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.future.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA256.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA256.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA384.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA384.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA384.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA512.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA512.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA512.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + check_text: true, + }, + { + filename: "alice.sig.SHA1.opaque.env.eml", + enc: true, + sig: true, + sig_good: false, + check_text: true, + }, + { + filename: "alice.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA256.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA384.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA512.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + + // encrypt-then-sign + { + filename: "alice.env.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.env.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + + // encrypt-then-sign, then sign again + { + filename: "alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + + // sign, then sign again + { + filename: "alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + + { + filename: "alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + }, + { + filename: "alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, +]; + +let gCopyWaiter = PromiseUtils.defer(); + +add_task(async function copy_messages() { + for (let msg of gMessages) { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + + MailServices.copy.copyFileMessage( + do_get_file(smimeDataDirectory + msg.filename), + gInbox, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + + await promiseCopyListener.promise; + promiseCopyListener = null; + } + gCopyWaiter.resolve(); +}); + +add_task(async function check_smime_message() { + await gCopyWaiter.promise; + + let hdrIndex = 0; + + for (let msg of gMessages) { + console.log("checking " + msg.filename); + + let numExpected = 1; + if (msg.enc && msg.sig) { + numExpected++; + } + + let eventsExpected = numExpected; + if ("extra" in msg) { + eventsExpected += msg.extra; + } + + let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex); + let uri = hdr.folder.getUriForMsg(hdr); + let sinkPromise = smimeHeaderSink.expectResults(eventsExpected); + + let conversion = apply_mime_conversion(uri, smimeHeaderSink); + await conversion.promise; + + let contents = conversion._data; + // dump("contents: " + contents + "\n"); + + if (!msg.sig || msg.sig_good || "check_text" in msg) { + let expected = "This is a test message from Alice to Bob."; + Assert.ok(contents.includes(expected)); + } + // Check that we're also using the display output. + Assert.ok(contents.includes("<html>")); + + await sinkPromise; + + let r = smimeHeaderSink._results; + Assert.equal(r.length, numExpected); + + let sigIndex = 0; + + if (msg.enc) { + Assert.equal(r[0].type, "encrypted"); + Assert.equal(r[0].status, 0); + Assert.equal(r[0].certificate, null); + sigIndex = 1; + } + if (msg.sig) { + Assert.equal(r[sigIndex].type, "signed"); + let cert = r[sigIndex].certificate; + if (msg.sig_good) { + Assert.notEqual(cert, null); + } + if (cert) { + if ("dave" in msg) { + Assert.equal(cert.emailAddress, "dave@example.com"); + } else { + Assert.equal(cert.emailAddress, "alice@example.com"); + } + } + if (msg.sig_good) { + Assert.equal(r[sigIndex].status, 0); + } else { + Assert.notEqual(r[sigIndex].status, 0); + } + } + + hdrIndex++; + } +}); diff --git a/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js b/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js new file mode 100644 index 0000000000..3e2eff43ae --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_smime_decrypt_allow_sha1.js @@ -0,0 +1,717 @@ +/* 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 file is mostly a copy of test_smime_decrypt.js + * with the difference that pref + * mail.smime.accept_insecure_sha1_message_signatures is set to true, + * and tests using sha-1 are expected to pass. + * + * This file must not run in parallel with other s/mime tests. + */ + +/** + * Tests to ensure signed and/or encrypted S/MIME messages are + * processed correctly, and the signature status is treated as good + * or bad as expected. + */ + +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +var { SmimeUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/smimeUtils.jsm" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref( + "mail.smime.accept_insecure_sha1_message_signatures" + ); +}); + +add_setup(function () { + Services.prefs.setBoolPref( + "mail.smime.accept_insecure_sha1_message_signatures", + true + ); + + let messageInjection = new MessageInjection({ mode: "local" }); + gInbox = messageInjection.getInboxFolder(); + SmimeUtils.ensureNSS(); + + SmimeUtils.loadPEMCertificate( + do_get_file(smimeDataDirectory + "TestCA.pem"), + Ci.nsIX509Cert.CA_CERT + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Alice.p12"), + "nss" + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Bob.p12"), + "nss" + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Dave.p12"), + "nss" + ); +}); + +add_task(async function verifyTestCertsStillValid() { + // implementation of nsIDoneFindCertForEmailCallback + var doneFindCertForEmailCallback = { + findCertDone(email, cert) { + Assert.notEqual(cert, null); + if (!cert) { + Assert.ok( + false, + "The S/MIME test certificates are invalid today.\n" + + "Please look at the expiration date in file comm/mailnews/test/data/smime/expiration.txt\n" + + "If that date is in the past, new certificates need to be generated and committed.\n" + + "Follow the instructions in comm/mailnews/test/data/smime/README.md\n" + + "If that date is in the future, the test failure is unrelated to expiration and indicates " + + "an error in certificate validation." + ); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIDoneFindCertForEmailCallback"]), + }; + + let composeSecure = Cc[ + "@mozilla.org/messengercompose/composesecure;1" + ].createInstance(Ci.nsIMsgComposeSecure); + composeSecure.asyncFindCertByEmailAddr( + "Alice@example.com", + doneFindCertForEmailCallback + ); +}); + +var gInbox; + +var smimeDataDirectory = "../../../data/smime/"; + +let smimeHeaderSink = { + expectResults(maxLen) { + // dump("Restarting for next test\n"); + this._deferred = PromiseUtils.defer(); + this._expectedEvents = maxLen; + this.countReceived = 0; + this._results = []; + this.haveSignedBad = false; + this.haveEncryptionBad = false; + this.resultSig = null; + this.resultEnc = null; + this.resultSigFirst = undefined; + return this._deferred.promise; + }, + signedStatus(aNestingLevel, aSignedStatus, aSignerCert) { + console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel); + // dump("Signed message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveSignedBad) { + // override with newer allowed + this.resultSig = { + type: "signed", + status: aSignedStatus, + certificate: aSignerCert, + }; + if (aSignedStatus != 0) { + this.haveSignedBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = true; + } + } + this.countReceived++; + this.checkFinished(); + }, + encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) { + console.log( + "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel + ); + // dump("Encrypted message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveEncryptionBad) { + // override with newer allowed + this.resultEnc = { + type: "encrypted", + status: aEncryptedStatus, + certificate: aRecipientCert, + }; + if (aEncryptedStatus != 0) { + this.haveEncryptionBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = false; + } + } + this.countReceived++; + this.checkFinished(); + }, + checkFinished() { + if (this.countReceived == this._expectedEvents) { + if (this.resultSigFirst) { + this._results.push(this.resultSig); + if (this.resultEnc != null) { + this._results.push(this.resultEnc); + } + } else { + this._results.push(this.resultEnc); + if (this.resultSig != null) { + this._results.push(this.resultSig); + } + } + this._deferred.resolve(this._results); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]), +}; + +/** + * Note on FILENAMES taken from the NSS test suite: + * - env: CMS enveloped (encrypted) + * - dsig: CMS detached signature (with multipart MIME) + * - sig: CMS opaque signature (content embedded inside signature) + * - bad: message text does not match signature + * - mismatch: embedded content is different + * + * Control variables used for checking results: + * - env: If true, we expect a report to encryptionStatus() that message + * is encrypted. + * - sig: If true, we expect a report to signedStatus() that message + * is signed. + * - sig_good: If true, we expect that the reported signature has a + * good status. + * If false, we expect a report of bad status. + * Because of the sequential processing caused by nested + * messages, additional calls to signedStatus() might + * override an earlier decision. + * (An earlier bad status report cannot be overridden by a + * later report of a good status.) + * - extra: If set to a number > 0, we expect that nested processing of + * MIME parts will trigger the given number of additional + * status calls. + * (default is 0.) + * - dave: If true, we expect that the outermost message was done by + * Dave's certificate. + * (default is false, which means we expect Alice's cert.) + */ + +var gMessages = [ + { + filename: "alice.env.eml", + enc: true, + sig: false, + sig_good: false, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA1.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: true, + check_text: true, + }, + { + filename: "alice.dsig.SHA1.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA256.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA256.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA256.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA384.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA384.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA384.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA512.multipart.bad.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA512.multipart.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.dsig.SHA512.multipart.mismatch-econtent.eml", + enc: false, + sig: true, + sig_good: false, + }, + { + filename: "alice.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: true, + check_text: true, + }, + { + filename: "alice.sig.SHA1.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + check_text: true, + }, + { + filename: "alice.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA256.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA384.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: true, + }, + { + filename: "alice.sig.SHA512.opaque.env.eml", + enc: true, + sig: true, + sig_good: true, + }, + + // encrypt-then-sign + { + filename: "alice.env.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + extra: 1, + }, + + // encrypt-then-sign, then sign again + { + filename: "alice.env.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.env.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + + // sign, then sign again + { + filename: "alice.plain.sig.SHA1.opaque.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA1.multipart.dave.sig.SHA1.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA256.opaque.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA256.multipart.dave.sig.SHA256.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA384.opaque.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA384.multipart.dave.sig.SHA384.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA512.opaque.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA512.multipart.dave.sig.SHA512.opaque.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + + { + filename: "alice.plain.sig.SHA1.opaque.dave.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.dsig.SHA1.multipart.dave.dsig.SHA1.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA256.opaque.dave.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA256.multipart.dave.dsig.SHA256.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA384.opaque.dave.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA384.multipart.dave.dsig.SHA384.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: "alice.plain.sig.SHA512.opaque.dave.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, + { + filename: + "alice.plain.dsig.SHA512.multipart.dave.dsig.SHA512.multipart.eml", + enc: false, + sig: true, + sig_good: false, + dave: 1, + extra: 1, + }, +]; + +let gCopyWaiter = PromiseUtils.defer(); + +add_task(async function copy_messages() { + for (let msg of gMessages) { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + + MailServices.copy.copyFileMessage( + do_get_file(smimeDataDirectory + msg.filename), + gInbox, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + + await promiseCopyListener.promise; + promiseCopyListener = null; + } + gCopyWaiter.resolve(); +}); + +add_task(async function check_smime_message() { + await gCopyWaiter.promise; + + let hdrIndex = 0; + + for (let msg of gMessages) { + console.log("checking " + msg.filename); + + let numExpected = 1; + if (msg.enc && msg.sig) { + numExpected++; + } + + let eventsExpected = numExpected; + if ("extra" in msg) { + eventsExpected += msg.extra; + } + + let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex); + let uri = hdr.folder.getUriForMsg(hdr); + let sinkPromise = smimeHeaderSink.expectResults(eventsExpected); + + let conversion = apply_mime_conversion(uri, smimeHeaderSink); + await conversion.promise; + + let contents = conversion._data; + // dump("contents: " + contents + "\n"); + + if (!msg.sig || msg.sig_good || "check_text" in msg) { + let expected = "This is a test message from Alice to Bob."; + Assert.ok(contents.includes(expected)); + } + // Check that we're also using the display output. + Assert.ok(contents.includes("<html>")); + + await sinkPromise; + + let r = smimeHeaderSink._results; + Assert.equal(r.length, numExpected); + + let sigIndex = 0; + + if (msg.enc) { + Assert.equal(r[0].type, "encrypted"); + Assert.equal(r[0].status, 0); + Assert.equal(r[0].certificate, null); + sigIndex = 1; + } + if (msg.sig) { + Assert.equal(r[sigIndex].type, "signed"); + let cert = r[sigIndex].certificate; + if (msg.sig_good) { + Assert.notEqual(cert, null); + } + if (cert) { + if ("dave" in msg) { + Assert.equal(cert.emailAddress, "dave@example.com"); + } else { + Assert.equal(cert.emailAddress, "alice@example.com"); + } + } + if (msg.sig_good) { + Assert.equal(r[sigIndex].status, 0); + } else { + Assert.notEqual(r[sigIndex].status, 0); + } + } + + hdrIndex++; + } +}); diff --git a/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js b/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js new file mode 100644 index 0000000000..4ae1374f1a --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_smime_perm_decrypt.js @@ -0,0 +1,274 @@ +/* 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 to ensure signed and/or encrypted S/MIME messages are + * processed correctly, and the signature status is treated as good + * or bad as expected. + */ + +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +var { SmimeUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/smimeUtils.jsm" +); +const { EnigmailPersistentCrypto } = ChromeUtils.import( + "chrome://openpgp/content/modules/persistentCrypto.jsm" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +let gCertValidityResult = 0; + +/** + * @implements nsICertVerificationCallback + */ +class CertVerificationResultCallback { + constructor(callback) { + this.callback = callback; + } + verifyCertFinished(prErrorCode, verifiedChain, hasEVPolicy) { + gCertValidityResult = prErrorCode; + this.callback(); + } +} + +function testCertValidity(cert, date) { + let prom = new Promise((resolve, reject) => { + const certificateUsageEmailRecipient = 0x0020; + let result = new CertVerificationResultCallback(resolve); + let flags = Ci.nsIX509CertDB.FLAG_LOCAL_ONLY; + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + certdb.asyncVerifyCertAtTime( + cert, + certificateUsageEmailRecipient, + flags, + "Alice@example.com", + date, + result + ); + }); + return prom; +} + +add_setup(async function () { + let messageInjection = new MessageInjection({ mode: "local" }); + gInbox = messageInjection.getInboxFolder(); + SmimeUtils.ensureNSS(); + + SmimeUtils.loadPEMCertificate( + do_get_file(smimeDataDirectory + "TestCA.pem"), + Ci.nsIX509Cert.CA_CERT + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Alice.p12"), + "nss" + ); + SmimeUtils.loadCertificateAndKey( + do_get_file(smimeDataDirectory + "Bob.p12"), + "nss" + ); +}); + +var gInbox; + +var smimeDataDirectory = "../../../data/smime/"; + +let smimeHeaderSink = { + expectResults(maxLen) { + // dump("Restarting for next test\n"); + this._deferred = PromiseUtils.defer(); + this._expectedEvents = maxLen; + this.countReceived = 0; + this._results = []; + this.haveSignedBad = false; + this.haveEncryptionBad = false; + this.resultSig = null; + this.resultEnc = null; + this.resultSigFirst = undefined; + return this._deferred.promise; + }, + signedStatus(aNestingLevel, aSignedStatus, aSignerCert) { + console.log("signedStatus " + aSignedStatus + " level " + aNestingLevel); + // dump("Signed message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveSignedBad) { + // override with newer allowed + this.resultSig = { + type: "signed", + status: aSignedStatus, + certificate: aSignerCert, + }; + if (aSignedStatus != 0) { + this.haveSignedBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = true; + } + } + this.countReceived++; + this.checkFinished(); + }, + encryptionStatus(aNestingLevel, aEncryptedStatus, aRecipientCert) { + console.log( + "encryptionStatus " + aEncryptedStatus + " level " + aNestingLevel + ); + // dump("Encrypted message\n"); + Assert.equal(aNestingLevel, 1); + if (!this.haveEncryptionBad) { + // override with newer allowed + this.resultEnc = { + type: "encrypted", + status: aEncryptedStatus, + certificate: aRecipientCert, + }; + if (aEncryptedStatus != 0) { + this.haveEncryptionBad = true; + } + if (this.resultSigFirst == undefined) { + this.resultSigFirst = false; + } + } + this.countReceived++; + this.checkFinished(); + }, + checkFinished() { + if (this.countReceived == this._expectedEvents) { + if (this.resultSigFirst) { + if (this.resultSig) { + this._results.push(this.resultSig); + } + if (this.resultEnc) { + this._results.push(this.resultEnc); + } + } else { + if (this.resultEnc) { + this._results.push(this.resultEnc); + } + if (this.resultSig) { + this._results.push(this.resultSig); + } + } + this._deferred.resolve(this._results); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]), +}; + +/** + * Note on FILENAMES taken from the NSS test suite: + * - env: CMS enveloped (encrypted) + * - dsig: CMS detached signature (with multipart MIME) + * - sig: CMS opaque signature (content embedded inside signature) + * - bad: message text does not match signature + * - mismatch: embedded content is different + * + * Control variables used for checking results: + * - env: If true, we expect a report to encryptionStatus() that message + * is encrypted. + */ + +var gMessages = [{ filename: "alice.env.eml", enc: true }]; + +var gDecFolder; + +add_task(async function copy_messages() { + for (let msg of gMessages) { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + + MailServices.copy.copyFileMessage( + do_get_file(smimeDataDirectory + msg.filename), + gInbox, + null, + true, + 0, + "", + promiseCopyListener, + null + ); + + await promiseCopyListener.promise; + promiseCopyListener = null; + } + gInbox.server.rootFolder.createSubfolder("decrypted", null); + gDecFolder = gInbox.server.rootFolder.getChildNamed("decrypted"); +}); + +add_task(async function check_smime_message() { + let hdrIndex = 0; + + for (let msg of gMessages) { + console.log("checking " + msg.filename); + + let numExpected = 1; + + let eventsExpected = numExpected; + + let hdr = mailTestUtils.getMsgHdrN(gInbox, hdrIndex); + let uri = hdr.folder.getUriForMsg(hdr); + let sinkPromise = smimeHeaderSink.expectResults(eventsExpected); + + let conversion = apply_mime_conversion(uri, smimeHeaderSink); + await conversion.promise; + + let contents = conversion._data; + // dump("contents: " + contents + "\n"); + + // Check that we're also using the display output. + Assert.ok(contents.includes("<html>")); + + await sinkPromise; + + let r = smimeHeaderSink._results; + Assert.equal(r.length, numExpected); + + if (msg.enc) { + Assert.equal(r[0].type, "encrypted"); + Assert.equal(r[0].status, 0); + Assert.equal(r[0].certificate, null); + } + + await EnigmailPersistentCrypto.cryptMessage( + hdr, + gDecFolder.URI, + false, + null + ); + + eventsExpected = 0; + + hdr = mailTestUtils.getMsgHdrN(gDecFolder, hdrIndex); + uri = hdr.folder.getUriForMsg(hdr); + sinkPromise = smimeHeaderSink.expectResults(eventsExpected); + + conversion = apply_mime_conversion(uri, smimeHeaderSink); + await conversion.promise; + + contents = conversion._data; + // dump("contents: " + contents + "\n"); + + // Check that we're also using the display output. + Assert.ok(contents.includes("<html>")); + + // A message without S/MIME content didn't produce any events, + // so we must manually force this check. + smimeHeaderSink.checkFinished(); + await sinkPromise; + + // If the result length is 0, it wasn't decrypted. + Assert.equal(smimeHeaderSink._results.length, 0); + + hdrIndex++; + } +}); diff --git a/comm/mailnews/mime/test/unit/test_structured_headers.js b/comm/mailnews/mime/test/unit/test_structured_headers.js new file mode 100644 index 0000000000..aedc70ac59 --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_structured_headers.js @@ -0,0 +1,249 @@ +/* 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 tests the msgIStructuredHeaders and msgIWritableStructuredHeaders +// interfaces. + +// Verify that a specific XPCOM error code is thrown. +function verifyError(block, errorCode) { + let caught = undefined; + try { + block(); + } catch (actual) { + caught = actual.result; + } + Assert.equal(caught, errorCode); +} + +var StructuredHeaders = CC( + "@mozilla.org/messenger/structuredheaders;1", + Ci.msgIWritableStructuredHeaders +); + +add_task(async function check_addressing() { + let headers = new StructuredHeaders(); + headers.setHeader("To", [{ name: "undisclosed-recipients", group: [] }]); + Assert.ok(Array.isArray(headers.getHeader("To"))); + let flat = headers.getAddressingHeader("To", false); + Assert.ok(Array.isArray(flat)); + Assert.equal(flat.length, 0); + let full = headers.getAddressingHeader("To", true); + Assert.ok(Array.isArray(full)); + Assert.equal(full.length, 1); + Assert.equal(full[0].name, "undisclosed-recipients"); + Assert.ok(Array.isArray(full[0].group)); + Assert.equal(headers.getRawHeader("To"), "undisclosed-recipients: ;"); + + headers.setHeader("To", [{ name: "\u00D3", email: "test@foo.invalid" }]); + Assert.equal( + headers.getRawHeader("To"), + "=?UTF-8?B?w5M=?= <test@foo.invalid>" + ); + headers.setAddressingHeader("To", [ + { name: "Comma, Name", email: "test@foo.invalid" }, + ]); + Assert.equal(headers.getRawHeader("To"), '"Comma, Name" <test@foo.invalid>'); +}); + +add_task(async function check_custom_header() { + // Load an extension for our custom header. + let url = Services.io.newFileURI(do_get_file("custom_header.js")).spec; + let promise = new Promise((resolve, reject) => { + function observer(subject, topic, data) { + Assert.equal(topic, "xpcom-category-entry-added"); + Assert.equal(data, "custom-mime-encoder"); + resolve(); + Services.obs.removeObserver(observer, "xpcom-category-entry-added"); + } + Services.obs.addObserver(observer, "xpcom-category-entry-added"); + }); + Services.catMan.addCategoryEntry( + "custom-mime-encoder", + "X-Unusual", + url, + false, + true + ); + // The category manager doesn't fire until a later timestep. + await promise; + let headers = new StructuredHeaders(); + headers.setRawHeader("X-Unusual", "10"); + Assert.equal(headers.getHeader("X-Unusual"), 16); + headers.setHeader("X-Unusual", 32); + Assert.equal(headers.getRawHeader("X-Unusual"), "20"); +}); + +add_task(async function check_raw() { + let headers = new StructuredHeaders(); + Assert.ok(!headers.hasHeader("Date")); + let day = new Date("2000-01-01T00:00:00Z"); + headers.setHeader("Date", day); + Assert.ok(headers.hasHeader("Date")); + Assert.ok(headers.hasHeader("date")); + Assert.equal(headers.getHeader("Date"), day); + Assert.equal(headers.getHeader("date"), day); + verifyError( + () => headers.getUnstructuredHeader("Date"), + Cr.NS_ERROR_ILLEGAL_VALUE + ); + verifyError( + () => headers.getAddressingHeader("Date"), + Cr.NS_ERROR_ILLEGAL_VALUE + ); + // This is easier than trying to match the actual value for the Date header, + // since that depends on the current timezone. + Assert.equal(new Date(headers.getRawHeader("Date")).getTime(), day.getTime()); + + // Otherwise, the string values should work. + headers.setRawHeader("Custom-Date", "1 Jan 2000 00:00:00 +0000"); + Assert.equal( + headers.getRawHeader("Custom-Date"), + "1 Jan 2000 00:00:00 +0000" + ); + headers.deleteHeader("Custom-Date"); + + headers.setUnstructuredHeader("Content-Description", "A description!"); + Assert.equal(headers.getHeader("Content-Description"), "A description!"); + Assert.equal( + headers.getUnstructuredHeader("Content-Description"), + "A description!" + ); + verifyError( + () => headers.getAddressingHeader("Content-Description"), + Cr.NS_ERROR_ILLEGAL_VALUE + ); + Assert.equal(headers.getRawHeader("Content-Description"), "A description!"); + + Assert.ok(!headers.hasHeader("Subject")); + Assert.ok(headers.getUnstructuredHeader("Subject") === null); + headers.setRawHeader("Subject", "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?="); + Assert.equal( + headers.getHeader("Subject"), + "\u79c1\u306f\u4ef6\u540d\u5348\u524d" + ); + Assert.equal( + headers.getRawHeader("Subject"), + "=?UTF-8?B?56eB44Gv5Lu25ZCN5Y2I5YmN?=" + ); + + // Multiple found headers + Assert.equal(headers.getHeader("Not-Found-Anywhere"), undefined); + Assert.notEqual(headers.getHeader("Not-Found-Anywhere"), ""); + Assert.equal(headers.getRawHeader("Not-Found-Anywhere"), undefined); + headers.setHeader("Not-Found-Anywhere", 515); + Assert.equal(headers.getHeader("Not-Found-Anywhere"), 515); + headers.deleteHeader("not-found-anywhere"); + Assert.equal(headers.getHeader("Not-Found-Anywhere"), undefined); + + // Check the enumeration of header values. + headers.setHeader("unabashed-random-header", false); + let headerList = [ + "Date", + "Content-Description", + "Subject", + "Unabashed-Random-Header", + ]; + for (let value of headers.headerNames) { + Assert.equal(value.toLowerCase(), headerList.shift().toLowerCase()); + } + + // Check that copying works + let moreHeaders = new StructuredHeaders(); + moreHeaders.addAllHeaders(headers); + for (let value of headers.headerNames) { + Assert.equal(moreHeaders.getHeader(value), headers.getHeader(value)); + } + headers.deleteHeader("Date"); + Assert.ok(moreHeaders.hasHeader("Date")); +}); + +add_task(async function check_nsIMimeHeaders() { + let headers = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + Assert.ok(headers instanceof Ci.msgIStructuredHeaders); + Assert.equal(false, headers instanceof Ci.msgIWritableStructuredHeaders); + headers.initialize( + mailTestUtils.loadFileToString(do_get_file("../../../data/draft1")) + ); + Assert.equal(headers.getHeader("To").length, 1); + Assert.equal(headers.getHeader("To")[0].email, "bugmail@example.org"); + Assert.equal(headers.getAddressingHeader("To").length, 1); + Assert.equal(headers.getHeader("Content-Type").type, "text/html"); + + let headerList = [ + "X-Mozilla-Status", + "X-Mozilla-Status2", + "X-Mozilla-Keys", + "FCC", + "BCC", + "X-Identity-Key", + "Message-ID", + "Date", + "From", + "X-Mozilla-Draft-Info", + "User-Agent", + "MIME-Version", + "To", + "Subject", + "Content-Type", + "Content-Transfer-Encoding", + ]; + for (let value of headers.headerNames) { + Assert.equal(value.toLowerCase(), headerList.shift().toLowerCase()); + } +}); + +add_task(async function checkBuildMimeText() { + let headers = new StructuredHeaders(); + headers.setHeader("To", [ + { name: "François Smith", email: "user@☃.invalid" }, + ]); + headers.setHeader("From", [{ name: "John Doe", email: "jdoe@test.invalid" }]); + headers.setHeader( + "Subject", + "A subject that spans a distance quite in " + + "excess of 80 characters so as to force an intermediary CRLF" + ); + headers.setHeader( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101 Thunderbird/40.0a1" + ); + let mimeText = + "To: =?UTF-8?Q?Fran=C3=A7ois_Smith?= <user@☃.invalid>\r\n" + + "From: John Doe <jdoe@test.invalid>\r\n" + + "Subject: A subject that spans a distance quite in excess of 80 characters so\r\n" + + " as to force an intermediary CRLF\r\n" + + "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101\r\n" + + " Thunderbird/40.0a1\r\n"; + Assert.equal(headers.buildMimeText(), mimeText); + + // Check the version used for the nsIMimeHeaders implementation. This requires + // initializing with a UTF-8 version. + let utf8Text = mimeText.replace("☃", "\xe2\x98\x83"); + let mimeHeaders = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance( + Ci.nsIMimeHeaders + ); + mimeHeaders.initialize(utf8Text); + Assert.equal(mimeHeaders.getHeader("To")[0].email, "user@☃.invalid"); + Assert.equal(mimeHeaders.buildMimeText(), mimeText); + Assert.equal(mimeHeaders.allHeaders, utf8Text); + + // Check date header sanitization + headers = new StructuredHeaders(); + headers.setHeader("Date", new Date("Fri, 6 Mar 2020 00:12:34 +0100")); + mimeText = "Date: Thu, 5 Mar 2020 23:12:00 +0000\r\n"; + Assert.equal(headers.buildMimeText(true), mimeText); +}); + +/** + * Test that very long message id can be encoded without error. + */ +add_task(async function test_longMessageId() { + let msgId = + "<loqrvrxAUJXbUjUpqbrOJ8nHnJ49hmTREaUhehHZQv0AELQUM7ym6MUklPkt13aw4UD81bYIwO91pQL2OaeKMYVYD5hvZiRT2lSUmGtJkthgb3p5-y03p9bkxbnixgary7va1z0rv6hmd0yy69dm9exwga43h5k6266uwwchtjuxail7ipjhu6307yuft5bm186nu9vejf2joegwtq309cz9m-o3gwPZsvyB4qDpaAkxaj8iyh4OHc0kJsbQPQG8c5z6l3mmtwJuFHC4PxJnzAx9TyQzfnxhiXetQqFaNfvjNYetmNGMd4oq-sihw-d26z-bmdkvy47cloy2vwrnEYPKxtmjXtsmyFJGNxL7d1CeFIAOloSFAwccA6Onq6zPC9lfwWcAOFFje5XqkGVK2XNsUsFao5PR51WsOZStvoCzkqPuWB5PpJ791D9gzPXvGVa45ahuwgpmr1v8g1h5dalaytuxtpettthl506s7l4odqnkhufkvqkja56ulbd4ukgpbd88o3msjz3qk906pbfq6cahdecxoidplpbtsm-673718934717750999799265953521388769563044829819888815300763892678635939321303281062602679958225188.n050jeqcu1blxrm38i58q9dsws108c2m3xcc1tfmlgx8ya2wjyvzxyikgaaed3q6r@ZGCDPKIGJZGEPNVFFMXMTCMUFOPRMBFLIIPXSXECXKGNXBSDNPPHRBCXQRPCTUOCDDZVBEXYODLMFEQTUGBMHDJYUYus-575687677-2.673718934717750999799265953521388769563044829819888815300763892678635939321303281062602679958225188.invalid>"; + let headers = new StructuredHeaders(); + headers.setHeader("Message-ID", msgId); + Assert.equal(headers.getRawHeader("message-id"), msgId); +}); diff --git a/comm/mailnews/mime/test/unit/test_text_attachment.js b/comm/mailnews/mime/test/unit/test_text_attachment.js new file mode 100644 index 0000000000..bea9d5c31a --- /dev/null +++ b/comm/mailnews/mime/test/unit/test_text_attachment.js @@ -0,0 +1,89 @@ +/* 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 test verifies that we don't display text attachments inline + * when mail.inline_attachments is false. + */ + +var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +const TEXT_ATTACHMENT = "inline text attachment"; + +var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); +var msgGen = new MessageGenerator(); +var inbox; +var messageInjection = new MessageInjection({ mode: "local" }); +var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +add_setup(function () { + inbox = messageInjection.getInboxFolder(); +}); + +add_task(async function test_message_attachments_no_inline() { + Services.prefs.setBoolPref("mail.inline_attachments", false); + Services.prefs.setBoolPref("mail.inline_attachments.text", true); + await test_message_attachments({ + // text attachment + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "test.txt", + format: "", + }, + ], + }); +}); + +add_task(async function test_message_attachments_no_inline_text() { + Services.prefs.setBoolPref("mail.inline_attachments", true); + Services.prefs.setBoolPref("mail.inline_attachments.text", false); + await PromiseTestUtils.promiseDelay(100); + await test_message_attachments({ + // text attachment + attachments: [ + { + body: TEXT_ATTACHMENT, + filename: "test.txt", + format: "", + }, + ], + }); +}); + +async function test_message_attachments(info) { + let synMsg = msgGen.makeMessage(info); + let synSet = new SyntheticMessageSet([synMsg]); + await messageInjection.addSetsToFolders([inbox], [synSet]); + + let msgURI = synSet.getMsgURI(0); + let msgService = MailServices.messageServiceFromURI(msgURI); + + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + + msgService.streamMessage( + msgURI, + streamListener, + msgWindow, + null, + true, // have them create the converter + // additional uri payload, note that "header=" is prepended automatically + "filter", + false + ); + + let data = await streamListener.promise; + // check that text attachment contents didn't end up inline. + Assert.ok(!data.includes(TEXT_ATTACHMENT)); +} diff --git a/comm/mailnews/mime/test/unit/xpcshell.ini b/comm/mailnews/mime/test/unit/xpcshell.ini new file mode 100644 index 0000000000..4e1e665f91 --- /dev/null +++ b/comm/mailnews/mime/test/unit/xpcshell.ini @@ -0,0 +1,35 @@ +[DEFAULT] +head = head_mime.js +tail = +support-files = + custom_header.js + ../../../../mail/test/browser/openpgp/data/keys/* + ../../../../mail/test/browser/openpgp/data/eml/* + +[test_EncodeMimePartIIStr_UTF8.js] +[test_alternate_p7m_handling.js] +[test_attachment_size.js] +[test_badContentType.js] +[test_bug493544.js] +[test_handlerRegistration.js] +[test_hidden_attachments.js] +[test_jsmime_charset.js] +[test_message_attachment.js] +[test_mimeContentType.js] +[test_mimeStreaming.js] +[test_nsIMsgHeaderParser1.js] +[test_nsIMsgHeaderParser2.js] +[test_nsIMsgHeaderParser3.js] +[test_nsIMsgHeaderParser4.js] +[test_nsIMsgHeaderParser5.js] +[test_openpgp_decrypt.js] +[test_parser.js] +[test_rfc822_body.js] +[test_smime_decrypt.js] + +[test_smime_decrypt_allow_sha1.js] +run-sequentially = Because it set's a pref that must not be active for other tests. + +[test_smime_perm_decrypt.js] +[test_structured_headers.js] +[test_text_attachment.js] |