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.
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.
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
- You are sending bulk or marketing email: Nodemailer gives you no list management, templates, analytics, or bounce handling, and raw SMTP deliverability is entirely on you
- You deploy to serverless or edge platforms where outbound SMTP ports are throttled or blocked; an HTTP-API provider SDK like resend or @sendgrid/mail is the path of least pain there
- You expected first-party TypeScript support: types live in the community @types/nodemailer package and the maintainer explicitly redirects TS issues to it
- You plan to authenticate to Gmail with a plain password: that era is over, you need an app password or OAuth2, and the README itself says Gmail either works well or does not work at all
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
| Package | Registry | Pick it when |
|---|---|---|
| resend | npm | You want an HTTP API with a modern SDK and are fine committing to one provider |
| @sendgrid/mail | npm | You are already on SendGrid and want their templates and analytics with an official SDK |
| postmark | npm | Transactional email where deliverability metrics matter more than SMTP flexibility |