How-To Guides

Test Email Delivery: The Complete Developer Guide

TempMailSpot Editorial Team
12 min read

Stop sending test emails to your personal inbox or (worse) real users. This developer guide covers every method for testing email delivery safely and effectively.

"Test email delivery" hides two separate questions, and conflating them is why email features ship broken. The first: did my code actually hand a message to an SMTP server and get an acceptance back? The second: when that message reaches a real provider like Gmail or Outlook, does it land in the inbox or the spam folder? The first is a send test you run locally; the second is a deliverability test that depends on DNS authentication you set up once and verify before every domain change.

This guide separates the two. You capture outgoing mail locally so test messages never touch real recipients, confirm your sender authentication passes, then verify rendering and arrival from the recipient's side. Each step below is a concrete command or check.

Key takeaways

  • "Test email delivery" is two jobs: a send test (did the SMTP server return 250 OK?) and a deliverability test (does the message land in the inbox?). Test them separately.
  • Capture outgoing mail locally with Mailpit (SMTP 1025, UI 8025) so test messages never reach real recipients; it's the maintained successor to MailHog, whose last release was August 2020.
  • Deliverability depends on three DNS standards passing and aligning: SPF (RFC 7208), DKIM (RFC 6376), and DMARC (RFC 7489). Verify them by reading a delivered message's Authentication-Results header.
  • For recipient-side checks (rendering, verification links, one-time codes), send to a disposable inbox; a free no-registration option is TempMailSpot, and Mail.tm, Guerrilla Mail, and Maildrop offer free APIs for automation.
  • In CI, run a bounded poll-and-assert against your capture server on every commit; keep the real-send SPF/DKIM/DMARC probe in a slower scheduled job to avoid flaky builds.
  • Make blocking real sends outside production the default by configuration, never hardcode production SMTP credentials, and test failure paths (rejected sends, timeouts), not just the happy path.

The two questions "test email delivery" actually asks

Before you reach for a tool, decide which problem you have.

The first question is mechanical. Your application calls an SMTP server, issues MAIL FROM, RCPT TO, and DATA, and the server either accepts the message or rejects it. In SMTP, an accepted command returns a 250 OK reply (RFC 5321), the Simple Mail Transfer Protocol standard that also defines how MX records route mail. A 250 after DATA means the server took responsibility for the message. That is the success signal an automated send test asserts on. It tells you nothing about whether a human ever sees the email.

The second question is about placement. A perfectly-sent message still lands in spam if your domain fails authentication. Receivers grade you on three DNS-published standards, and a deliverability test exists to confirm all three pass and align. Skip authentication and a 250 OK is no guarantee of arrival: the message left your server and quietly died in a junk folder.

Most "my emails aren't arriving" bugs are actually the second question in disguise. The send succeeded; the placement failed. Test them separately so you know which layer to fix.

Step 1: Capture mail locally so tests never reach real users

The cardinal rule of email testing: a test message must never reach a real recipient. The clean way to guarantee that is to point your app at a fake SMTP server that traps everything.

Run a local capture server with Mailpit

Mailpit is an email and SMTP testing tool for developers. It acts as an SMTP server, provides a web interface to view and test captured emails, and ships a REST API for automated integration testing. It was inspired by MailHog, which is no longer maintained and hasn't had active development or security updates in years, so reach for Mailpit as the actively-maintained successor rather than copying an old MailHog tutorial.

Mailpit listens on SMTP port 1025 and serves its web UI on port 8025 by default. Run it with Docker:

docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit

Point your application's SMTP config at it:

SMTP_HOST=localhost
SMTP_PORT=1025
# no auth, no TLS needed locally

Now send a message from your app, then open http://localhost:8025 to read it exactly as the bytes left your code: headers, HTML, plain-text part, and attachments. Nothing leaves your machine.

Use a hosted sandbox for staging

For shared staging environments where teammates and CI need to see the same captured mail, a hosted trap is easier than self-hosting. Mailtrap Email Sandbox acts as a fake SMTP server that traps all emails sent from your application, and messages sent to the sandbox never reach real recipients. Swap the host and port for the sandbox credentials and the same safety property holds in the cloud.

For a quick comparison of the local-capture options:

ToolRoleDefault portsAPIMaintenance status
MailpitLocal SMTP capture + web UISMTP 1025 / UI 8025RESTActively maintained
MailHogLocal SMTP capture + web UISMTP 1025 / UI 8025RESTUnmaintained; last release v1.0.1, Aug 11 2020
Mailtrap SandboxHosted SMTP trapProvider SMTPRESTCommercial, maintained

If an old guide still tells you to docker run mailhog/mailhog, know that MailHog's last release was version 1.0.1 on August 11, 2020 and the absence of releases since suggests it is no longer maintained. The ports and config are nearly identical, so migrating to Mailpit is mostly a one-line change.

Step 2: Verify the message left your server (250 OK)

With a capture server running, the send test is small and deterministic. You want to assert two things: the SMTP transaction succeeded, and the message arrived in the trap with the right content.

Assert on the SMTP response, not on "it didn't throw"

When the receiving server accepts your DATA, it returns a 250 OK reply. Most SMTP libraries surface this as a resolved promise or a response object containing the code. Assert on the code explicitly so a silent change in transport behaviour can't pass your test:

import nodemailer from 'nodemailer';

const transport = nodemailer.createTransport({
  host: 'localhost',
  port: 1025, // Mailpit
});

const info = await transport.sendMail({
  from: 'app@example.com',
  to: 'recipient@example.com',
  subject: 'Password reset',
  html: '<p>Reset link: ...</p>',
});

// Nodemailer exposes the server's reply string
console.log(info.response); // "250 2.0.0 Ok: queued"
if (!info.response.startsWith('250')) {
  throw new Error(`SMTP did not accept message: ${info.response}`);
}

Then confirm the captured message

A 250 proves the handoff. To prove the message is correct, read it back from the capture server's API. Mailpit's REST API lets a test fetch the latest captured message and assert on its subject, recipients, and body:

const res = await fetch('http://localhost:8025/api/v1/messages');
const { messages } = await res.json();
const latest = messages[0];

expect(latest.Subject).toBe('Password reset');
expect(latest.To[0].Address).toBe('recipient@example.com');

This is the whole send test: transaction accepted, message captured, content verified, with zero risk to real inboxes. What it deliberately does not test is whether Gmail would have filed the same message under spam. That is the next step.

Step 3: Test deliverability with SPF, DKIM, and DMARC

Once mail leaves your server for real recipients, three DNS-published standards decide whether it lands in the inbox. Deliverability testing is, in practice, confirming that all three pass and align for your sending domain.

What each standard does

SPF (Sender Policy Framework) declares which mail servers are authorized to send mail for your domain. It is defined by RFC 7208, published April 2014, and lives as a TXT record in your DNS. A receiver checks whether the connecting server is on your authorized list.

DKIM (DomainKeys Identified Mail) cryptographically signs outgoing email so a recipient can verify the message was not altered in transit and genuinely came from your domain. It is defined by RFC 6376, published September 2011. You publish a public key in DNS; your sending platform signs with the private key.

DMARC (Domain-based Message Authentication, Reporting, and Conformance) is the policy layer that ties SPF and DKIM together and tells receivers what to do when authentication fails: accept, quarantine, or reject. It is defined by RFC 7489, published March 2015, and is also how you receive aggregate reports about who is sending under your domain.

StandardSpecificationPublishedDNS recordVerifies
SPFRFC 7208April 2014TXTAuthorized sending servers
DKIMRFC 6376September 2011TXT (public key)Message integrity + origin
DMARCRFC 7489March 2015TXT (_dmarc)Policy + alignment + reporting

How to test that they pass

You cannot see SPF, DKIM, or DMARC results in a local capture server, because those checks happen on the receiving side. To test them, send a single real message to a deliverability checker or to an inbox you control and read the received-message headers. In a delivered message, look at the Authentication-Results header: you want spf=pass, dkim=pass, and dmarc=pass. A common subtle failure is alignment, where SPF and DKIM individually pass but the domains they authenticate don't match the visible From: domain, so DMARC still fails. Dedicated spam-score and inbox-placement checkers exist for this exact job; whichever you use, the headers are the ground truth.

Run this check after any change to your DNS, your sending platform, or your From domain, not just once at launch.

Step 4: Verify rendering and arrival from the recipient's side

A capture server shows you the bytes you sent. It does not show you what a recipient's email client renders, and it cannot exercise the full path of a verification or password-reset link the way a real inbox does. For that, send to a disposable inbox you can open in a browser and read like any user would.

Manual rendering checks with a disposable inbox

Generate a throwaway address, drop it into your seed data or a signup form, and read the result in a real inbox view. TempMailSpot is a free, no-registration option for this: open it, copy the address, and new mail appears automatically within seconds (it polls aggressively for the first minute and a half, then settles to a slower cadence), so you see the welcome or reset email arrive without refreshing. The 10-minute default expiry, extendable without limit, means you do not accumulate test accounts, and you can export the captured message as PDF, JSON, or EML if you need to attach it to a bug report. Because TempMailSpot can also send a message (behind a CAPTCHA), it is useful for the rarer case of testing an inbound or reply-to flow, where most disposable services are receive-only. For the longer view on where disposable inboxes fit a developer workflow, see our temporary email for developers guide.

Automatable disposable inboxes (APIs)

For end-to-end tests that need to read a real delivered message, such as clicking a verification link or asserting a one-time code, a disposable-inbox API lets you generate an address and poll for arrival in code. A few free options, none requiring a paid key to start:

ServiceAPI styleSend?Notable limits
Mail.tmREST, no keyReceive-only8 requests/sec per IP
Guerrilla MailJSONReceive-onlyNo documented send function
MaildropREST / GraphQLReceive-onlyMax 10 messages; cleared after 24h idle
TempMailSpotREST (/api/v1) + widgetSend (CAPTCHA)10-min default, extendable

Mail.tm is a completely free temp-mail REST API that needs no registration or key to start and is rate-limited to 8 queries per second per IP. Guerrilla Mail's JSON API covers getting an address and checking, fetching, and deleting mail, with no documented send function. Maildrop holds a maximum of 10 messages per inbox and erases everything after 24 hours idle. Pick the one whose rate limits and retention fit your test cadence; the integration shape is the same across all of them: create address, poll inbox, read message, assert. (For how these services work under the hood, see how disposable email works.)

One caveat worth noting in tests: some production sites block disposable domains at signup using public blocklists like the disposable-email-domains list that PyPI and others use. If your own product blocks these domains, your end-to-end test address may be rejected, which is itself a useful thing to discover before a user does.

Step 5: Wire it into CI

Manual checks catch the obvious break. CI catches the regression three sprints later when someone refactors the mailer and the 250 stops coming. The pattern is the same loop you ran by hand, made deterministic.

A reliable email integration test in CI does four things:

  1. Start a capture server as a service container (Mailpit on 1025/8025) or generate a disposable address via an API.
  2. Trigger the application behaviour that sends mail, such as registering a user or requesting a reset.
  3. Poll for arrival with a bounded timeout and a retry interval, because delivery is asynchronous. Treat a missing message after the timeout as a failure, not a flake to ignore.
  4. Assert on the captured message: recipient, subject, a body token, and the presence of the expected link.
// Poll Mailpit until the message arrives or we time out
async function waitForMessage(timeoutMs = 15000, intervalMs = 500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { messages } = await fetch('http://localhost:8025/api/v1/messages')
      .then((r) => r.json());
    if (messages.length > 0) return messages[0];
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error('No email captured within timeout');
}

Keep the deliverability check (SPF/DKIM/DMARC) out of the per-commit pipeline. It depends on real DNS and a real send, so it belongs in a slower scheduled job that runs after deploys or on a daily cron, alerting if Authentication-Results stops returning pass. Mixing a real-send deliverability probe into unit CI is how you get flaky builds and rate-limit bans.

A safe-by-default email testing checklist

Most email-in-development incidents trace back to a missing guardrail rather than a clever bug. The defaults below make the safe path the easy path.

  1. Block real sends outside production. In any non-production environment, route every outgoing message to a capture server or sandbox by configuration, not by remembering to. If NODE_ENV !== 'production', the SMTP host should point at the trap.
  2. Never hardcode production SMTP credentials in code or test fixtures. Keep them in environment variables that simply are not present in dev and CI.
  3. Test the message types that actually matter: transactional mail (welcome, password reset, verification), then the edge cases such as long display names, non-ASCII characters, and missing optional fields.
  4. Test failure paths too: an invalid recipient, a server that returns something other than 250, a slow or timing-out connection. Your code's behaviour on a rejected send is part of the feature.
  5. Verify authentication after every relevant change. SPF, DKIM, and DMARC are set-and-forget right up until someone adds a new sending service or changes the From domain, then they silently break placement.
  6. For context on why test messages must stay out of real inboxes at all: nearly half of global email is unwanted, and 47.27% of all email sent in 2024 was spam, per Kaspersky. Recipients and providers are already hostile to noise; a stray test blast trains spam filters against your real domain.

The through-line: separate the send test (local, deterministic, every commit) from the deliverability test (real, scheduled, after changes), and make the safe configuration the default one.

Treat "test email delivery" as two jobs, not one. The send test is mechanical and belongs on every commit. Point your app at a local capture server like Mailpit, assert on the SMTP 250 OK, and read the captured message back through its API so no test ever reaches a real person. The deliverability test is about placement and belongs on a slower cadence. Confirm SPF, DKIM, and DMARC pass and align by inspecting the Authentication-Results header on a real delivered message, and re-run it after any DNS or sender change.

For the recipient's-eye view (rendering, verification links, one-time codes), send to a disposable inbox you can read in a browser or drive through an API. A free, no-registration inbox like TempMailSpot covers the manual pass and, because it can send behind a CAPTCHA, the occasional reply-flow test that receive-only services can't. Wire the local loop into CI with a bounded poll-and-assert, keep the real-send deliverability probe in a scheduled job, and make blocking real sends outside production the default rather than a habit.

Frequently asked questions

Sources

  1. IETF / RFC Editor, RFC 5321: Simple Mail Transfer Protocol (opens in new tab) (2008)
  2. Mail.tm, Temp Mail API - Mail.tm (opens in new tab) (2026)
  3. Guerrilla Mail JSON API, Guerrilla Mail JSON API (opens in new tab) (2026)
  4. Maildrop, Maildrop Documentation (opens in new tab) (2026)
  5. disposable-email-domains (GitHub), disposable-email-domains: a list of disposable and temporary email address domains (opens in new tab) (2014)
  6. Kaspersky Securelist, Spam and phishing in 2024 (opens in new tab) (2025)
  7. IETF / RFC Editor, RFC 7208: Sender Policy Framework (SPF) for Authorizing Use of Domains in Email, Version 1 (opens in new tab) (2014)
  8. IETF / RFC Editor, RFC 6376: DomainKeys Identified Mail (DKIM) Signatures (opens in new tab) (2011)
  9. IETF / RFC Editor, RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC) (opens in new tab) (2015)
  10. Mailpit (GitHub), axllent/mailpit: An email and SMTP testing tool with API for developers (opens in new tab) (2026)
  11. Mailpit (GitHub wiki), Mailpit Wiki - Configuration (opens in new tab) (2026)
  12. MailHog (GitHub, mailhog/MailHog), GitHub - mailhog/MailHog: Web and API based SMTP testing (opens in new tab) (2020)
  13. Mailtrap, Email Sandbox Overview - Mailtrap Documentation (opens in new tab) (2026)

Recommended privacy tools

Independent privacy tools that complement a disposable inbox.

NordVPN

VPN

Encrypted tunneling across thousands of servers with an audited no-logs policy. For private browsing on untrusted networks.

Learn More

ExpressVPN

VPN

Consistently fast servers in 90 plus countries, an audited no-logs policy, and a clean app on every platform.

Learn More

Surfshark

VPN

Unlimited devices on one plan, with ad and tracker blocking built in. The budget pick that does not feel budget.

Learn More

Related articles