InstantTempEmail.com
Guide

How Developers Use Disposable Emails for Testing (With Real Examples)

A practical guide for developers on using temporary and disposable email addresses in testing workflows — covering manual testing, automated CI/CD pipelines, staging environments, and tools like Mailinator, Mailtrap, and custom SMTP catchers.

TM
··10 min read

Email flows are among the hardest parts of a web application to test properly. Registration confirmations, password resets, transactional emails, onboarding sequences — they all require a working inbox to verify.

Using real email addresses for this is a bad idea: it clutters inboxes, risks sending test emails to real users, and doesn't scale to automated testing. Disposable and temporary email addresses solve all three problems.

This guide covers the full toolkit — from quick manual testing with a temp inbox to fully automated email verification in CI/CD pipelines.


The Core Problem With Email Testing

When you build an email flow, you need to verify:

  1. The email was actually sent
  2. It was delivered (not bounced or caught by spam filters)
  3. The content is correct (subject, body, links)
  4. The links in the email work correctly (confirmation links, password reset tokens)
  5. The flow completes correctly after the link is clicked

Doing this with real email addresses creates several problems:

  • Inbox pollution — Your team's inboxes fill with hundreds of test emails
  • Accidental sends to real users — A misconfigured environment variable and your test emails go to production users
  • No programmatic access — You can't read a real inbox from a CI/CD pipeline without OAuth or IMAP configuration
  • Slow feedback loops — Checking a real inbox manually for every test run doesn't scale

Tool Categories for Email Testing

There are four main approaches, each suited to different situations:

1. Public Disposable Inboxes (Manual Testing)

Services like InstantTempEmail, Guerrilla Mail, or 10 Minute Mail give you a working inbox instantly. Use these when you're manually testing a flow and just need to receive a verification email quickly.

When to use:

  • Local development testing of a new email flow
  • One-off QA checks during development
  • Testing how your email renders in a real inbox

When not to use:

  • Automated tests — you can't programmatically read these inboxes in a test suite

2. Development SMTP Catchers (Automated, Isolated)

These tools intercept all outgoing emails in your development or staging environment — nothing is actually delivered to real addresses. You inspect the captured emails via a web UI or API.

Mailtrap is the most widely used. You configure your app to send via their SMTP server in non-production environments. All emails go to a sandbox inbox.

MailHog is a self-hosted alternative — a lightweight Go application that runs an SMTP server and web UI locally. Zero cost, zero external dependency.

Mailpit is a newer MailHog replacement with a better UI and active development.


3. Public API Inboxes (Automated, With Inbox Access)

Services like Mailinator provide public inboxes accessible via HTTP API. You send email to testuser@mailinator.com from your app, then check the inbox via their API in your test suite.

When to use:

  • End-to-end tests that need to verify email content and click links
  • CI/CD pipelines where you need inbox access without OAuth
  • Testing email-triggered flows (confirmation → redirect → onboarding)

4. Transactional Email Testing Services

Services like Mailtrap (their testing product, separate from the sandbox), Postmark's test mode, or SendGrid's sandbox mode test the full email sending pipeline — API calls, rendering, deliverability — without actually sending.


Setting Up MailHog for Local Development

MailHog is the easiest way to set up email testing locally. It runs an SMTP server that captures all outgoing mail and displays it in a web UI.

Install with Docker Compose

Add MailHog to your docker-compose.yml:

services:
  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"   # SMTP port
      - "8025:8025"   # Web UI port
    restart: unless-stopped

Configure your app to use MailHog

In your .env for development:

MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=null

All emails your app sends in development now go to MailHog. Access the inbox at http://localhost:8025.

Why this is better than real email in development

  • Emails never leave your machine — no accidental sends to real users
  • Instant delivery — no waiting for real SMTP delivery
  • Full email content visible in browser — headers, HTML, text parts, attachments
  • No authentication required
  • Easy to clear all emails between test runs

Automated Email Testing With Mailinator API

For end-to-end tests that need to read emails programmatically, Mailinator's API is the most straightforward option.

The free tier has limits, but their paid API is affordable for most teams.

Basic inbox check with fetch

async function getLatestEmail(inboxName) {
  const response = await fetch(
    `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inboxName}`,
    {
      headers: {
        Authorization: process.env.MAILINATOR_API_KEY
      }
    }
  )

  const data = await response.json()

  if (!data.msgs || data.msgs.length === 0) {
    throw new Error(`No emails found in inbox: ${inboxName}`)
  }

  return data.msgs[0] // Most recent email
}

async function getEmailBody(messageId) {
  const response = await fetch(
    `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/test/messages/${messageId}`,
    {
      headers: {
        Authorization: process.env.MAILINATOR_API_KEY
      }
    }
  )

  return response.json()
}

Full end-to-end registration test with Playwright

import { test, expect } from '@playwright/test'

const TEST_EMAIL = `e2e-test-${Date.now()}@mailinator.com`

test('user registration and email confirmation', async ({ page }) => {
  // Step 1: Register on the site
  await page.goto('https://staging.yourapp.com/register')
  await page.fill('[name="email"]', TEST_EMAIL)
  await page.fill('[name="password"]', 'TestPassword123!')
  await page.click('[type="submit"]')

  // Expect redirect to "check your email" page
  await expect(page).toHaveURL(/check-email/)

  // Step 2: Wait for and fetch the confirmation email
  const email = await waitForEmail(TEST_EMAIL.split('@')[0], {
    timeout: 30000,
    subject: 'Confirm your email'
  })

  // Step 3: Extract confirmation link from email body
  const confirmUrl = extractConfirmationLink(email.body)
  expect(confirmUrl).toBeTruthy()

  // Step 4: Visit the confirmation link
  await page.goto(confirmUrl)

  // Step 5: Verify successful confirmation
  await expect(page).toHaveURL(/dashboard/)
  await expect(page.locator('h1')).toContainText('Welcome')
})

async function waitForEmail(inbox, options = {}) {
  const { timeout = 15000, subject } = options
  const startTime = Date.now()

  while (Date.now() - startTime < timeout) {
    const response = await fetch(
      `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inbox}`,
      { headers: { Authorization: process.env.MAILINATOR_API_KEY } }
    )

    const data = await response.json()
    const emails = data.msgs || []

    const match = subject
      ? emails.find(e => e.subject?.includes(subject))
      : emails[0]

    if (match) {
      // Fetch full email body
      const bodyResponse = await fetch(
        `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${inbox}/messages/${match.id}`,
        { headers: { Authorization: process.env.MAILINATOR_API_KEY } }
      )
      return bodyResponse.json()
    }

    await new Promise(resolve => setTimeout(resolve, 1000))
  }

  throw new Error(`Email not received within ${timeout}ms`)
}

function extractConfirmationLink(emailBody) {
  const htmlContent = emailBody.parts?.[0]?.body || ''
  const urlPattern = /https?:\/\/[^\s"'<>]+confirm[^\s"'<>]*/i
  const match = htmlContent.match(urlPattern)
  return match?.[0] || null
}

Using Unique Addresses Per Test Run

A common mistake is reusing the same test email address across test runs. Old emails pollute the inbox and tests may pick up the wrong email.

Pattern: timestamp-based unique addresses

// Generate a unique inbox per test run
const testRunId = Date.now()
const email = `registration-test-${testRunId}@mailinator.com`

Pattern: UUID-based per test

import { randomUUID } from 'crypto'

function testEmail(prefix = 'test') {
  return `${prefix}-${randomUUID().slice(0, 8)}@mailinator.com`
}

// Usage
const email = testEmail('registration') // registration-a1b2c3d4@mailinator.com

This ensures each test run has a fresh inbox with no history.


Setting Up Mailpit (Modern MailHog Replacement)

Mailpit has a better UI, active development, and more features than MailHog. It's the better choice for new setups.

# docker-compose.yml
services:
  mailpit:
    image: axllent/mailpit
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # Web UI
    environment:
      MP_MAX_MESSAGES: 500
      MP_DATABASE: /data/mailpit.db
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
    volumes:
      - mailpit_data:/data

volumes:
  mailpit_data:

Mailpit also has a REST API for programmatic inbox access, making it viable for automated tests in a Docker Compose environment:

// Mailpit API — for use in local/CI Docker Compose setups
async function getMailpitInbox() {
  const response = await fetch('http://localhost:8025/api/v1/messages')
  return response.json()
}

async function deleteAllMessages() {
  await fetch('http://localhost:8025/api/v1/messages', { method: 'DELETE' })
}

Clear the inbox at the start of each test run with deleteAllMessages() to ensure a clean state.


Preventing Accidental Production Email Sends

This is critical. A misconfigured environment variable that points staging to production SMTP credentials will send real emails to real users.

Approach 1: Environment-based SMTP switching

// email.config.js
const emailConfig = {
  development: {
    host: 'localhost',
    port: 1025,
    secure: false,
    auth: null
  },
  test: {
    host: 'localhost',
    port: 1025,
    secure: false,
    auth: null
  },
  staging: {
    host: process.env.MAILHOG_HOST || 'mailhog',
    port: 1025,
    secure: false
  },
  production: {
    host: process.env.SMTP_HOST,
    port: 587,
    secure: true,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS
    }
  }
}

export const smtpConfig = emailConfig[process.env.NODE_ENV] || emailConfig.development

Approach 2: Domain allowlist for non-production environments

function shouldSendEmail(toAddress) {
  if (process.env.NODE_ENV === 'production') return true

  const allowedTestDomains = [
    'mailinator.com',
    'guerrillamail.com',
    'instanttempemail.com',
    'yourcompany.com'  // Internal test addresses
  ]

  const domain = toAddress.split('@')[1]
  return allowedTestDomains.includes(domain)
}

Testing Email Rendering Across Clients

The tools above verify email delivery and content, but not how the email renders in different clients. Email HTML behaves differently across Gmail, Outlook, Apple Mail, and mobile clients.

For rendering tests, use Email on Acid or Litmus — they render screenshots of your email across 90+ clients. Both have free trials.

For simpler checks, the Mailtrap email preview feature renders your email in common client contexts.


Frequently Asked Questions

Can I use Gmail or Outlook for automated testing? Technically yes, using OAuth or IMAP access. In practice, it's slower, more complex to set up, and introduces rate limits and authentication management. Dedicated tools are always faster and more reliable.

What's the difference between Mailtrap and MailHog? Mailtrap is a hosted SaaS service — your emails are sent to their servers and viewable in their web UI. MailHog is self-hosted — it runs on your machine or in your Docker Compose setup. MailHog has zero cost and zero data leaving your environment. Mailtrap is easier to share across a team.

Is Mailinator safe to use for sensitive test data? No. Mailinator inboxes are public — anyone who knows the inbox name can read the emails. Use test data only. Never use real user data in Mailinator inboxes. For private automated testing, use MailHog or Mailpit in your Docker Compose setup instead.

How do I test email flows in a GitHub Actions CI pipeline? Add a MailHog or Mailpit service to your GitHub Actions workflow using the services key in your workflow YAML. Your app sends to localhost:1025, and your tests read via the local API.

jobs:
  test:
    services:
      mailpit:
        image: axllent/mailpit
        ports:
          - 1025:1025
          - 8025:8025

Should I test with real email providers in staging? Only if you specifically need to test deliverability (spam scoring, DKIM/DMARC, inbox placement). For functional testing of email flows, a local catcher like MailHog is faster and safer. Run real deliverability checks separately, less frequently, using test accounts at major providers.

Try it now — it's free

Get Your Disposable Email Instantly

No sign-up. No spam. Your address is ready in one click.

Open TempMail →