// Test-only mail capture. Never imported by the application. import { createServer as createSMTPServer } from "node:net"; import { createServer as createHTTPServer } from "node:http"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; const maxMailBytes = 128 * 1024; export async function startLocalSMTPFixture({ smtpPort = 0, controlPort = 0 } = {}) { const messages = []; const sockets = new Set(); const smtp = createSMTPServer((socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); socket.on("error", () => socket.destroy()); socket.setEncoding("latin1"); socket.write("220 localhost test capture ESMTP\r\n"); let buffer = ""; let sender = ""; let recipients = []; let data = null; let dataBytes = 0; const reset = () => { sender = ""; recipients = []; data = null; dataBytes = 0; }; socket.on("data", (chunk) => { buffer += chunk; if (buffer.length + dataBytes > maxMailBytes) { reset(); socket.end("552 Fixture message size exceeded\r\n"); return; } while (buffer.includes("\n")) { const end = buffer.indexOf("\n"); const line = buffer.slice(0, end).replace(/\r$/, ""); buffer = buffer.slice(end + 1); if (data !== null) { if (line === ".") { try { if (messages.length >= 100) throw new Error("Fixture mail capacity exceeded."); messages.push({ sequence: messages.length + 1, recipients: [...recipients], body: decodePlainMessage(data.join("\r\n") + "\r\n") }); socket.write("250 Captured locally\r\n"); } catch { socket.write("554 Unsupported fixture mail\r\n"); } reset(); } else { dataBytes += line.length + 2; data.push(line.startsWith("..") ? line.slice(1) : line); } continue; } if (/^(EHLO|HELO)(\s|$)/i.test(line)) socket.write(`250-localhost\r\n250 SIZE ${maxMailBytes}\r\n`); else if (/^MAIL FROM:/i.test(line)) { reset(); sender = /^MAIL FROM:\s*<([^>]+)>/i.exec(line)?.[1] ?? ""; socket.write(sender ? "250 Sender accepted\r\n" : "501 Invalid sender\r\n"); } else if (/^RCPT TO:/i.test(line)) { const recipient = /^RCPT TO:\s*<([^>]+)>/i.exec(line)?.[1]?.toLowerCase() ?? ""; if (!sender) socket.write("503 Sender required\r\n"); else if (!/^[^@\s]+@[^@\s]+\.test$/.test(recipient)) socket.write("550 Only synthetic .test recipients are accepted\r\n"); else { recipients.push(recipient); socket.write("250 Recipient accepted\r\n"); } } else if (/^DATA$/i.test(line)) { if (!sender || !recipients.length) socket.write("503 Envelope required\r\n"); else { data = []; dataBytes = 0; socket.write("354 End with dot on its own line\r\n"); } } else if (/^RSET$/i.test(line)) { reset(); socket.write("250 Reset\r\n"); } else if (/^NOOP$/i.test(line)) socket.write("250 OK\r\n"); else if (/^QUIT$/i.test(line)) { socket.end("221 Closing\r\n"); break; } else socket.write("502 Command unavailable in local fixture\r\n"); } }); }); const control = createHTTPServer((request, response) => { response.setHeader("Cache-Control", "no-store"); response.setHeader("Content-Type", "application/json; charset=utf-8"); if (request.method !== "GET" || request.headers.origin) { response.writeHead(403).end("{}"); return; } const url = new URL(request.url ?? "/", "http://127.0.0.1"); if (url.pathname === "/health") { response.end('{"status":"local-fixture"}'); return; } const recipient = (url.searchParams.get("recipient") ?? "").toLowerCase(); if (url.pathname !== "/messages" || !/^[^@\s]+@[^@\s]+\.test$/.test(recipient)) { response.writeHead(400).end("{}"); return; } response.end(JSON.stringify(messages.filter((message) => message.recipients.includes(recipient)).map(({ sequence, body }) => ({ sequence, body })))); }); const listen = (server, port) => new Promise((accept, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", () => { server.off("error", reject); accept(); }); }); await listen(smtp, smtpPort); try { await listen(control, controlPort); } catch (error) { smtp.close(); throw error; } return { smtpPort: smtp.address().port, controlPort: control.address().port, close: async () => { sockets.forEach((socket) => socket.destroy()); control.closeAllConnections(); await Promise.all([smtp, control].map((server) => new Promise((accept) => server.close(accept)))); }, }; } export function decodePlainMessage(raw) { const separator = /\r?\n\r?\n/.exec(raw); if (!separator) throw new Error("Mail has no MIME header boundary."); const headers = raw.slice(0, separator.index).replace(/\r?\n[ \t]+/g, " "); const contentType = /^content-type:\s*([^\r\n]+)/im.exec(headers)?.[1] ?? "text/plain"; if (!/^text\/plain(?:;|$)/i.test(contentType)) throw new Error("Only text/plain fixture mail is supported."); const charset = /charset=["']?([^;\s"']+)/i.exec(contentType)?.[1] ?? "utf-8"; if (!/^(utf-8|us-ascii)$/i.test(charset)) throw new Error("Unsupported fixture mail charset."); const encoding = /^content-transfer-encoding:\s*([^\r\n]+)/im.exec(headers)?.[1].trim().toLowerCase() ?? "7bit"; let body = raw.slice(separator.index + separator[0].length); if (encoding === "base64") return Buffer.from(body.replace(/\s/g, ""), "base64").toString("utf8"); if (encoding === "quoted-printable") { body = body.replace(/=\r?\n/g, "").replace(/=([\da-f]{2})/gi, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))); } else if (encoding !== "7bit" && encoding !== "8bit") { throw new Error("Unsupported fixture mail transfer encoding."); } return Buffer.from(body, "latin1").toString("utf8"); } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const fixture = await startLocalSMTPFixture({ smtpPort: Number.parseInt(process.env.AUTH_RECOVERY_SMTP_PORT ?? "2527", 10), controlPort: Number.parseInt(process.env.AUTH_RECOVERY_MAIL_PORT ?? "5303", 10), }); process.stdout.write(`Local SMTP fixture ready on 127.0.0.1:${fixture.smtpPort}; control 127.0.0.1:${fixture.controlPort}\n`); for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void fixture.close(); }); }