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:"), 250); assert.equal(await smtp.command("RCPT TO:"), 550); assert.equal(await smtp.command("RCPT TO:"), 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:"), 250); assert.equal(await smtp.command("RCPT TO:"), 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:"), 250); assert.equal(await smtp.command("RCPT TO:"), 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/); });