Skip to main content
Technology & EngineeringEmail Services225 lines

Nodemailer

Send email with Nodemailer in Node.js. Use this skill when the project needs to

Quick Summary27 lines
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 lines
Paste into your CLAUDE.md or agent config

Nodemailer 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

Get CLI access →