Skip to content
Engineering

Create Temp Email With Custom Domain: A CI-Friendly Recipe

| | 8 min read
Create Temp Email With Custom Domain: A CI-Friendly Recipe
Create Temp Email With Custom Domain: A CI-Friendly Recipe

CI failures caused by email are rarely “email problems.” They are isolation problems (shared mailboxes), observability problems (no stable IDs), and timing problems (fixed sleeps, unknown delays). The fastest way to make those failures go away is to stop treating email like a human inbox and start treating it like an event stream.

When you create temp email with custom domain support, you gain two CI-friendly advantages at once:

  • Your tests use disposable, isolated recipients that will not collide in parallel runs.
  • Your recipients live under a domain you control (usually a dedicated subdomain), which is much easier to allowlist in enterprise environments.

Below is a practical, CI-ready recipe you can copy, adapt, and automate.

What “temp email with custom domain” should mean in CI

A CI-friendly setup is not “a mailbox with a vanity domain.” It is:

  • Routable inbound email (your domain’s MX points somewhere that accepts mail).
  • Programmatic isolation (you can create a fresh inbox per test attempt).
  • Machine-readable retrieval (emails arrive as structured data, not HTML you scrape).
  • Deterministic waiting (webhooks first, polling as fallback, explicit deadlines).
  • Security boundaries (verify webhook authenticity, treat email as untrusted input).

Mailhook is built around these primitives: programmable disposable inboxes via API, email delivered as structured JSON, real-time webhooks (with signed payloads), and a polling API fallback. For the canonical integration contract, always refer to mailhook.co/llms.txt.

The CI-friendly recipe (high level)

This recipe assumes you already own a domain (or can create a subdomain) and want your CI runs to generate temporary inboxes under it.

Step 1: Choose a safe domain layout (use a dedicated subdomain)

Use a dedicated subdomain rather than your main production domain. For example:

  • ci-mail.example.com for CI
  • staging-mail.example.com for staging

Why this helps:

  • Lets you isolate reputation and policies from production mail.
  • Makes it easy to rotate providers or disable the whole surface area quickly.
  • Prevents accidental cross-environment collisions.

If you need a deeper explanation of domain strategy trade-offs, see Mailhook’s guide on shared vs custom domains.

Step 2: Point MX records to your inbound provider

To receive email at [email protected], the subdomain must be routable via MX records.

Practical notes for CI teams:

  • Keep MX configuration in code (Terraform, Pulumi, or your DNS provider’s GitOps workflow).
  • Verify MX propagation in a pipeline step or preflight script (for example, using dig).
  • Remember the SMTP distinction: delivery routes by the envelope recipient, which can differ from the To: header. If you build routing rules, build them on the right layer.

If you are using Cloudflare DNS, their MX record documentation is a good quick reference.

Step 3: Decide how addresses map to inboxes

A custom domain alone does not solve collisions. You need a routing strategy that produces unique recipients per run (and preferably per retry attempt).

Common CI-friendly patterns include:

  • Inbox-per-attempt (recommended): create a new disposable inbox for each attempt, and use the provider-assigned address.
  • Deterministic recipients: encode a run ID, test ID, and attempt number into the local-part (useful when you need to correlate by address alone).

If you want a routing deep dive, Mailhook has a detailed post on routing patterns that scale.

Step 4: In CI, create an inbox per attempt and pass the address into the system under test

This is the key move that eliminates races.

In your test harness (or agent tool), create an inbox at the start of the attempt, then use its email address in the signup/reset/magic-link flow.

A provider-agnostic interface works well:

  • create_inbox({ domain, metadata }) -> { inbox_id, email_address }
  • wait_for_message({ inbox_id, matcher, deadline }) -> message_json

With Mailhook, you can implement those functions using the API contract documented in llms.txt without exposing low-level email plumbing to every test.

Step 5: Receive messages webhook-first, polling as fallback

Webhook-first is the CI-friendly default: fast, cheap, and naturally parallel.

Polling fallback is how you keep the system reliable when webhooks are temporarily unavailable, blocked by a network policy, or delayed.

A practical waiting policy:

  • Use a single overall deadline for the attempt (for example, 30 to 90 seconds depending on your mail latency and product UX).
  • Prefer webhooks to push events into a queue or in-memory buffer keyed by inbox_id.
  • If the deadline is approaching and nothing arrived, poll with a short backoff.

Mailhook supports both webhooks and polling, which lets you implement this hybrid pattern without changing your test semantics.

Step 6: Parse as JSON, extract the minimal artifact

In CI (and especially with LLM agents), treat inbound email as hostile input:

  • Prefer extracting the single artifact you need (OTP, verification URL) instead of giving the whole HTML body to downstream tools.
  • Store the full message JSON as a CI artifact for debugging, but do not print sensitive content to logs.

If you want a clean, provider-agnostic schema mindset, Mailhook’s article on email-to-JSON fields is a solid starting point.

Step 7: Verify authenticity of inbound events (signed webhooks)

A common security mistake is assuming “the email was DKIM signed” implies your webhook payload is authentic.

They are separate threat models.

If you receive email via webhook, you should verify the webhook request itself. Mailhook provides signed payloads for this purpose. Use the verification guidance and exact header/field names from the canonical spec in llms.txt.

For the conceptual model and checklist, see verify webhook payload authenticity.

Step 8: Clean up inbox lifecycle (reduce clutter and risk)

Disposable inboxes are an operational tool, so treat them like any other CI resource:

  • Ensure inboxes do not linger indefinitely.
  • Keep retention short by default, longer only when debugging.
  • Prefer attaching message JSON to CI artifacts over keeping long-lived mailboxes.

Mailhook is designed around disposable inbox workflows; consult llms.txt for lifecycle semantics.

A concrete CI wiring pattern

Most teams end up with the same architecture, regardless of CI vendor:

  • A test job creates an inbox per attempt.
  • The address goes into the app under test.
  • Your webhook endpoint receives the email-as-JSON event.
  • The test waits on an internal “arrival buffer” keyed by inbox ID.

A simple CI email testing architecture: CI runner creates a disposable inbox on a custom subdomain, app sends verification email, inbound provider delivers JSON via signed webhook to a test webhook endpoint, tests extract OTP or link and assert.

Why this stays stable under parallel CI

Parallel runs and retries create the classic failure modes: wrong message selected, duplicate deliveries, and timeouts.

The recipe avoids them by construction:

  • Isolation: inbox-per-attempt means no shared mailbox state.
  • Determinism: tests wait for a message in a specific inbox, not “the latest email.”
  • Idempotency-friendly: duplicates can be safely deduped using stable IDs.

For a deeper reliability breakdown, Mailhook’s engineering post on email testing in parallel CI is worth keeping as a reference.

Checklist table: what to implement (and why)

Recipe component Why it matters in CI What to look for in a provider
Dedicated subdomain Isolation from production and easy allowlisting Custom domain support + clear DNS instructions
Inbox per attempt Eliminates collisions in parallel and retries Disposable inbox creation via API
Webhook-first delivery Low latency and scalable waiting Real-time webhook notifications
Polling fallback Reliability when webhooks are delayed/unavailable Polling API with stable message listing
JSON email output Assertions on data, not HTML scraping Structured JSON email output
Webhook authenticity Prevent spoofing and replay Signed payloads for security
Batch handling Efficient for large suites Batch email processing

Common pitfalls (and how to avoid them)

Pitfall: using your production root domain for tests

Use a subdomain. It keeps operational risk low and makes it easy to rotate or disable.

Pitfall: relying on plus-addressing for isolation

Plus-tags help with correlation, but they do not guarantee isolation, and they often break when systems normalize addresses.

Pitfall: fixed sleeps (“wait 10 seconds for email”)

Replace sleeps with deadline-based waiting. CI environments vary, and email latency is not a constant.

Pitfall: feeding full HTML email into an LLM agent

Extract the minimal artifact, and only pass the minimum required fields. Treat links and HTML as dangerous inputs.

FAQ

Do I really need a custom domain, or can I start with a shared domain? You can start with a shared domain to validate your harness quickly, then migrate to a custom domain when you need allowlisting, tighter isolation, or governance.

What should I allowlist in enterprise environments? Usually the dedicated subdomain you use for inbound test email (for example, ci-mail.example.com). Keep it separate from production to simplify approvals.

Should CI always use webhooks? Prefer webhooks, but keep polling as a fallback so your tests do not fail when your webhook receiver is temporarily unreachable.

How do I prevent email duplicates from breaking tests? Use an inbox-per-attempt pattern and make your processing idempotent. Deduplicate at the message or extracted-artifact level using stable identifiers.

Where do I find the exact Mailhook API details for custom domains, inbox creation, webhooks, and signatures? Use the canonical integration reference: mailhook.co/llms.txt.

Try it with Mailhook

If you want a practical way to create disposable inboxes on a custom domain and consume inbound mail as structured JSON in CI, Mailhook is built for exactly this workflow.

Related Articles