import { afterEach, describe, expect, it } from "vitest"; import nodemailer from "nodemailer"; import { MAIL_TEXT_ENCODING, buildVerifyLinkParts, resolveMailFrom, } from "../../lib/server/mail"; const VERIFY_URL = "https://staging.communityrule.info/api/auth/magic-link/verify?token=5IdE_BHowaw-QJj7Rwue7CbB8wDXvYITvnxRb1FGqxA"; function decodeBase64Parts(raw: string): string { const blocks = [ ...raw.matchAll( /Content-Transfer-Encoding:\s*base64\s*\r?\n\r?\n([A-Za-z0-9+/=\s]+)/gi, ), ]; return blocks .map((match) => Buffer.from(match[1].replace(/\s/g, ""), "base64").toString("utf8"), ) .join("\n"); } const MAIL_FROM_KEYS = ["SMTP_FROM", "CLOUDRON_MAIL_FROM"] as const; const ORIGINAL_FROM = Object.fromEntries( MAIL_FROM_KEYS.map((key) => [key, process.env[key]]), ) as Record<(typeof MAIL_FROM_KEYS)[number], string | undefined>; afterEach(() => { for (const key of MAIL_FROM_KEYS) { const original = ORIGINAL_FROM[key]; if (original === undefined) delete process.env[key]; else process.env[key] = original; } }); describe("buildVerifyLinkParts", () => { it("puts the exact verify URL in text, href, and visible HTML link", () => { const { text, html } = buildVerifyLinkParts( VERIFY_URL, "Open this link to sign in (it expires in 60 minutes):", "If you did not request this, you can ignore this email.", "Sign in", ); expect(text).toContain(`<${VERIFY_URL}>`); expect(html).toContain(`href="${VERIFY_URL}"`); expect(html).toContain(`>${VERIFY_URL}`); expect(html).toContain(">Sign in"); }); it("escapes HTML in the intro and href", () => { const { html } = buildVerifyLinkParts( 'https://example.test/verify?token=a&b="c"', 'View "Rule "', "Ignore if unexpected.", "Open", ); expect(html).toContain("View "Rule <beta>""); expect(html).toContain( 'href="https://example.test/verify?token=a&b="c""', ); }); }); describe("MIME encoding of verify-link mail", () => { it("uses base64 so the raw MIME never contains token=3D", async () => { const { text, html } = buildVerifyLinkParts( VERIFY_URL, "Open this link to sign in (it expires in 60 minutes):", "If you did not request this, you can ignore this email.", "Sign in", ); const transporter = nodemailer.createTransport({ streamTransport: true, buffer: true, newline: "unix", }); const info = await transporter.sendMail({ from: "Community Rule ", to: "member@example.com", subject: "Sign in to Community Rule", text, html, textEncoding: MAIL_TEXT_ENCODING, }); const raw = Buffer.isBuffer(info.message) ? info.message.toString("utf8") : String(info.message); expect(raw).toMatch(/Content-Transfer-Encoding:\s*base64/i); expect(raw).not.toContain("token=3D"); expect(raw).not.toMatch(/quoted-printable/i); const decoded = decodeBase64Parts(raw); expect(decoded).toContain(`href="${VERIFY_URL}"`); expect(decoded).toContain(VERIFY_URL); }); }); describe("resolveMailFrom", () => { it("prefers SMTP_FROM, then CLOUDRON_MAIL_FROM", () => { delete process.env.SMTP_FROM; delete process.env.CLOUDRON_MAIL_FROM; expect(resolveMailFrom()).toBe("noreply@localhost"); process.env.CLOUDRON_MAIL_FROM = "staging.app@communityrule.info"; expect(resolveMailFrom()).toBe("staging.app@communityrule.info"); process.env.SMTP_FROM = "Community Rule "; expect(resolveMailFrom()).toBe( "Community Rule ", ); }); });