Nodemailer integration

Test Nodemailer email with InboxTap

InboxTap accepts the same SMTP message that a Nodemailer application sends to a delivery provider, but captures it in bounded local memory instead of relaying it. The official fixture supplies a ready transport and dynamic SMTP settings so tests can exercise real message construction without fixed ports.

Configure plain local SMTP correctly

A manual Nodemailer transport for InboxTap uses the local host and port, secure: false, ignoreTLS: true, and no auth object. secure: false means that TLS is not active at connection time; by itself, it does not stop Nodemailer from attempting a later STARTTLS upgrade.

InboxTap disables authentication and STARTTLS because it is a loopback-only capture server. Keep those development settings separate from the authenticated, encrypted transport used for production delivery.

Prefer the verified fixture

startInboxTapFixture() selects free SMTP and API ports, starts the server, creates a Nodemailer transport, calls verify(), and checks the InboxTap health endpoint before returning. Its close() method is idempotent and cleans up both the transport and listeners.

The fixture transport is convenient for template-level integration tests. To test the application-owned mailer, configure that mailer with inboxTap.smtp and send through the application API instead.

email.test.ts
import { startInboxTapFixture } from "inboxtap/fixtures";

const inboxTap = await startInboxTapFixture();

try {
  const inbox = await inboxTap.createInbox();

  await inboxTap.transport.sendMail({
    from: "app@local.test",
    to: inbox.address,
    subject: "Account",
    text: "https://app.local.test/verify",
  });

  const email = await inbox.waitForMessage();
  expect(email.envelope.to).toContain(inbox.address);
} finally {
  await inboxTap.close();
}

Understand what transport verification proves

Nodemailer's verify() checks DNS resolution, the TCP connection, any TLS upgrade, and authentication without sending a message. It does not prove that a server will accept a particular envelope sender or message.

Keep at least one real sendMail() transaction in the test. InboxTap stores the message only after SMTP DATA completes successfully, so the resulting CapturedEmail represents an accepted delivery rather than transport readiness alone.

Assert the envelope and content

Use toHaveRecipient() when delivery routing matters because it compares the SMTP envelope rather than a display address parsed from the message header. Use toContainLink() for extracted HTTP or HTTPS URLs and waitForCode() for a numeric value.

Reach for waitForMessage() when the test needs the subject, normalized headers, text, HTML, raw source, or every extracted link. Avoid printing the body or token-bearing URLs in routine diagnostics.

Close every resource owner

Place fixture.close() in a finally block when managing the explicit lifecycle. If the application creates its own Nodemailer transport, the application fixture must close that transport before InboxTap shuts down.

Runner-native adapters automate this ordering for Bun, Vitest, and Playwright. Keeping ownership explicit prevents open sockets from holding the test process alive after a failure.

Connect a real application sender

Continue with a runnable Express and Vitest workflow that tests links, custom tokens, OTPs, recipients, and headers.

Read the Nodemailer guide