Skip to main content

Architecture

Trace how a message moves through the Worker, how routing decisions are made, and how the CLI turns config.json into a running deployment.

Overview

Mailmask has two parts that run on different runtimes:

  • Cloudflare Worker (src/worker/) — Receives inbound mail from Cloudflare Email Routing and forwards, rejects, or drops it.
  • Node.js CLI (src/cli/) — Edits config.json, generates wrangler.toml, and deploys.

The CLI writes the config, and deploy injects it into the Worker as three JSON environment variables: SETTINGS, DESTINATIONS, and ALIASES. There is no database or storage binding.

Mail Flow

  1. Receive — The catch-all hands every address without its own rule to the Worker.
  2. Validate — src/worker/index.ts parses the three variables and checks them with the same Zod schema the CLI uses. Invalid or unparsable config rejects the message instead of silently accepting it.
  3. Resolve — resolveRoute in src/worker/router.ts returns a decision: forward, reject, or drop, with a reason.
  4. Act — handleEmail in src/worker/handlers.ts forwards with alias headers, or refuses and sends a best-effort notification for known aliases.

Routing Decision

resolveRoute is a pure function with no I/O, so every rule is unit-tested directly. It checks, in order:

  1. Domain — A recipient outside the base domain is rejected.
  2. Subaddress — With subaddressing on, a +tag suffix is stripped.
  3. Alias lookup — An unknown address follows the unknown alias action.
  4. Enabled — A disabled alias is refused.
  5. Expiry — An alias whose expires_at has passed is refused.
  6. Block list — A sender matching blocked_senders is refused.
  7. Allow list — When allowed_senders is set, a sender matching none of it is refused.
  8. Destination — A missing destination is rejected as misconfigured; otherwise the message is forwarded.

A refusal uses drop when the unknown alias action is drop, and reject otherwise. Refusals never fall through to the default destination.

Forwarding

The Worker calls message.forward() with the destination address. When add_alias_headers is on, it adds:

  • X-Mailmask-Alias — The original recipient, including any +tag.
  • X-Mailmask-Label — The alias label, when one is set.

The message body is never read, stored, or logged.

Deploy Pipeline

src/cli/commands/deploy.ts drives each step through the Cloudflare v4 API, and src/cli/commands/generate.ts writes wrangler.toml with workers_dev and preview_urls turned off. Deploy never enables Email Routing or edits DNS.

Error Strategy

LayerStrategy
Worker entryInvalid or unparsable config rejects the message with a temporary error.
RouterPure function that always returns a decision and never throws.
NotificationsBest effort. Network failures are swallowed.
CLIErrors are caught per command and set a non-zero exit code.
Config validationZod schemas plus checks for missing destinations, duplicates, and the default destination.

Source Structure

text
packages/mailmask/src/
├── cli/
│   ├── commands/   — Alias, destination, settings, validate, generate, and deploy
│   ├── menu/       — Interactive configuration interface
│   └── index.ts    — Commander entry point (mailmask / mm)
├── lib/            — Constants, regexes, package paths, and Zod schemas
├── worker/
│   ├── handlers.ts — Forward, reject, or drop one message
│   ├── index.ts    — Worker entry point (email and fetch)
│   ├── notify.ts   — Best-effort refusal notifications
│   └── router.ts   — Pure alias resolution and sender matching
├── tests/          — Unit tests mirroring the source
└── types/          — Type declarations mirroring the source