Skip to content
Engineering

Routing Emails to Inboxes: Keys, Aliases, and Catch-All

| | 10 min read
Routing Emails to Inboxes: Keys, Aliases, and Catch-All
Routing Emails to Inboxes: Keys, Aliases, and Catch-All

Email automation breaks in boring, expensive ways: the “wrong” test run consumes the email, a staging flow picks up production mail, or your LLM agent replies to something it should never have seen. Most of those failures are not “email problems,” they’re routing problems.

When you route inbound mail into API inboxes (temporary inboxes, QA inboxes, agent inboxes), you need a deterministic rule that answers one question:

Given an SMTP recipient, which inbox should receive this message?

In practice, routing boils down to three common patterns:

  • Keys (encoded local-parts): the recipient contains a routing key you can decode.
  • Aliases (mapping tables): the recipient is looked up in a table that points to an inbox.
  • Catch-all: the domain accepts any recipient, then you route by conventions and correlation.

This guide focuses on those three patterns, how they fail, and how to pick one that stays reliable under retries, parallel CI, and agent-driven workflows.

What “routing emails to inboxes” really means

If you’re using a programmable inbound email provider (or building your own), email routing is usually a three-layer pipeline:

  1. Domain routing (DNS/MX): mail for @yourdomain.com is delivered to your inbound system.
  2. Recipient-to-inbox mapping: the inbound system decides which logical inbox gets the message.
  3. Delivery to code: the message is delivered as JSON (webhook and/or polling) to your application.

Only layer 2 is the “keys, aliases, catch-all” part, but layer 1 and 3 affect how safe and debuggable it is.

A simple diagram showing three layers of inbound email routing: DNS/MX routes a domain to an inbound processor, recipient mapping routes addresses to specific inboxes (keys, alias table, catch-all), and delivery sends messages as JSON to webhooks or a polling API.

A key detail many teams miss: routing should be based on the SMTP envelope recipient (the RCPT TO), not the rendered To: header. The envelope is what the SMTP server actually used to deliver the message. If you want the standards reference, see RFC 5321 (SMTP).

Pattern 1: Keys (encoded local-parts)

Keys (also called “encoded local-parts”) are the cleanest way to do deterministic routing at scale.

You generate recipients like:

…and your router decodes the local-part into an inbox_id (or into a compound key you can map to an inbox).

Why keys work well

Keys are attractive because they are stateless at routing time:

  • No database lookup is required to decide where the message goes.
  • Parallel runs do not collide if keys are unique.
  • You can embed environment and purpose into the key so it is self-describing.

That “statelessness” is especially valuable for CI and LLM agents because retries and concurrency are the default, not the exception.

Designing a good routing key

A good key is boring: predictable to generate, strict to parse, and hard to confuse.

Common design rules:

  • Use a restricted alphabet. Base32 (uppercase A-Z + 2-7) or lowercase alphanumerics are easier to normalize reliably than “anything allowed in email.”
  • Treat the local-part as case-insensitive even though email technically allows case sensitivity. Many systems normalize to lowercase.
  • Make parsing unambiguous. Prefer prefix_version_payload_checksum over free-form tokens.
  • Avoid secrets and PII in the address. Email addresses leak into logs, analytics, and vendor systems.
  • Add a checksum or short MAC-like check (even a simple checksum) to detect truncation and typos.

A pragmatic format:

mh1_<env>_<payload>_<check>@inbox.yourdomain.com

Where:

  • mh1 is a versioned prefix.
  • <env> is dev|stg|prod (or similar).
  • <payload> is a compact encoding of your inbox handle.
  • <check> helps reject malformed recipients early.

The key failure modes

Keys fail when you assume email address handling is “string safe.” The common footguns:

  • Plus addressing and dot normalization. Some providers treat [email protected] as the same as [email protected], and Gmail-style dot normalization (a.b@) can surprise you. If you need deterministic routing, do not rely on provider-specific behaviors.
  • Length limits. Very long local-parts can be truncated by downstream systems or UIs.
  • Ambiguous separators. Using many symbols (-, ., +) can collide with systems that rewrite addresses.

If you want a key-based strategy but also need human-friendly addresses, you can keep the key short and store metadata in your system instead of the recipient.

Pattern 2: Aliases (recipient mapping tables)

Aliases mean you treat the email recipient as a lookup key:

recipient_emailinbox_id

This is the most familiar model if you come from mailbox systems (“this address belongs to that inbox”), but for automation it is usually a deliberate trade: you are choosing state and operational overhead in exchange for flexibility.

When aliases are the right choice

Aliases are useful when:

  • You must support pre-existing recipients (legacy tests, vendor callbacks, hard-coded templates).
  • You need human readable addresses (for client ops workflows, demos, or manual QA).
  • You want explicit governance: “only these recipients are valid.”

That last point is underrated. Aliases provide a built-in allowlist: if the alias does not exist, you can reject or quarantine the message.

Minimal alias table design

A production-friendly alias mapping has at least:

Field Why it exists
alias_email Exact recipient you expect to receive
inbox_id The target inbox
created_at Auditing and debugging
expires_at (or TTL) Prevents stale aliases from receiving unexpected mail
state (active, draining, closed) Handles late arrivals safely

The big decision is lifecycle: do aliases live as long as the inbox, or do you rotate them more aggressively than inboxes? For CI and agents, short-lived aliases reduce surprise mail.

Alias failure modes

Aliases tend to fail operationally, not conceptually:

  • Race conditions in parallel runs when two jobs request the same alias.
  • Stale mappings where an alias points to a closed inbox.
  • Migration gaps when you switch providers or domains and forget to port mappings.

If you choose aliases, treat them like any other production mapping table: idempotent writes, uniqueness constraints, and explicit expiry.

Pattern 3: Catch-all domains (accept anything)

A catch-all domain accepts mail for any local-part, then you decide how to route it.

Catch-all is appealing because it feels “zero setup,” but it can quietly become the highest-risk pattern if you do not add constraints.

Catch-all can mean two different things

  • Catch-all with key routing: you still encode a key in the local-part, you just do not pre-register recipients.
  • Catch-all with correlation routing: you accept many recipients, then pick the right message later using subject tokens, headers, or body content.

The first is usually fine. The second is where flakiness and security issues appear.

Safer catch-all: constrain the namespace

If you use catch-all, make the local-part self-validating and reject anything that does not match your pattern.

Example convention:

<prefix>_<env>_<run_id>_<rand>@inbox.yourdomain.com

Then enforce:

  • <prefix> must be a known constant.
  • <env> must match known environments.
  • <run_id> must be in a strict format.
  • <rand> must be minimum length.

This turns catch-all into “key routing without registration,” which keeps routing deterministic.

Risky catch-all: routing by headers or subject tags

Routing by subject tags (like “include [RUN:123] in the subject”) or custom headers can work, but it is much easier to break:

  • The sending system changes templates.
  • Forwarders rewrite headers.
  • Attackers can spoof correlation tokens.

If you must route this way (for example, you cannot control the recipient address), treat it as a best-effort signal and combine it with other constraints (time windows, known sender domains, strict message matchers).

Choosing between keys, aliases, and catch-all

Most teams end up using more than one pattern, but you should have a default.

Pattern Routing state Best for Main downside Default recommendation
Keys (encoded local-parts) Stateless CI, QA automation, LLM agents, high parallelism Requires key design discipline Best default for automation
Aliases (mapping table) Stateful Legacy recipients, human-friendly addresses, explicit allowlists Operational overhead (expiry, collisions) Use selectively
Catch-all Mixed Fast experiments, “any recipient” inbound integrations Noise and misrouting unless constrained Only with strict patterns

If your primary problem is flakiness under parallelism, keys are usually the fastest path to determinism.

Implementation guardrails (the parts that prevent 2 AM incidents)

Routing is not just “where does it go,” it is also “how do we prove it went there, exactly once, and safely.” A few guardrails matter disproportionately:

Normalize recipients before routing

Pick a canonicalization policy and apply it consistently:

  • Lowercase the local-part and domain.
  • Strip surrounding whitespace.
  • Reject unexpected characters instead of trying to be clever.

This prevents subtle mismatches between what you generated and what you received.

Log routing identifiers, not raw content

For debugging, log:

  • inbox_id
  • the normalized recipient
  • message identifiers (for example, provider delivery IDs and message IDs)
  • timestamps

Avoid logging full bodies by default. Email often contains secrets (OTP codes, magic links) and personal data.

Treat inbound email as untrusted input

This matters even more when LLM agents are involved. Do not hand an agent raw HTML email and ask it to “click the link.” Build a minimized view or extract a single artifact (OTP or verified URL) and give the agent only that.

Secure delivery events (webhooks)

If you route into webhooks, verify authenticity. Email-level signals like DKIM do not authenticate the webhook request itself.

Mailhook supports signed payloads for webhook notifications, so you can verify the request body before processing. For the exact integration contract and current API details, use the canonical reference: llms.txt.

Putting it together with Mailhook (without guessing at your API)

Mailhook is built for this “email as an event stream” model:

  • Create disposable inboxes via API
  • Receive emails as structured JSON
  • Get real-time webhooks (with signature support)
  • Fall back to a polling API when needed
  • Use shared domains instantly, or bring a custom domain for tighter control
  • Handle volume with batch email processing

A typical routing setup looks like:

  • Pick your routing pattern (keys are a strong default).
  • Provision an inbox for each run, attempt, or agent session.
  • Generate an address whose local-part encodes the routing key (or allocate an alias).
  • Consume messages as JSON via webhook, using polling as a fallback.

When you are ready to implement, start with the API contract in Mailhook’s llms.txt and the product overview at Mailhook. That avoids the most common failure in email routing projects: implementing “almost the same” semantics as your provider.

A practical rule of thumb

If you only take one thing from this article:

Use keys for determinism, aliases for compatibility, and catch-all only when you also enforce a strict key format.

That combination gives you predictable routing today and a sane migration path later, especially when your “consumer” is not a human, but a test harness or an autonomous agent.

Related Articles