mrkeyoor.com_
Wed 05 Aug 05:06 UTC
npmInfraupdated 05 Aug 2026

nodemailer

Nodemailer is the default way to send email from Node.js. You create a transport (usually SMTP, with connection details and credentials), then call sendMail with from, to, subject, and text or HTML. It handles the whole MIME layer for you: attachments, embedded images, unicode, address formatting, TLS upgrades. It has zero runtime dependencies and has been maintained by the same author for over a decade. What it does not do is deliverability: it hands your message to an SMTP server and the rest is that server's problem.

Verdict

For provider-agnostic transactional email over SMTP, Nodemailer is the boring, correct choice and has been for a decade. If you are on serverless or want deliverability handled for you, use a provider's HTTP SDK instead and skip SMTP entirely.

API stability5/5createTransport plus sendMail has been the whole story for years; major version bumps (v9 current) rarely change application code.
Docs4/5nodemailer.com covers transports, TLS, OAuth2, and DKIM well, but the GitHub README is mostly a troubleshooting FAQ and TypeScript users are sent to third-party types.
Maintenance4/5Very active (9.0.4 published and repo pushed August 2026, one open issue or PR), but it is effectively a single-maintainer project, which is a bus-factor risk to price in.
Ecosystem5/5About 19M weekly downloads, wrappers in every framework (Nest, Adonis, Ghost), and nearly every SMTP provider documents Nodemailer as the reference client.

Use it if

  • You send transactional email (password resets, receipts, alerts) through an SMTP server or a provider's SMTP endpoint
  • You want one API that works the same against Gmail, Office 365, Amazon SES SMTP, Mailgun, or your own Postfix
  • You need attachments, inline images, or custom headers without hand-building MIME messages
  • You want zero runtime dependencies in your mail path
Skip it if

Setup reality

npm install nodemailer is instant since there are no dependencies. The real setup is everything around it: getting SMTP credentials, picking port 465 versus 587, and learning that secure: true is only for 465 (the most common first-run TLS error). Expect firewall and ISP port-blocking surprises (ETIMEDOUT), antivirus TLS interception on dev machines, and a mandatory detour through app passwords or OAuth2 for Gmail. Types are a separate @types/nodemailer install. For local dev, the built-in Ethereal test account generator saves you from spamming yourself.

Patterns

Create an SMTP transportcreate-transport

import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  auth: { user: 'apikey-or-user', pass: process.env.SMTP_PASS }
});

secure: true is ONLY for port 465; on 587 leave it false and Nodemailer still upgrades to TLS via STARTTLS.

Send a basic messagesend-mail

const info = await transporter.sendMail({
  from: '"App Name" <no-reply@example.com>',
  to: 'user@example.com',
  subject: 'Your receipt',
  text: 'Plain text body',
  html: '<p>HTML body</p>'
});
console.log(info.messageId);

Always provide a text fallback alongside html; some spam filters score HTML-only mail worse.

Attach files from disk, Buffer, or URLsend-attachment

await transporter.sendMail({
  from: 'no-reply@example.com',
  to: 'user@example.com',
  subject: 'Invoice',
  text: 'Attached.',
  attachments: [
    { filename: 'invoice.pdf', path: '/tmp/invoice.pdf' },
    { filename: 'data.csv', content: Buffer.from('a,b\n1,2') }
  ]
});

path also accepts URLs and data URIs; content takes strings, Buffers, or streams.

Embed an image inside HTML with cidembed-inline-image

await transporter.sendMail({
  from: 'no-reply@example.com',
  to: 'user@example.com',
  subject: 'Welcome',
  html: '<img src="cid:logo@app"/> <p>Hello!</p>',
  attachments: [{ filename: 'logo.png', path: './logo.png', cid: 'logo@app' }]
});

The cid value must match the src exactly; embedded images count toward message size limits.

Verify SMTP credentials at startupverify-connection

try {
  await transporter.verify();
  console.log('SMTP connection OK');
} catch (err) {
  console.error('SMTP config broken:', err.message);
}

verify() checks connection and auth but cannot prove the server will accept mail for your from address.

Dev testing with an Ethereal inboxtest-account

const testAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport({
  host: 'smtp.ethereal.email',
  port: 587,
  auth: { user: testAccount.user, pass: testAccount.pass }
});
const info = await transporter.sendMail({ from: 'a@b.c', to: 'x@y.z', subject: 'test', text: 'hi' });
console.log(nodemailer.getTestMessageUrl(info));

Ethereal messages are never delivered; getTestMessageUrl gives you a web preview of exactly what was sent.

Send through Gmail with an app passwordgmail-app-password

const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: { user: 'me@gmail.com', pass: process.env.GMAIL_APP_PASSWORD }
});

Regular account passwords no longer work; create an app password (requires 2FA) and expect Gmail to rewrite the from address to the authenticated account.

Pool connections for many messagesconnection-pool

const transporter = nodemailer.createTransport({
  pool: true,
  host: 'smtp.example.com',
  port: 465,
  secure: true,
  maxConnections: 5,
  maxMessages: 100,
  auth: { user: 'user', pass: 'pass' }
});
// later, when the queue is done:
transporter.close();

Pooling reuses TCP connections across sendMail calls; call close() or the process may hang on open sockets.

Pin TLS options for picky serversfix-tls-errors

const transporter = nodemailer.createTransport({
  host: 'smtp.example.com',
  port: 587,
  secure: false,
  tls: {
    minVersion: 'TLSv1.2',
    rejectUnauthorized: true
  }
});

Setting rejectUnauthorized: false silences self-signed cert errors but disables verification; do it only against servers you control.

Sign outgoing mail with DKIMdkim-signing

const transporter = nodemailer.createTransport({
  host: 'smtp.example.com',
  port: 465,
  secure: true,
  auth: { user: 'user', pass: 'pass' },
  dkim: {
    domainName: 'example.com',
    keySelector: 'mail',
    privateKey: process.env.DKIM_PRIVATE_KEY
  }
});

The public key must be published as a DNS TXT record at mail._domainkey.example.com or receivers will fail the signature.

Alternatives

PackageRegistryPick it when
resendnpmYou want an HTTP API with a modern SDK and are fine committing to one provider
@sendgrid/mailnpmYou are already on SendGrid and want their templates and analytics with an official SDK
postmarknpmTransactional email where deliverability metrics matter more than SMTP flexibility