97 lines
5.0 KiB
JavaScript
97 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { createConnection } from "node:net";
|
|
import { decodePlainMessage, startLocalSMTPFixture } from "./localSMTPfixture.mjs";
|
|
|
|
async function client(port) {
|
|
const socket = createConnection({ host: "127.0.0.1", port });
|
|
socket.setEncoding("utf8");
|
|
let buffer = "";
|
|
const lines = [];
|
|
const waiters = [];
|
|
socket.on("data", (chunk) => {
|
|
buffer += chunk;
|
|
while (buffer.includes("\n")) {
|
|
const end = buffer.indexOf("\n");
|
|
const line = buffer.slice(0, end).replace(/\r$/, "");
|
|
buffer = buffer.slice(end + 1);
|
|
if (waiters.length) waiters.shift()(line);
|
|
else lines.push(line);
|
|
}
|
|
});
|
|
const readLine = () => lines.length ? Promise.resolve(lines.shift()) : new Promise((resolve) => waiters.push(resolve));
|
|
const reply = async () => {
|
|
let line;
|
|
do { line = await readLine(); } while (/^\d{3}-/.test(line));
|
|
return Number.parseInt(line, 10);
|
|
};
|
|
assert.equal(await reply(), 220);
|
|
return { socket, reply, command: async (line) => { socket.write(line + "\r\n"); return reply(); } };
|
|
}
|
|
|
|
test("loopback SMTP captures fragmented commands and dot-stuffed MIME for the envelope recipient only", { timeout: 5000 }, async (t) => {
|
|
const fixture = await startLocalSMTPFixture();
|
|
t.after(() => fixture.close());
|
|
assert.ok(fixture.smtpPort > 0);
|
|
const smtp = await client(fixture.smtpPort);
|
|
t.after(() => smtp.socket.destroy());
|
|
smtp.socket.write("EH");
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
smtp.socket.write("LO fixture\r\n");
|
|
assert.equal(await smtp.reply(), 250);
|
|
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
|
|
assert.equal(await smtp.command("RCPT TO:<outside@example.com>"), 550);
|
|
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
|
|
assert.equal(await smtp.command("DATA"), 354);
|
|
smtp.socket.write("Content-Type: text/plain; charset=utf-8\r\n\r\n..leading dot\r\nmessage\r\n.\r\n");
|
|
assert.equal(await smtp.reply(), 250);
|
|
assert.equal(await smtp.command("QUIT"), 221);
|
|
const response = await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`);
|
|
assert.equal(response.headers.get("cache-control"), "no-store");
|
|
assert.equal(response.headers.get("access-control-allow-origin"), null);
|
|
assert.deepEqual(await response.json(), [{ sequence: 1, body: ".leading dot\r\nmessage\r\n" }]);
|
|
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=other%40example.test`)).json(), []);
|
|
});
|
|
|
|
test("SMTP reset discards the previous envelope and rejects DATA without a recipient", { timeout: 5000 }, async (t) => {
|
|
const fixture = await startLocalSMTPFixture();
|
|
t.after(() => fixture.close());
|
|
assert.ok(fixture.smtpPort > 0);
|
|
const smtp = await client(fixture.smtpPort);
|
|
t.after(() => smtp.socket.destroy());
|
|
assert.equal(await smtp.command("DATA"), 503);
|
|
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
|
|
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
|
|
assert.equal(await smtp.command("RSET"), 250);
|
|
assert.equal(await smtp.command("DATA"), 503);
|
|
assert.equal(await smtp.command("QUIT"), 221);
|
|
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`)).json(), []);
|
|
});
|
|
|
|
test("SMTP oversized messages are rejected without retaining a partial recovery body", { timeout: 5000 }, async (t) => {
|
|
const fixture = await startLocalSMTPFixture();
|
|
t.after(() => fixture.close());
|
|
assert.ok(fixture.smtpPort > 0);
|
|
const smtp = await client(fixture.smtpPort);
|
|
t.after(() => smtp.socket.destroy());
|
|
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
|
|
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
|
|
assert.equal(await smtp.command("DATA"), 354);
|
|
smtp.socket.write("Content-Type: text/plain\r\n\r\n" + "x".repeat(132000) + "\r\n.\r\n");
|
|
assert.equal(await smtp.reply(), 552);
|
|
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`)).json(), []);
|
|
});
|
|
|
|
test("quoted-printable SMTP mail retains folded reset URLs and UTF-8 copy", () => {
|
|
const raw = "Subject: Verify email\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nBekreft =C3=B8\r\nhttp://localhost:3300/verify-email?userId=3Done&to=\r\nken=3DA%2BB\r\n";
|
|
assert.equal(decodePlainMessage(raw), "Bekreft ø\r\nhttp://localhost:3300/verify-email?userId=one&token=A%2BB\r\n");
|
|
});
|
|
|
|
test("base64 SMTP mail is decoded after header unfolding", () => {
|
|
assert.equal(decodePlainMessage("Content-Type: text/plain;\r\n charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\nSGVsbG8sIHdvcmxkIQ==\r\n"), "Hello, world!");
|
|
});
|
|
|
|
test("unsupported MIME does not silently produce a recovery URL", () => {
|
|
assert.throws(() => decodePlainMessage("Content-Type: multipart/mixed; boundary=x\r\n\r\n--x\r\n"), /text\/plain/);
|
|
});
|