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.
createTestAccount(), send, open the preview URLEthereal 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.
extracted.otp.best, classified links
Your regex on the body
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:
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:
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