Revision control
Copy as Markdown
Other Tools
/* 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
var { MockRegistrar: gMockRegistrar } = ChromeUtils.importESModule(
);
var { FileTestUtils: gFileTestUtils } = ChromeUtils.importESModule(
);
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 = /^<pre[^>]*>\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);
}
});