Nodemailer
Send email with Nodemailer in Node.js. Use this skill when the project needs to
You are an email operations specialist who integrates Nodemailer into Node.js projects. Nodemailer is the de facto standard for sending email from Node.js via SMTP. It works with any SMTP server — managed services (SES, SendGrid SMTP, Gmail) or self-hosted (Postfix, Dovecot). ## Key Points - Use pooled connections for sending more than a few emails per minute - Always include both HTML and plain-text content - Verify the SMTP connection at startup with `transporter.verify()` - Use OAuth2 instead of app passwords for Gmail and Outlook - Close the transporter when your application shuts down - Use connection timeouts to prevent hanging sends - Build a send queue with retry logic for production use - Creating a new transporter for every email — reuse transporters - Using Gmail SMTP for production transactional email — rate limits are low - Not handling rejected recipients from `info.rejected` - Sending high-volume email without connection pooling - Storing SMTP passwords in code instead of environment variables ## Quick Example ```bash npm install nodemailer ```
skilldb get email-services-skills/NodemailerFull skill: 225 linesNodemailer Email Integration
You are an email operations specialist who integrates Nodemailer into Node.js projects. Nodemailer is the de facto standard for sending email from Node.js via SMTP. It works with any SMTP server — managed services (SES, SendGrid SMTP, Gmail) or self-hosted (Postfix, Dovecot).
Core Philosophy
SMTP is universal
Every email service supports SMTP. Nodemailer lets you switch providers by changing transport configuration. No vendor-specific SDK lock-in.
Bring your own everything
Nodemailer sends email. It doesn't manage contacts, track opens, handle bounces, or provide analytics. You build those layers. This is the right choice when you need full control or when budget constraints rule out managed APIs.
Connection pooling for throughput
For high-volume sending, use pooled SMTP connections. A pool of persistent connections avoids the overhead of handshake per message and dramatically improves throughput.
Setup
Install
npm install nodemailer
Basic SMTP transport
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: false, // true for 465, false for 587 (STARTTLS)
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
Key Techniques
Simple send
const info = await transporter.sendMail({
from: '"App" <noreply@yourdomain.com>',
to: 'user@example.com',
subject: 'Your password reset link',
text: 'Reset: https://...',
html: '<p>Click <a href="...">here</a> to reset.</p>',
});
console.log('Message sent:', info.messageId);
Attachments
await transporter.sendMail({
from: '"App" <noreply@yourdomain.com>',
to: 'user@example.com',
subject: 'Your invoice',
html: '<p>Please find your invoice attached.</p>',
attachments: [
{ filename: 'invoice.pdf', path: '/tmp/invoice-123.pdf' },
{ filename: 'logo.png', path: '/assets/logo.png', cid: 'logo@company' },
],
});
Embedded images (CID)
await transporter.sendMail({
from: '"App" <noreply@yourdomain.com>',
to: 'user@example.com',
subject: 'Welcome',
html: '<img src="cid:logo@company" /><p>Welcome!</p>',
attachments: [
{ filename: 'logo.png', path: '/assets/logo.png', cid: 'logo@company' },
],
});
Pooled connections (high volume)
const transporter = nodemailer.createTransport({
pool: true,
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
maxConnections: 5,
maxMessages: 100, // Messages per connection before reconnect
});
OAuth2 (Gmail, Outlook)
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: 'you@gmail.com',
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
},
});
Common SMTP configurations
AWS SES:
const transporter = nodemailer.createTransport({
host: `email-smtp.${process.env.AWS_REGION}.amazonaws.com`,
port: 587,
auth: {
user: process.env.SES_SMTP_USER,
pass: process.env.SES_SMTP_PASS,
},
});
SendGrid SMTP:
const transporter = nodemailer.createTransport({
host: 'smtp.sendgrid.net',
port: 587,
auth: {
user: 'apikey',
pass: process.env.SENDGRID_API_KEY,
},
});
Postmark SMTP:
const transporter = nodemailer.createTransport({
host: 'smtp.postmarkapp.com',
port: 587,
auth: {
user: process.env.POSTMARK_SERVER_TOKEN,
pass: process.env.POSTMARK_SERVER_TOKEN,
},
});
HTML templating with Handlebars
import Handlebars from 'handlebars';
import fs from 'fs';
const source = fs.readFileSync('./templates/welcome.hbs', 'utf-8');
const template = Handlebars.compile(source);
const html = template({ name: 'Alice', url: 'https://...' });
await transporter.sendMail({
from: '"App" <noreply@yourdomain.com>',
to: 'user@example.com',
subject: 'Welcome',
html,
});
Verify connection
try {
await transporter.verify();
console.log('SMTP connection verified');
} catch (err) {
console.error('SMTP connection failed:', err);
}
Error Handling
try {
const info = await transporter.sendMail(message);
// info.accepted — addresses that accepted the message
// info.rejected — addresses that rejected the message
// info.messageId — the Message-ID header
} catch (err) {
if (err.responseCode === 421) {
// Service temporarily unavailable — retry with backoff
} else if (err.responseCode >= 500) {
// Permanent failure — do not retry
}
}
Best Practices
- Use pooled connections for sending more than a few emails per minute
- Always include both HTML and plain-text content
- Verify the SMTP connection at startup with
transporter.verify() - Use OAuth2 instead of app passwords for Gmail and Outlook
- Close the transporter when your application shuts down
- Use connection timeouts to prevent hanging sends
- Build a send queue with retry logic for production use
Anti-Patterns
- Creating a new transporter for every email — reuse transporters
- Using Gmail SMTP for production transactional email — rate limits are low
- Not handling rejected recipients from
info.rejected - Sending high-volume email without connection pooling
- Storing SMTP passwords in code instead of environment variables
- Not implementing retry logic — SMTP connections are inherently unreliable
- Using Nodemailer when a managed API (Resend, SendGrid, etc.) would be simpler
Install this skill directly: skilldb add email-services-skills
Related Skills
AWS Ses
Send email at scale with Amazon SES (Simple Email Service). Use this skill when
Brevo
Send transactional and marketing email with Brevo (formerly Sendinblue). Use this
Courier
Send transactional notifications including email with Courier. Use this skill when
Customerio
Send transactional and marketing email with Customer.io. Use this skill when the
Email Deliverability
Optimize email deliverability across any provider. Use this skill when the project
Loops
Send transactional and marketing email with Loops. Use this skill when the project