/* 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/. */ /* * General testing of the JS Mime Emitter to make sure it doesn't choke on any * scenarios. * * We do not test, but should consider testing: * - MimeEncryptedPKCS7, whatever that translates to. * - apple double * - sun attachment */ /* * Do not include GlodaTestHelper because we do not want gloda loaded and it * adds a lot of runtime overhead which makes certain debugging strategies like * using chronicle-recorder impractical. */ var { GlodaDatastore } = ChromeUtils.import( "resource:///modules/gloda/GlodaDatastore.jsm" ); var { MsgHdrToMimeMessage } = ChromeUtils.import( "resource:///modules/gloda/MimeMessage.jsm" ); var { MessageGenerator, SyntheticPartLeaf, SyntheticPartMultiAlternative, SyntheticDegeneratePartEmpty, SyntheticPartMultiSignedSMIME, SyntheticPartMultiMixed, SyntheticPartMultiSignedPGP, SyntheticPartMultiRelated, SyntheticPartMultiDigest, SyntheticPartMultiParallel, SyntheticMessageSet, } = ChromeUtils.import( "resource://testing-common/mailnews/MessageGenerator.jsm" ); var { MessageInjection } = ChromeUtils.import( "resource://testing-common/mailnews/MessageInjection.jsm" ); // While we're at it, we'll also test the correctness of the GlodaAttachment // representation, esp. its "I just need the part information to rebuild the // URLs" claim. var { GlodaFundAttr } = ChromeUtils.import( "resource:///modules/gloda/GlodaFundAttr.jsm" ); const DEATH_TO_NEWLINE_TYPE_THINGS = /[\r\n]+/g; var msgGen = new MessageGenerator(); var messageInjection; var partText = new SyntheticPartLeaf("I am text! Woo!"); var partHtml = new SyntheticPartLeaf( "I am HTML! Woo! ", { contentType: "text/html", } ); var partEnriched = new SyntheticPartLeaf( "I am not a popular format! sad woo :(", { contentType: "text/enriched", } ); var partAlternative = new SyntheticPartMultiAlternative([partText, partHtml]); var partMailingListFooter = new SyntheticPartLeaf("I am an annoying footer!"); // We need to make sure a part that has content-disposition: attachment, even // though it doesn't have any filename, still is treated as an attachment. var tachNoFilename = { body: "I like Bordeaux wine", contentType: "text/plain", disposition: "attachment", }; // This is an external attachment, i.e. a mime part that basically says "go find // the attachment on disk, assuming it still exists, here's the path to the file // on disk". It turns out feed enclosures are presented in the exact same way, // so this covers this case as well. var tachExternal = { body: "You deleted an attachment from this message. The original MIME headers for the attachment were:\n" + "Content-Type: image/png;\n" + ' name="conversations-bug1.png"\n' + "Content-Transfer-Encoding: base64\n" + "Content-Disposition: attachment;\n" + ' filename="conversations-bug1.png"', contentType: "image/png", filename: "conversations-bug1.png", charset: null, format: null, encoding: "base64", extraHeaders: { "X-Mozilla-External-Attachment-URL": "file:///tmp/conversations-bug1.png", "X-Mozilla-Altered": 'AttachmentDetached; date="Wed Aug 03 11:11:33 2011"', }, }; var tachText = { filename: "bob.txt", body: "I like cheese!" }; var partTachText = new SyntheticPartLeaf(tachText.body, tachText); var tachInlineText = { filename: "foo.txt", body: "Rock the mic", format: null, charset: null, disposition: "inline", }; new SyntheticPartLeaf(tachInlineText.body, tachInlineText); var tachImage = { filename: "bob.png", contentType: "image/png", encoding: "base64", charset: null, format: null, body: "YWJj\n", }; var partTachImage = new SyntheticPartLeaf(tachImage.body, tachImage); var relImage = { contentType: "image/png", encoding: "base64", charset: null, format: null, contentId: "part1.foo@bar.invalid", body: "YWJj\n", }; var partRelImage = new SyntheticPartLeaf(relImage.body, relImage); var tachVCard = { filename: "bob.vcf", contentType: "text/vcard", encoding: "7bit", body: "begin:vcard\nfn:Bob\nend:vcard\n", }; var partTachVCard = new SyntheticPartLeaf(tachVCard.body, tachVCard); var tachApplication = { filename: "funky.funk", contentType: "application/x-funky", body: "funk!", }; var partTachApplication = new SyntheticPartLeaf( tachApplication.body, tachApplication ); var partTachMessages = [msgGen.makeMessage(), msgGen.makeMessage()]; var partEmpty = new SyntheticDegeneratePartEmpty(); var messageInfos = [ // -- Simple { name: "text/plain", bodyPart: partText, }, { name: "text/html", bodyPart: partHtml, }, // -- Simply ugly { name: "text/enriched", bodyPart: partEnriched, }, // -- Simple w/attachment { name: "text/plain w/text attachment (=> multipart/mixed)", bodyPart: partText, attachments: [tachText], }, { name: "text/plain w/image attachment (=> multipart/mixed)", bodyPart: partText, attachments: [tachImage], }, { name: "text/plain w/vcard attachment (=> multipart/mixed)", bodyPart: partText, attachments: [tachVCard], }, { name: "text/plain w/app attachment (=> multipart/mixed)", bodyPart: partText, attachments: [tachApplication], }, { name: "text/html w/text attachment (=> multipart/mixed)", bodyPart: partHtml, attachments: [tachText], }, { name: "text/html w/image attachment (=> multipart/mixed)", bodyPart: partHtml, attachments: [tachImage], }, { name: "text/html w/vcard attachment (=> multipart/mixed)", bodyPart: partHtml, attachments: [tachVCard], }, { name: "text/html w/app attachment (=> multipart/mixed)", bodyPart: partHtml, attachments: [tachApplication], }, // -- Alternatives { name: "multipart/alternative: text/plain, text/html", bodyPart: partAlternative, }, { name: "multipart/alternative plain/html w/text attachment", bodyPart: partAlternative, attachments: [tachText], }, { name: "multipart/alternative plain/html w/image attachment", bodyPart: partAlternative, attachments: [tachImage], }, { name: "multipart/alternative plain/html w/vcard attachment", bodyPart: partAlternative, attachments: [tachVCard], }, { name: "multipart/alternative plain/html w/app attachment", bodyPart: partAlternative, attachments: [tachApplication], }, // -- S/MIME. { name: "S/MIME alternative", bodyPart: new SyntheticPartMultiSignedSMIME(partAlternative), }, { name: "S/MIME alternative with text attachment inside", // We have to do the attachment packing ourselves on this one. bodyPart: new SyntheticPartMultiSignedSMIME( new SyntheticPartMultiMixed([partAlternative, partTachText]) ), }, { name: "S/MIME alternative with image attachment inside", // We have to do the attachment packing ourselves on this one. bodyPart: new SyntheticPartMultiSignedSMIME( new SyntheticPartMultiMixed([partAlternative, partTachImage]) ), }, { name: "S/MIME alternative with image attachment inside", // We have to do the attachment packing ourselves on this one. bodyPart: new SyntheticPartMultiSignedSMIME( new SyntheticPartMultiMixed([partAlternative, partTachVCard]) ), }, { name: "S/MIME alternative with app attachment inside", // We have to do the attachment packing ourselves on this one. bodyPart: new SyntheticPartMultiSignedSMIME( new SyntheticPartMultiMixed([partAlternative, partTachApplication]) ), }, { name: "S/MIME alternative wrapped in mailing list", bodyPart: new SyntheticPartMultiMixed([ new SyntheticPartMultiSignedSMIME(partAlternative), partMailingListFooter, ]), }, // -- PGP signature // We mainly care that all the content-type parameters show up. { name: "PGP signed alternative", bodyPart: new SyntheticPartMultiSignedPGP(partAlternative), }, // -- Attached RFC822 { // Not your average attachment, pack ourselves for now. name: "attached rfc822", bodyPart: new SyntheticPartMultiMixed([ partAlternative, partTachMessages[0], ]), }, // -- Multipart/related { name: "multipart/related", bodyPart: new SyntheticPartMultiRelated([partHtml, partRelImage]), }, { name: "multipart/related inside multipart/alternative", bodyPart: new SyntheticPartMultiAlternative([ partText, new SyntheticPartMultiRelated([partHtml, partRelImage]), ]), }, // -- Multipart/digest { name: "multipart/digest", bodyPart: new SyntheticPartMultiDigest(partTachMessages.concat()), }, // -- Multipart/parallel (allegedly the same as mixed) { name: "multipart/parallel", bodyPart: new SyntheticPartMultiParallel([partText, partTachImage]), }, // --- Previous bugs // -- Bug 495057, text/enriched was being dumb { name: "text/enriched inside related", bodyPart: new SyntheticPartMultiRelated([partEnriched]), }, // -- Empty sections // This was a crasher because the empty part made us try and close the // child preceding the empty part a second time. The nested multipart led // to the crash providing evidence of the double-close bug but there was // nothing inherently nested-multipart-requiring to trigger the double-close // bug. { name: "nested multipart with empty multipart section", bodyPart: new SyntheticPartMultiMixed([ new SyntheticPartMultiRelated([partAlternative, partTachText]), partEmpty, ]), }, { name: "empty multipart section produces no child", bodyPart: new SyntheticPartMultiMixed([partText, partEmpty, partTachText]), }, ]; add_setup(async function () { messageInjection = new MessageInjection({ mode: "local" }, msgGen); registerCleanupFunction(function () { GlodaDatastore.shutdown(); }); }); add_task(async function test_stream_message() { for (let messageInfo of messageInfos) { await stream_message(messageInfo); } }); /** * Stream */ add_task(async function test_sane_bodies() { // 60 bytes long... (becomes 59 on the other side when \r is dropped) let hugeString = "don't know what you want but I can't stream it anymore...\r\n"; const powahsOfTwo = 10; for (let i = 0; i < powahsOfTwo; i++) { hugeString = hugeString + hugeString; } // This will come out to be 60k, of course. Assert.equal(hugeString.length, 60 * Math.pow(2, powahsOfTwo)); let synMsg = msgGen.makeMessage({ body: { body: hugeString, contentType: "text/plain" }, }); let synSet = new SyntheticMessageSet([synMsg]); await messageInjection.addSetsToFolders( [messageInjection.getInboxFolder()], [synSet] ); let msgHdr = synSet.getMsgHdr(0); let promiseResolve; let promise = new Promise(resolve => { promiseResolve = resolve; }); MsgHdrToMimeMessage( msgHdr, null, function (aMsgHdr, aMimeMsg) { let bodyPart = aMimeMsg.parts[0]; // (the \r gets gone, so it's only 59 per line) if (bodyPart.body.length > 20 * 1024 + 59) { do_throw( "Mime body length is " + bodyPart.body.length + " bytes long but should not be!" ); } promiseResolve(); }, false, { saneBodySize: true } ); await promise; }); // Additional testing for the correctness of allAttachments and // allUserAttachments representation var partTachNestedMessages = [ // Looks like the synthetic part generator appends the charset=ISO-8859-1 part // all by itself. That allows us to create a non-UTF-8 subject, and ensure the // resulting attachment name is indeed São Paulo.eml. msgGen.makeMessage({ subject: "S" + String.fromCharCode(0xe3) + "o Paulo", bodyPart: new SyntheticPartLeaf( "I am HTML! Woo! ", { contentType: "text/html", } ), }), msgGen.makeMessage({ attachments: [tachImage], }), msgGen.makeMessage({ attachments: [tachImage, tachApplication], }), ]; var attMessagesParams = [ { attachments: [tachNoFilename], }, { attachments: [tachExternal], }, { name: "attached rfc822", bodyPart: new SyntheticPartMultiMixed([ partAlternative, partTachNestedMessages[0], ]), }, { name: "attached rfc822 w. image inside", bodyPart: new SyntheticPartMultiMixed([ partAlternative, partTachNestedMessages[1], ]), }, { name: "attached x/funky + attached rfc822 w. (image + x/funky) inside", bodyPart: new SyntheticPartMultiMixed([ partAlternative, partTachApplication, partTachNestedMessages[2], ]), }, ]; var expectedAttachmentsInfo = [ { allAttachmentsContentTypes: ["text/plain"], allUserAttachmentsContentTypes: ["text/plain"], }, { allAttachmentsContentTypes: ["image/png"], allUserAttachmentsContentTypes: ["image/png"], }, { allAttachmentsContentTypes: [], allUserAttachmentsContentTypes: ["message/rfc822"], firstAttachmentName: "S\u00e3o Paulo.eml", }, { allAttachmentsContentTypes: ["image/png"], allUserAttachmentsContentTypes: ["message/rfc822"], }, { allAttachmentsContentTypes: [ "application/x-funky", "image/png", "application/x-funky", ], allUserAttachmentsContentTypes: ["application/x-funky", "message/rfc822"], }, ]; add_task(async function test_attachments_correctness() { for (let [i, params] of attMessagesParams.entries()) { let synMsg = msgGen.makeMessage(params); let synSet = new SyntheticMessageSet([synMsg]); await messageInjection.addSetsToFolders( [messageInjection.getInboxFolder()], [synSet] ); let msgHdr = synSet.getMsgHdr(0); let promiseResolve; let promise = new Promise(resolve => { promiseResolve = resolve; }); MsgHdrToMimeMessage( msgHdr, null, function (aMsgHdr, aMimeMsg) { try { let expected = expectedAttachmentsInfo[i]; if ("firstAttachmentName" in expected) { let att = aMimeMsg.allUserAttachments[0]; Assert.equal(att.name.length, expected.firstAttachmentName.length); for (let j = 0; j < att.name.length; ++j) { Assert.equal( att.name.charCodeAt(j), expected.firstAttachmentName.charCodeAt(j) ); } } Assert.equal( aMimeMsg.allAttachments.length, expected.allAttachmentsContentTypes.length ); for (let [j, att] of aMimeMsg.allAttachments.entries()) { Assert.equal( att.contentType, expected.allAttachmentsContentTypes[j] ); } Assert.equal( aMimeMsg.allUserAttachments.length, expected.allUserAttachmentsContentTypes.length ); for (let [j, att] of aMimeMsg.allUserAttachments.entries()) { Assert.equal( att.contentType, expected.allUserAttachmentsContentTypes[j] ); } // Test for (let att of aMimeMsg.allUserAttachments) { let uri = aMsgHdr.folder.getUriForMsg(aMsgHdr); let glodaAttachment = GlodaFundAttr.glodaAttFromMimeAtt( { folderMessageURI: uri }, att ); // The GlodaAttachment appends the filename, which is not always // present Assert.ok(glodaAttachment.url.startsWith(att.url)); } } catch (e) { dump(aMimeMsg.prettyString() + "\n"); do_throw(e); } promiseResolve(); }, false ); await promise; } }); var bogusMessage = msgGen.makeMessage({ body: { body: "whatever" } }); bogusMessage._contentType = "woooooo"; // Breaking abstraction boundaries. Bad. var weirdMessageInfos = [ // This message has an unnamed part as an attachment (with // Content-Disposition: inline and which is displayable inline). Previously, // libmime would emit notifications for this to be treated as an attachment, // name Part 1.2. Now it's not the case anymore, so we should ensure this // message has no attachments. { name: "test message with part 1.2 attachment", attachments: [ { body: "attachment", filename: "", format: "", }, ], }, ]; add_task(async function test_part12_not_an_attachment() { let synMsg = msgGen.makeMessage(weirdMessageInfos[0]); let synSet = new SyntheticMessageSet([synMsg]); await messageInjection.addSetsToFolders( [messageInjection.getInboxFolder()], [synSet] ); let msgHdr = synSet.getMsgHdr(0); let promiseResolve; let promise = new Promise(resolve => { promiseResolve = resolve; }); MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) { try { Assert.ok(aMimeMsg.allUserAttachments.length == 0); Assert.ok(aMimeMsg.allAttachments.length == 0); } catch (e) { do_throw(e); } promiseResolve(); }); await promise; }); async function stream_message(info) { let synMsg = msgGen.makeMessage(info); let synSet = new SyntheticMessageSet([synMsg]); await messageInjection.addSetsToFolders( [messageInjection.getInboxFolder()], [synSet] ); let msgHdr = synSet.getMsgHdr(0); let promiseResolve; let promise = new Promise(resolve => { promiseResolve = resolve; }); MsgHdrToMimeMessage(msgHdr, null, function (aMsgHdr, aMimeMsg) { verify_stream_message(info, synMsg, aMsgHdr, aMimeMsg); promiseResolve(); }); await promise; } /** * Verify the streamed results are what we wanted. For now, this just means * receiving a representation; we don't check it for correctness. */ function verify_stream_message(aInfo, aSynMsg, aMsgHdr, aMimeMsg) { if (aMimeMsg == null) { do_throw("We really should have gotten a result!"); } try { // aMimeMsg is normalized; it only ever actually gets one child. verify_body_part_equivalence(aSynMsg.bodyPart, aMimeMsg.parts[0]); } catch (ex) { dump("Something was wrong with the MIME rep!\n!!!!!!!!\n"); dump("Synthetic looks like:\n " + aSynMsg.prettyString() + "\n\n"); dump( "MIME looks like: \n" + aMimeMsg.prettyString(true, " ", true) + "\n\n" ); do_throw(ex); } dump("Everything is just fine.\n"); dump("Synthetic looks like:\n " + aSynMsg.prettyString() + "\n\n"); dump( "MIME looks like:\n " + aMimeMsg.prettyString(true, " ", false) + "\n\n" ); } /** * Applies any transformations to the synthetic body part that we would expect * to happen to a message during its libmime journey. It may be better to * just put the expected translations in the synthetic body part instead of * trying to make this method do anything complex. */ function synTransformBody(aSynBodyPart) { let text = aSynBodyPart.body.trim(); // This transforms things into HTML apparently. if (aSynBodyPart._contentType == "text/enriched") { // Our job here is just to transform just enough for our example above. // We also could have provided a manual translation on the body part. text = text.replace(/bold/g, "B").replace(/italic/g, "I"); } return text; } function verify_body_part_equivalence(aSynBodyPart, aMimePart) { // The content-type devoid of parameters should match. Assert.equal(aSynBodyPart._contentType, aMimePart.contentType); // The header representation of the content-type should also match unless // this is an rfc822 part, in which case it should only match for the // actual contents. if (aMimePart.contentType != "message/rfc822") { Assert.equal( aSynBodyPart.contentTypeHeaderValue.replace( DEATH_TO_NEWLINE_TYPE_THINGS, "" ), aMimePart.get("content-type").replace(DEATH_TO_NEWLINE_TYPE_THINGS, "") ); } // XXX body part checking will get brittle if we ever actually encode things! if ( aSynBodyPart.body && !aSynBodyPart._filename && aSynBodyPart._contentType.startsWith("text/") ) { Assert.equal( synTransformBody(aSynBodyPart), aMimePart.body .trim() .replace(/\r/g, "") // Remove stuff added by libmime for HTML parts. .replace( /[\n]*[\n]*/g, "" ) .replace(/[\n]+<\/body>/, "") ); } if (aSynBodyPart.parts) { let iPart; let realPartOffsetCompensator = 0; for (iPart = 0; iPart < aSynBodyPart.parts.length; iPart++) { let subSyn = aSynBodyPart.parts[iPart]; // If this is a degenerate empty, it should not produce output, so // compensate for the offset drift and get on with our lives. if (subSyn instanceof SyntheticDegeneratePartEmpty) { realPartOffsetCompensator--; continue; } let subMime = aMimePart.parts[iPart + realPartOffsetCompensator]; // Our special case is the signature, which libmime does not expose to us. // Ignore! (Also, have our too-many-part checker below not trip on this.) if (subSyn._contentType != "application/x-pkcs7-signature") { if (subMime == null) { do_throw( "No MIME part matching " + subSyn.contentTypeHeaderValue + "\n" ); } verify_body_part_equivalence(subSyn, subMime); } } // Only check if there are still more mime parts; don't check for a count // mismatch (the PKCS case from above needs to be handled). if (iPart < aMimePart.parts.length) { do_throw("MIME part has more sub-parts than syn part?"); } } }