diff --git a/README.md b/README.md index e1e2eb72..ec6ec412 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ https://github.com/cloudflare/agentic-inbox/issues/4#issuecomment-4269118513 ### To set up -1. Deploy to Cloudflare. The deploy flow will automatically provision R2, Durable Objects, and Workers AI. You'll be prompted for **DOMAINS**, which is the domain (yourdomain.com) you want to receive emails for (email@yourdomain.com). +1. Deploy to Cloudflare. The deploy flow will automatically provision R2, Durable Objects, and Workers AI. You'll be prompted for **DOMAINS**, which is the domain (yourdomain.com) you want to receive emails for (email@yourdomain.com). To serve more than one domain from a single instance, pass a comma-separated list (e.g. `yourdomain.com,anotherdomain.com`) — see [Using multiple domains](#using-multiple-domains). [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agentic-inbox) @@ -59,9 +59,27 @@ npm run dev ### Configuration -1. Set your domain in `wrangler.jsonc` +1. Set your domain (or domains) in `wrangler.jsonc` via the `DOMAINS` var 2. Create an R2 bucket named `agentic-inbox`: `wrangler r2 bucket create agentic-inbox` +### Using multiple domains + +A single instance can serve multiple domains. Set `DOMAINS` to a comma-separated list: + +```jsonc +"DOMAINS": "example.com,another.com" +``` + +Then, for **each** domain: + +- Add a catch-all [Email Routing](https://developers.cloudflare.com/email-routing/) rule that forwards to this Worker (for receiving) +- Verify the domain for [Email Service](https://developers.cloudflare.com/email-service/) (for sending) + +Notes: + +- The **New Mailbox** dialog shows a domain picker automatically once more than one domain is configured; mailbox creation is restricted to the configured domains. +- If you set `EMAIL_ADDRESSES` to restrict mailbox creation, it may list addresses across any of the configured domains (e.g. `["hello@example.com", "hi@another.com"]`). + ### Deploy ```bash diff --git a/package.json b/package.json index 9d6f0272..311d4064 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "products": ["Workers", "Durable Objects", "R2", "Workers AI"], "bindings": { "DOMAINS": { - "description": "Your domain with [Email Routing](https://developers.cloudflare.com/email-routing/) enabled (e.g. `example.com`). After deploying, create a catch-all Email Routing rule pointing to this Worker." + "description": "Your domain with [Email Routing](https://developers.cloudflare.com/email-routing/) enabled (e.g. `example.com`). For multiple domains, pass a comma-separated list (e.g. `example.com,another.com`). After deploying, create a catch-all Email Routing rule pointing to this Worker for each domain." } } }, diff --git a/workers/index.ts b/workers/index.ts index fd3359ce..4301fe49 100644 --- a/workers/index.ts +++ b/workers/index.ts @@ -50,6 +50,12 @@ function slugify(text: string) { // can return "" for non-alphanumeric input .replace(/--+/g, "-").replace(/^-+/, "").replace(/-+$/, ""); } +// Parse the comma-separated DOMAINS var into a trimmed, non-empty list. +// Supports multiple domains on one instance, e.g. "example.com,another.com". +function parseDomains(raw: string | undefined): string[] { + return (raw || "").split(",").map((d) => d.trim()).filter(Boolean); +} + function intQuery(c: AppContext, key: string): number | undefined { const v = c.req.query(key); if (!v) return undefined; @@ -86,8 +92,7 @@ app.use("/api/v1/mailboxes/:mailboxId/*", requireMailbox); // -- Config --------------------------------------------------------- app.get("/api/v1/config", (c) => { - const domainsRaw = c.env.DOMAINS || ""; - const domains = domainsRaw.split(",").map((d) => d.trim()).filter(Boolean); + const domains = parseDomains(c.env.DOMAINS); const emailAddresses = c.env.EMAIL_ADDRESSES ?? []; return c.json({ domains, emailAddresses }); }); @@ -103,9 +108,18 @@ app.post("/api/v1/mailboxes", async (c) => { const { name, settings, email: rawEmail } = CreateMailboxBody.parse(await c.req.json()); const email = rawEmail.toLowerCase(); const allowedAddresses = (c.env.EMAIL_ADDRESSES ?? []) as string[]; - if (allowedAddresses.length > 0 && !allowedAddresses.map((a) => a.toLowerCase()).includes(email)) { + const isExplicitlyAllowed = allowedAddresses.map((a) => a.toLowerCase()).includes(email); + if (allowedAddresses.length > 0 && !isExplicitlyAllowed) { return c.json({ error: "Mailbox creation is restricted to configured EMAIL_ADDRESSES" }, 403); } + // When DOMAINS is configured, mailboxes must be on one of those domains — this mirrors the + // front-end domain picker. Explicit EMAIL_ADDRESSES entries bypass the check (they are the + // authoritative allow-list and may legitimately span domains). + const domains = parseDomains(c.env.DOMAINS); + const domain = email.split("@")[1]; + if (!isExplicitlyAllowed && domains.length > 0 && (!domain || !domains.some((d) => d.toLowerCase() === domain))) { + return c.json({ error: "Mailbox domain must be one of the configured DOMAINS" }, 400); + } const key = `mailboxes/${email}.json`; if (await c.env.BUCKET.head(key)) return c.json({ error: "Mailbox already exists" }, 409); const defaultSettings = { fromName: name, forwarding: { enabled: false, email: "" }, signature: { enabled: false, text: "" }, autoReply: { enabled: false, subject: "", message: "" } }; diff --git a/wrangler.jsonc b/wrangler.jsonc index 53a65cd0..98300c5b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -13,7 +13,13 @@ // Production deploys must also define POLICY_AUD and TEAM_DOMAIN. // TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL. // The worker now fails closed outside local development if Access is not configured. + // + // DOMAINS accepts a single domain or a comma-separated list to serve multiple + // domains from one instance, e.g. "example.com,another.com". Each domain needs its + // own Email Routing catch-all rule, and must be verified for outbound sending. "DOMAINS": "example.com", + // EMAIL_ADDRESSES optionally restricts mailbox creation to specific addresses, and + // may span the configured domains, e.g. ["hello@example.com", "hi@another.com"]. "EMAIL_ADDRESSES": [] }, "send_email": [