DEV Community

Houzelle
Houzelle

Posted on

Catch-all email aliasing: a different address for every service (Haraka + SRS + DKIM)

Most people use the same email address everywhere. One breach and it leaks for
years. The known fix is a different alias per service — but every tool I tried
made me create each alias upfront. I wanted the opposite: a subdomain where
every address just works, and the alias is born the moment the first email
arrives.

This post is the engineering behind that: how to run a catch-all subdomain
mail flow without becoming an open relay, and how to forward mail without
nuking your deliverability (SPF, SRS, DKIM, DMARC). It's what powers
Tomatoes.run in production, but the ideas apply to
any forwarding setup.

The core idea: decide at RCPT time, not at signup

Instead of a table of pre-created aliases, you give each user a personal
subdomain — you.example.com — and treat every local-part as potentially
valid: amazon@you.example.com, github@you.example.com, anything. The
validity decision happens when the mail is received, not when an alias is
created.

The stack:

  • Haraka (Node.js SMTP server) as the MX.
  • A small internal HTTP API (Next.js route) that owns the forward / reject / tempfail decision.
  • Postgres for users, aliases and metadata only — message content is never stored, it's forwarded immediately.
inbound mail ──> Haraka (MX) ──hook_rcpt──> internal API ──> decision
                                                              │
                        forward ◄─ rewrite envelope (SRS) ────┘
                        + DKIM sign ──> outbound queue ──> user's real inbox
Enter fullscreen mode Exit fullscreen mode

Challenge 1 — a catch-all that isn't an open relay

The scary part of "accept any recipient" is accidentally relaying spam. The
trick is to be optimistic only for addresses you own, and fail closed for
everything else.

In Haraka's hook_rcpt:

exports.hook_rcpt = async function (next, connection, params) {
  const rcpt = params[0];
  const domain = rcpt.host.toLowerCase();

  // Our own catch-all subdomains: accept optimistically, resolve later.
  if (domain.endsWith('.example.com')) return next(OK);

  // Custom domains (bring-your-own): must be verified. Ask the API,
  // with a short-TTL cache to avoid a round-trip per RCPT.
  try {
    const known = await isVerifiedDomain(domain); // API + cache
    return next(known ? OK : DENY);
  } catch (err) {
    // API down? DENYSOFT (4xx) — the sender retries, we lose nothing,
    // and we never relay something we couldn't validate. Fail CLOSED.
    return next(DENYSOFT);
  }
};
Enter fullscreen mode Exit fullscreen mode

Two things matter here:

  • DENYSOFT (a 4xx tempfail), not DENY, when the validating API is unreachable. SMTP is store-and-forward: the sending server retries for days. A few minutes of downtime loses zero mail, and you never blindly accept.
  • The custom-domain gate is what keeps you off "open relay" lists. No verification, no acceptance.

Challenge 2 — forwarding breaks SPF, so rewrite the envelope (SRS)

Naive forwarding looks like this: mail comes in for you, you resend it to the
user's real inbox keeping the original MAIL FROM. The receiving MX checks
SPF on that MAIL FROM domain… and sees your server sending on behalf of
someone else's domain → SPF fail → spam folder or reject.

The fix is SRS (Sender Rewriting Scheme): rewrite the envelope sender to
your own domain, encoded so bounces can be reversed back to the original
sender.

// forward:  bob@gmail.com  ->  SRS0=hash=tt=gmail.com=bob@example.com
const bounce = srs.forward(originalMailFrom, 'example.com');

// on a bounce hitting SRS0=... @example.com, reverse it back:
const original = srs.reverse(bounceRecipient); // -> bob@gmail.com
Enter fullscreen mode Exit fullscreen mode

Now SPF is checked against your domain, which does authorize your server.
The hash makes the token tamper-proof and reversible, so DSNs still reach the
real sender. Keep the SRS secret stable — rotating it invalidates in-flight
bounce addresses.

Challenge 3 — DKIM-sign outbound, per domain

Even with SPF happy, unsigned forwarded mail is suspicious. So the outbound
message is DKIM-signed with your domain (d=example.com).

The interesting case is bring-your-own-domain. When a user adds their own
domain, you generate an RSA keypair, store the private key, and publish the
public key as their DNS TXT record. Outbound mail for that user then gets
signed twice:

  • d=example.com on the SRS return-path (envelope alignment), and
  • d=theircustomdomain.com on the visible From: (author-domain alignment),

so DMARC passes on the domain that actually appears in the headers. Two
signatures, one message — a small additive queue_outbound hook that reuses the
DKIM signing stream.

Challenge 4 — the "no pre-creation" magic

Back at the API, the decision endpoint is where the product logic lives:

// POST /internal/email/receive  { alias, domain, sender, ... }
// returns one of: forward | reject | tempfail
Enter fullscreen mode Exit fullscreen mode
  • Unknown-but-valid recipient → auto-create the alias on first email and forward. The alias simply appears in the dashboard; the user never created it.
  • Free-plan cap reached → reject new aliases (existing ones keep working).
  • Disabled alias → reject, so leaked addresses go silent at the server edge.
  • Only metadata is recorded (sender, date, size, status). The body is streamed straight through, never persisted.

That single "decide at receive time" inversion is what removes the
create-an-alias-first step entirely.

Gotchas worth knowing

  • tempfail vs reject semantics: use 4xx when you might be wrong (dependency down), 5xx only when the address is genuinely invalid/blocked.
  • DMARC alignment is about the header From, not the envelope — hence the per-domain DKIM signature above.
  • Catch-all + spam: every address existing means every address can be spammed. Per-alias disable (server-side reject) is the escape hatch.
  • Don't store content. It's less liability and, honestly, a better privacy story — you only ever hold metadata.

This runs in production as Tomatoes.run, a
France/EU-hosted take on per-service email aliases (independent, GDPR by
design). If you've fought SPF/DKIM/SRS on forwarding before, I'd love your war
stories in the comments — deliverability is a rabbit hole and I'm still digging. 🍅

Top comments (0)