Guides

Test Nodemailer emails with Vitest

Test Nodemailer delivery from an Express API with Vitest.

Any backend that sends transactional email through Nodemailer can deliver to InboxTap instead of a real provider, making welcome emails, invite tokens, and OTPs assertable in integration tests. The complete runnable project — an Express API tested with Vitest — lives in examples/express-nodemailer.

Point Nodemailer at InboxTap

InboxTap accepts plain SMTP with no authentication and no STARTTLS, so the transport needs exactly three non-default options and no auth field:

import { createTransport } from "nodemailer";

const transport = createTransport({
  host: process.env.SMTP_HOST ?? "localhost",
  port: Number(process.env.SMTP_PORT ?? 1025),
  secure: false,
  ignoreTLS: true,
});

Everything else about the sending code stays production-shaped: from, to, subject, text, and html all pass through unchanged and are captured verbatim.

Test with Vitest

Start InboxTap programmatically on ephemeral ports in each spec file, then hand the assigned SMTP host and port to the application under test. Parallel spec files each get an isolated stack:

const server = await new InboxTapServer({ apiPort: 0, smtpPort: 0 }).start();
const inboxTap = new InboxTapClient({ baseUrl: server.apiUrl, domain: server.domain });
const mailer = createMailer({ host: server.smtpHost, port: server.smtpPort });

Each test creates its own inbox and picks the wait helper that matches what the email carries:

it("verifies a new user through the emailed link", async () => {
  const inbox = await inboxTap.createInbox({ alias: "signup" });

  await postJson(`${baseUrl}/signup`, { email: inbox.address });

  const link = await inbox.waitForLink({ subject: /welcome/i, contains: "/verify?token=" });
  const verify = await fetch(link);
  expect(verify.status).toBe(200);
});

The example's other specs show the rest of the toolkit: waitForMatch() extracts a custom invite token with pattern: /invite_[a-f0-9]{12}/, waitForCode() grabs a six-digit OTP with its default pattern, and waitForMessage() returns the full captured email for shape assertions on recipients, HTML, and extracted links.

Run the example

cd examples/express-nodemailer
npm install
npm test

The tests boot InboxTap and the Express app themselves on ephemeral ports — no other terminal needed, and no port conflicts with anything already running. For interactive exploration, run npx inboxtap in one terminal and npm run dev in another, trigger an email with curl, and read it back from http://localhost:8025/api/emails/latest.