/* 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 { MockRegistrar: gMockRegistrar } = ChromeUtils.importESModule( "resource://testing-common/MockRegistrar.sys.mjs" ); var { FileTestUtils: gFileTestUtils } = ChromeUtils.importESModule( "resource://testing-common/FileTestUtils.sys.mjs" ); const BODY = [ "ordinary tail", "valid =41", "soft=\r\nbreak", "short=\nbreak", "malformed =Qx", "incomplete =Q", ].join("\r\n"); const MESSAGE = [ "From: sender@example.invalid", "To: recipient@example.invalid", "Subject: Quoted-printable draft", "MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "Content-Transfer-Encoding: quoted-printable", "", BODY, ].join("\r\n"); const EXPECTED_BODY = [ "ordinary tail", "valid A", "softbreak", "shortbreak", "malformed =Qx", "incomplete =Q", ].join("\n"); async function makeChannel() { const file = gFileTestUtils.getTempFile("quoted-printable-draft.eml"); await IOUtils.write(file.path, new TextEncoder().encode(MESSAGE)); const uri = Services.io.newFileURI(file); uri.scheme = "mailbox"; uri.query = "number=0"; return Services.io.newChannel( uri.spec, null, null, null, Services.scriptSecurityManager.getSystemPrincipal(), null, Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, Ci.nsIContentPolicy.TYPE_OTHER ); } function makeStream(data) { const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( Ci.nsIStringInputStream ); stream.setByteStringData(data); return stream; } async function convertMessage( chunks, outputType, outputListener = null, configureConverter = null ) { const channel = await makeChannel(); const converter = Cc[ "@mozilla.org/streamconv;1?from=message/rfc822&to=application/xhtml+xml" ].createInstance(Ci.nsIMimeStreamConverter); converter.setMimeOutputType(outputType); configureConverter?.(converter); converter .QueryInterface(Ci.nsIStreamConverter) .asyncConvertData(null, null, outputListener, channel); const listener = converter.QueryInterface(Ci.nsIStreamListener); listener.onStartRequest(channel); let offset = 0; for (const chunk of chunks) { listener.onDataAvailable(channel, makeStream(chunk), offset, chunk.length); offset += chunk.length; } listener.onStopRequest(channel, Cr.NS_OK); } async function renderMessage(chunks) { let output = ""; const outputListener = { QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), onStartRequest() {}, onDataAvailable(request, stream, offset, count) { output += NetUtil.readInputStreamToString(stream, count); }, onStopRequest(request, status) { Assert.equal(status, Cr.NS_OK, "rendering should finish successfully"); }, }; await convertMessage( chunks, Ci.nsMimeOutput.nsMimeMessageBodyQuoting, outputListener ); return output; } function getRenderedBody(rendered) { // The emitter terminates lines with MSG_LINEBREAK, which is CRLF on // Windows. Normalize so we can assert using LF cross-platform. rendered = rendered.replaceAll("\r\n", "\n"); const match = /^
]*>\n([\s\S]*)\n<\/pre>$/.exec(rendered);
Assert.ok(match, "quoting should render the decoded message body");
return match[1];
}
add_task(async function testQuotedPrintableChunking() {
const rendered = await renderMessage([MESSAGE]);
Assert.equal(
getRenderedBody(rendered),
EXPECTED_BODY,
"valid escapes should decode, while invalid or incomplete escapes are emitted literally"
);
const bodyStart = MESSAGE.indexOf(BODY);
const splitOffsets = new Set([bodyStart + 1, MESSAGE.length - 1]);
for (let offset = bodyStart; offset < MESSAGE.length; offset++) {
if (MESSAGE[offset] == "=") {
splitOffsets.add(offset);
splitOffsets.add(offset + 1);
splitOffsets.add(offset + 2);
}
}
for (const offset of splitOffsets) {
const splitRendered = await renderMessage([
MESSAGE.slice(0, offset),
MESSAGE.slice(offset),
]);
Assert.equal(
getRenderedBody(splitRendered),
EXPECTED_BODY,
`decoding should not depend on a stream split at offset ${offset}`
);
}
});
add_task(async function testMalformedDraftCompletes() {
let conversionStopped = false;
const outputListener = {
QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
onStartRequest() {},
onDataAvailable(request, stream, offset, count) {
NetUtil.readInputStreamToString(stream, count);
},
onStopRequest(request, status) {
Assert.equal(status, Cr.NS_OK, "draft conversion should succeed");
conversionStopped = true;
},
};
const composeCid = gMockRegistrar.register(
"@mozilla.org/messengercompose/compose;1",
{
QueryInterface: ChromeUtils.generateQI(["nsIMsgCompose"]),
}
);
try {
await convertMessage(
[MESSAGE],
Ci.nsMimeOutput.nsMimeMessageDraftOrTemplate,
outputListener,
converter => {
// Follow the filter path so draft processing hands off the body without
// trying to open a compose window in xpcshell.
converter.forwardInline = true;
converter.forwardInlineFilter = true;
converter.forwardToAddress = "recipient@example.invalid";
}
);
Assert.ok(
conversionStopped,
"draft conversion should hand malformed body data to completion"
);
} finally {
gMockRegistrar.unregister(composeCid);
}
});