// mail-router.ts: hand each email that arrives in ~/Maildir/new to its val. // Mail to @.exe.xyz (or +anything@) goes to ~/vals/, if // its .env names a VAL_EMAIL file; run.ts runs it in a process of its own, // with that val's .env. Handled mail moves to ~/Maildir/cur, mail whose // handler failed to ~/Maildir/failed, mail for no val to cur (and the log). const HOME = Deno.env.get("HOME")!; const MAIL = `${HOME}/Maildir`; const KIT = new URL(".", import.meta.url).pathname; await Deno.mkdir(`${MAIL}/new`, { recursive: true }); await Deno.mkdir(`${MAIL}/cur`, { recursive: true }); await Deno.mkdir(`${MAIL}/failed`, { recursive: true }); async function deliver(name: string) { const path = `${MAIL}/new/${name}`; let head: string; try { head = (await Deno.readTextFile(path)).slice(0, 64 * 1024); } catch { return; // already moved } const to = head.match(/^Delivered-To:\s*(\S+)/im)?.[1] ?? ""; if (!to && Date.now() - ((await Deno.stat(path)).mtime?.getTime() ?? 0) < 10_000) { setTimeout(drain, 2000); // still being written; look again shortly return; } const val = to.split("@")[0].split("+")[0].toLowerCase(); let env = ""; if (/^[a-z0-9_-]+$/.test(val)) { try { env = await Deno.readTextFile(`${HOME}/vals/${val}/.env`); } catch { /* no such val */ } } let dest = `${MAIL}/cur/${name}:2,S`; if (!/^VAL_EMAIL=/m.test(env)) { console.log(`${name}: no val takes mail for ${to}`); } else { const { code } = await new Deno.Command(Deno.execPath(), { args: ["run", "--allow-all", "--config=deno.json", "--env-file=.env", `${KIT}run.ts`, "email", path], cwd: `${HOME}/vals/${val}`, }).spawn().status; console.log(`${name}: ${to} -> ${val}: exit ${code}`); if (code !== 0) dest = `${MAIL}/failed/${name}`; } await Deno.rename(path, dest); } let running = false, again = false; async function drain() { if (running) return void (again = true); running = true; do { again = false; for await (const e of Deno.readDir(`${MAIL}/new`)) if (e.isFile) await deliver(e.name); } while (again); running = false; } await drain(); for await (const _ of Deno.watchFs(`${MAIL}/new`)) drain();