Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
},
Expand Down
20 changes: 17 additions & 3 deletions workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
});
Expand All @@ -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: "" } };
Expand Down
6 changes: 6 additions & 0 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down