Skip to content
Engineering

Generate Email Address Randomly for Each Test Run

| | 8 min read
Generate Email Address Randomly for Each Test Run
Generate Email Address Randomly for Each Test Run

If your test suite needs a fresh email address on every run, you are usually trying to avoid three things: mailbox collisions, parallel CI races, and flaky “which message is mine?” selection.

The trap is thinking the solution is purely string generation. A random-looking email address is only useful if it is appropriate for your test goal:

  • For validation-only tests, the address does not need to receive mail, it just needs to be syntactically valid and safely non-routable.
  • For end-to-end verification flows (OTP, magic links, password reset), the address must be routable to an inbox you can read deterministically.

This guide covers both, and shows how to generate email address random per test run without introducing new failure modes.

What “generate email address random” should mean for test automation

When people search “generate email address random,” they often mean “I want a unique address so my signup test doesn’t conflict with other runs.” In practice, you need one of these outcomes:

Outcome A: A safe, fake address (no email will be delivered)

Use this for:

  • frontend validation tests (regex, UI error states)
  • unit tests that must not touch SMTP
  • documentation examples

Here, “random” is about avoiding duplicates in your own dataset, not about receiving an email.

Outcome B: A unique, real inbox (email must be delivered)

Use this for:

  • signup verification emails
  • password reset emails
  • invite flows
  • LLM-agent driven “check your email and continue” tasks

Here, “random” must be paired with isolation and retrieval semantics, otherwise you still get collisions and flakes.

The minimum requirements for random-per-run email addresses

If you want reliability in CI and agent workflows, a per-run address should be:

  • Unique: no two concurrent runs share an inbox.
  • Routable (when needed): messages actually arrive somewhere.
  • Observable: your code can wait for arrival with explicit timeouts.
  • Machine-readable: tests should assert on structured fields, not scrape HTML.
  • Short-lived: inboxes expire and do not turn into accidental long-term data stores.

Here is how common approaches stack up:

Approach Can receive email? Parallel-safe? Typical failure mode Best used for
Random string + example.com No Yes False confidence, nothing arrives Validation-only tests
Plus-addressing (user+tag@domain) Yes (sometimes) Often no Shared mailbox collisions, provider rules vary Lightweight correlation
Catch-all domain you control Yes Depends Routing ambiguity, cleanup burden Controlled environments
Disposable inbox via API (inbox-per-run) Yes Yes Usually only fails if waits/dedupe are wrong CI E2E flows, LLM agents

How to generate a random email address for validation-only tests (the safe way)

If the test does not need to receive mail, do not use a real domain you do not control. Prefer reserved example domains and TLDs.

  • example.com, example.net, and example.org are reserved for documentation and examples (RFC 2606).
  • .test is reserved for testing (RFC 6761).

A robust format

A good pattern is:

<prefix>+<run_id>.<random>@example.test

Where:

  • run_id is deterministic (CI build number, Git SHA, attempt number)
  • random is a short cryptographic token to prevent collisions
  • local-part characters are kept to safe ASCII: a-z, 0-9, +, ., _, -

Example (TypeScript)

import { randomBytes } from "crypto";

export function randomTestEmail(runId: string) {
  const token = randomBytes(8).toString("hex"); // 16 chars
  const local = `test+${sanitize(runId)}.${token}`.toLowerCase();
  return `${local}@example.test`;
}

function sanitize(input: string) {
  // keep it simple and predictable
  return input.replace(/[^a-zA-Z0-9]/g, "").slice(0, 24);
}

This is ideal for validation tests because it is unique, safe, and cannot accidentally email a real person.

Why “random string emails” break end-to-end tests

If you generate [email protected] but you do not control routing and retrieval, your test harness usually ends up doing one of these:

  • reading from a shared mailbox and trying to “find the latest email”
  • sleeping for N seconds and hoping the email arrived
  • matching on weak signals like subject line only

These approaches fail under:

  • parallel CI (two runs interleave)
  • retries (stale emails from the previous attempt)
  • duplicate delivery (at-least-once webhooks, re-sent emails, provider retries)

The fix is not “more randomness.” The fix is inbox isolation.

The deterministic pattern: create one disposable inbox per test run

The most reliable approach is:

  1. Create a disposable inbox via API.
  2. Use the returned email address in your signup/reset flow.
  3. Wait for the expected message with explicit deadlines.
  4. Parse the email as structured data (JSON), extract only the needed artifact (OTP or verification URL).
  5. Expire the inbox.

Mailhook is built around this model: programmable disposable inboxes via API, emails delivered as structured JSON, and webhook-first notifications with polling as a fallback.

You should treat the API contract as canonical and implementation-specific. Use Mailhook’s published integration reference here: mailhook.co/llms.txt.

A simple flow diagram showing: CI test run creates a disposable inbox via API (returns email + inbox_id), app sends verification email to that address, Mailhook delivers message as JSON via webhook (with polling fallback), test extracts OTP or verification link, then inbox expires.

What to store per run

When you create an inbox, your test harness should store a small descriptor, not just a string email address:

  • email (the recipient you pass to the system under test)
  • inbox_id (the handle you use to fetch messages)
  • timestamps (created, expires)

This is what makes the system deterministic: your run reads from its own inbox.

Minimal provider-agnostic pseudocode

Below is intentionally generic so it does not guess endpoint paths. Use mailhook.co/llms.txt for exact request shapes.

type Inbox = {
  inbox_id: string;
  email: string;
  expires_at?: string;
};

type EmailMessage = {
  message_id: string;
  received_at: string;
  subject?: string;
  text?: string;
  // plus any provider-specific fields
};

async function createInbox(): Promise<Inbox> {
  // See mailhook.co/llms.txt for the exact endpoint and schema.
  const res = await fetch("MAILHOOK_CREATE_INBOX_ENDPOINT", {
    method: "POST",
    headers: { "Authorization": `Bearer ${process.env.MAILHOOK_API_KEY}` }
  });
  if (!res.ok) throw new Error(`createInbox failed: ${res.status}`);
  return await res.json();
}

async function waitForMessage(inboxId: string, deadlineMs: number): Promise<EmailMessage> {
  const started = Date.now();
  while (Date.now() - started < deadlineMs) {
    // Prefer webhook-driven receipt when possible.
    // Polling fallback: list messages for inboxId.
    const msg = await pollOnce(inboxId);
    if (msg) return msg;
    await sleep(500);
  }
  throw new Error("Timed out waiting for email");
}

function sleep(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}

Even if you implement webhooks (recommended), a polling fallback is still useful for CI resilience.

Making “random per run” retry-safe (important in CI)

Per-run randomness is not enough if your CI retries a failed job, because the retry can collide with the prior attempt’s state.

A practical policy is:

  • One inbox per attempt, not just per run.
  • Put the attempt identifier into your test logs and correlation fields.
  • Dedupe at the layer you actually consume (OTP, verification URL).

If you use webhooks, verify authenticity. Mailhook supports signed payloads (again, the canonical details live in llms.txt). Signature verification is what makes “email as JSON over HTTP” safe enough for automation.

LLM agent workflows: keep the email tool surface small

If an LLM agent is driving the flow, you want the email portion to be boring and constrained:

  • Agent creates inbox, receives email and inbox_id.
  • Agent waits for a message with a deadline.
  • A deterministic extractor returns only the artifact needed (OTP or a single allowlisted URL).

Avoid giving the model raw HTML or the ability to click arbitrary links from emails. Treat inbound email as untrusted input.

When you should not generate random inboxes

Random inboxes are great for test isolation, but there are cases where you may want a different strategy:

  • Deliverability testing (you often need controlled sender domains, stable recipient identities, and reputation analysis)
  • Long-lived customer support inboxes (you need persistence, access control, auditing)

For CI verification and agent automation, disposable inbox-per-run is usually the simplest reliable approach.

Frequently Asked Questions

Is it okay to use example.com when I generate a random email address? Yes, if you do not need to receive email. For receipt tests, you need a routable domain and a readable inbox.

Why not just use plus-addressing like [email protected]? Plus-addressing can help with correlation, but it still routes into one mailbox. In parallel CI and retries, isolation is usually the missing primitive.

How random does the local-part need to be? For uniqueness, 64 to 128 bits of randomness (for example, 8 to 16 bytes) is plenty. The bigger reliability issue is inbox isolation, not entropy.

What is the most common reason email tests flake even with random addresses? Using a shared mailbox, weak matchers (subject only), and fixed sleeps. “Random” does not fix nondeterministic retrieval.

Where do I find the exact Mailhook API endpoints and schemas? Use the canonical integration reference: https://mailhook.co/llms.txt.

Use Mailhook to generate a fresh inbox per run

If you want to generate a random email address for each test run and reliably receive the message, the workflow you want is inbox-first: create a disposable inbox via API, use its email address, then consume inbound mail as structured JSON.

Mailhook provides disposable inbox creation via API, JSON email output, webhook notifications (plus polling fallback), and signed payloads for automation safety. Start here:

Related Articles