Ethereal catches what you send.
MailFixture proves what arrived.

Ethereal is the Nodemailer team's free fake SMTP service: point your transporter at it, send to anyone, nothing is delivered, and you get a preview link. It's a sandbox for the sending side. MailFixture is an inbox on the receiving side. Different jobs, and this page is about which one your test actually needs.

ethereal for previews · real MX for assertions
ETHEREAL IS RIGHT WHEN
MAILFIXTURE IS RIGHT WHEN

Two sides of the same wire

Ethereal is a sending sandbox. nodemailer.createTestAccount() hands you SMTP credentials with no signup; your app sends through smtp.ethereal.email; whatever address you put in to, the message lands in that account's own mailbox and goes nowhere else. That's a genuinely good design for development — you can address mail to made-up people and nobody gets spammed, and getTestMessageUrl(info) gives you a link to look at exactly what Nodemailer produced.

Notice what the typical Ethereal test asserts, though. The Nodemailer docs' Jest example checks info.accepted — that Ethereal's SMTP server took the message. That is true the moment your transporter works, and it stays true when the template renders undefined, when staging still points at the sandbox provider, when your real ESP rejects the domain, or when the DKIM record you rotated last week no longer matches. Ethereal is structurally blind to all of that, because the message never leaves its box. Public accounts can't even receive external mail — inbound was switched off after abuse, and the help page says mail sent to those addresses bounces.

MailFixture is an inbox on the receiving side. Inboxes are real addresses on domains with real MX records; your app keeps its real transport and real provider, the test hands it a MailFixture address, and the assertion is on what arrived. Nothing in Nodemailer changes except the recipient. The wider, layer-by-layer version of this decision — mocks, local traps, shared mailboxes, inbound inboxes — is the fake SMTP vs. real test inbox guide.

Side by side, honestly

Different categories, so several rows aren't a contest. Rows where Ethereal wins say so.
MAILFIXTUREETHEREAL
What it is Receiving inbox API for tests Fake SMTP service for sending
Price Free tier, paid from $15/mo Free — Ethereal wins
Signup Account + API key None — one call or one click, Ethereal wins
What your app does Sends normally, through its real provider Sends through smtp.ethereal.email instead
Tests the production delivery path Yes — provider, DNS, templates No — the message never leaves Ethereal
Receives mail from the outside Yes — real MX No on public accounts; needs an EmailEngine subscription
Isolation for parallel tests An inbox per test One account per process by default; new accounts are cheap
Reading messages from code REST API, JS + Python SDKs, MCP IMAP / POP3, or the preview URL
Waiting for a message Long-poll, held server-side Poll over IMAP
OTP / link extraction extracted.otp.best, classified links Your regex on the body
SPF/DKIM/DMARC + spam score Automatic on every message No
Retention 3–30 days by plan A few hours; 100MB per account
Rate limit Plan quotas, 429 + Retry-After 200 messages/hour per public account
Preview links Dashboard, session-gated Public URL, auth encoded in the link
Runs from CI with zero infrastructure Yes Yes
ethereal facts from ethereal.email (home, faq, help), nodemailer.com/guides/testing-with-ethereal, and emailengine.app — checked oct 27, 2026. inbound access requires an emailengine subscription ($1,450/yr, unlimited mailboxes — a self-hosted email api, not a testing product). ethereal is a good sandbox; this table is about fit.

The Ethereal test, and the test it can't be

This is the shape the Nodemailer docs suggest for Jest: one account for the process, send, assert the transport accepted it, log a preview link for a human:

before · Jest + Ethereal
const account = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport({
  host: account.smtp.host, port: account.smtp.port, auth: { /* … */ },
});

const info = await transporter.sendMail({
  to: 'newuser@example.com', subject: 'Welcome!', text: 'Thanks for signing up.',
});
expect(info.accepted).toContain('newuser@example.com'); // Ethereal said 250
console.log(nodemailer.getTestMessageUrl(info)); // a human can look later

The same intent against MailFixture. Nodemailer isn't in the test at all — the app sends however it sends in that environment — and the assertion is on the message that arrived:

after · Jest + MailFixture
const mail = new MailFixture(); // reads MAILFIXTURE_API_KEY
const inbox = await mail.createInbox({ ttlSeconds: 900 });

await app.signUp({ email: inbox.address }); // your app, your real transport

const msg = await inbox.waitForMessage({ match: 'subject:Welcome', timeout: 30_000 });
expect(msg.textBody).toContain('Thanks for signing up');
expect(msg.auth?.dmarc?.result).toBe('pass'); // only meaningful on the real provider path

Fair questions

Is Ethereal Email a real inbox?
No. Ethereal is a fake SMTP service: your app sends through smtp.ethereal.email and every message is caught in the sending account's own mailbox, whatever recipient you addressed. Public accounts cannot receive mail from the outside — inbound is disabled and external mail bounces. Receiving is enabled only for accounts created by EmailEngine subscribers.
Should I replace Ethereal with MailFixture?
Only for the tests that need to prove an email arrived. Ethereal is free, needs no signup, and is the quickest way to eyeball what Nodemailer produced during development — keep it for that. MailFixture is for the suite: an inbox per test, a server-held wait, and the OTP or link as a typed field, with your app sending through its real provider.
Can I read Ethereal messages from a test?
Yes, over IMAP or POP3 with the generated credentials, or by opening the preview URL from getTestMessageUrl. There is no REST API for reading messages, no wait primitive, and no extraction — the test has to speak IMAP, poll, and regex the body itself. Messages are kept for a few hours.
Does MailFixture work with Nodemailer?
Yes, with nothing to configure in Nodemailer. MailFixture is on the receiving side: your app keeps its real transport, you give it a MailFixture address as the recipient, and the test reads what arrived. The only thing that changes is the to field.
100 messages/mo free · no card · keep ethereal for previews
Start free Read the quickstart