const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025"; export async function clearMailpit(): Promise { const res = await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" }); if (!res.ok) throw new Error(`Mailpit clear failed: ${res.status}`); } type MessageSummary = { ID: string; To: { Address: string }[]; Created: string }; async function listMessagesTo(email: string): Promise { const url = `${MAILPIT}/api/v1/search?query=${encodeURIComponent(`to:${email}`)}`; const res = await fetch(url); if (!res.ok) throw new Error(`Mailpit search failed: ${res.status}`); const data = (await res.json()) as { messages: MessageSummary[] }; return data.messages ?? []; } async function getMessageBody(id: string): Promise<{ text: string; html: string }> { const res = await fetch(`${MAILPIT}/api/v1/message/${id}`); if (!res.ok) throw new Error(`Mailpit fetch failed: ${res.status}`); const data = (await res.json()) as { Text?: string; HTML?: string }; return { text: data.Text ?? "", html: data.HTML ?? "" }; } export async function waitForMagicLink(email: string, opts: { timeoutMs?: number } = {}): Promise { const timeoutMs = opts.timeoutMs ?? 10_000; const deadline = Date.now() + timeoutMs; const linkRe = /https?:\/\/[^\s<>"]*\/api\/auth\/callback\/nodemailer[^\s<>"]*/i; while (Date.now() < deadline) { const messages = await listMessagesTo(email); if (messages.length > 0) { const newest = messages.sort((a, b) => b.Created.localeCompare(a.Created))[0]; const { text, html } = await getMessageBody(newest.ID); const m = (text.match(linkRe) ?? html.match(linkRe))?.[0]; if (m) return m.replace(/&/g, "&"); } await new Promise((r) => setTimeout(r, 250)); } throw new Error(`No magic link for ${email} within ${timeoutMs}ms`); }