packages/providers/email-nodemailer/README.md
A feature-rich Nodemailer email provider for Strapi with support for DKIM, OAuth2, connection pooling, calendar invitations, newsletters, and more.
| Category | Features |
|---|---|
| Sending | Priority, custom headers, attachments, embedded images, AMP4Email |
| Security | DKIM signing, OAuth2, requireTLS, file/URL access restrictions |
| Performance | Connection pooling, rate limiting |
| Deliverability | List-Unsubscribe headers (Gmail/Outlook), DSN bounce tracking, custom envelope |
| Rich Content | Calendar invitations (iCalendar), AMP4Email interactive emails |
| Connectivity | SOCKS/HTTP proxy support, NTLM and custom auth mechanisms |
| Utilities | RFC 5322/2047/6531 email address parsing and formatting |
# using yarn
yarn add @strapi/provider-email-nodemailer
# using npm
npm install @strapi/provider-email-nodemailer --save
Path - config/plugins.js
module.exports = ({ env }) => ({
// ...
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST', 'smtp.example.com'),
port: env('SMTP_PORT', 587),
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
// ... any custom nodemailer options
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
// ...
});
Check out the available options for nodemailer: https://nodemailer.com/about/
You can override the default configurations for specific environments. E.g. for
NODE_ENV=development in config/env/development/plugins.js:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: 'localhost',
port: 1025,
ignoreTLS: true,
},
},
},
});
The above setting is useful for local development with maildev.
It is also possible to use custom authentication methods. Here is an example for a NTLM authentication:
const nodemailerNTLMAuth = require('nodemailer-ntlm-auth');
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST', 'smtp.example.com'),
port: env('SMTP_PORT', 587),
auth: {
type: 'custom',
method: 'NTLM',
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
customAuth: {
NTLM: nodemailerNTLMAuth,
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
:warning: The Shipper Email (or defaultfrom) may also need to be changed in the
Email Templatestab on the admin panel for emails to send properly
To send an email from anywhere inside Strapi:
await strapi.plugin('email').service('email').send({
to: '[email protected]',
from: '[email protected]',
subject: 'Hello world',
text: 'Hello world',
html: `<h4>Hello world</h4>`,
});
The following fields are supported:
| Field | Description |
|---|---|
| Core | |
| from | Email address of the sender |
| to | Comma separated list or an array of recipients |
| cc | Comma separated list or an array of recipients |
| bcc | Comma separated list or an array of recipients |
| replyTo | Email address to which replies are sent |
| sender | Address for the Sender: field (for "on behalf of" emails) |
| subject | Subject of the email |
| Content | |
| text | Plaintext version of the message |
| html | HTML version of the message |
| watchHtml | Apple Watch specific HTML version |
| amp | AMP4Email content for interactive emails |
| attachments | Array of attachment objects. See: https://nodemailer.com/message/attachments/ |
| alternatives | Array of alternative content (e.g. Markdown alongside HTML) |
| Headers & Meta | |
| headers | Custom SMTP headers object (e.g. { 'X-Custom': 'value' }) |
| priority | Email priority: 'high', 'normal', or 'low' |
| messageId | Custom Message-ID (random generated if not set) |
| date | Custom Date value (current UTC if not set) |
| xMailer | Control X-Mailer header (false to remove, string to override) |
| Threading | |
| inReplyTo | Message-ID of the email being replied to |
| references | Message-ID list this email references (array or space-separated string) |
| Encoding | |
| textEncoding | Force content-transfer-encoding: 'quoted-printable' or 'base64' |
| encoding | Encoding for the message content |
| normalizeHeaderKey | Function to normalize header key casing |
| Advanced | |
| icalEvent | Calendar event invitation (iCalendar format) |
| list | RFC 2369 List-* headers - enables one-click unsubscribe in Gmail/Outlook |
| envelope | Custom SMTP envelope for bounce handling (MAIL FROM / RCPT TO) |
| dkim | Per-message DKIM signing options (overrides transport-level DKIM) |
| attachDataUrls | Convert data: URIs in HTML to embedded CID attachments (true/false) |
| dsn | Delivery Status Notification - request bounce/success reports |
| auth | Per-message OAuth2 credentials for multi-user sending |
| Security | |
| disableUrlAccess | Fail if content tries to load from a URL (true/false) |
| disableFileAccess | Fail if content tries to load from a file path (true/false) |
| Raw MIME | |
| raw | Pre-built MIME message (skips message generation, envelope must be set) |
Every field listed above is explicitly allowlisted. Unknown properties are silently dropped to prevent injection.
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Urgent',
text: 'Please respond ASAP',
priority: 'high',
headers: {
'X-Custom-Header': 'my-value',
},
});
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Meeting Invitation',
text: 'You are invited to a meeting',
icalEvent: {
method: 'REQUEST',
content: `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:20260130T100000Z
DTEND:20260130T110000Z
SUMMARY:Team Meeting
END:VEVENT
END:VCALENDAR`,
},
});
When sending newsletters, include List-Unsubscribe headers so Gmail and Outlook show a one-click "Unsubscribe" button:
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Weekly Newsletter',
html: '<h1>This week in tech...</h1>',
list: {
unsubscribe: {
url: 'https://example.com/unsubscribe?id=123',
comment: 'Unsubscribe',
},
help: '[email protected]?subject=help',
},
});
Request bounce reports or delivery confirmations:
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Important document',
text: 'Please confirm receipt',
dsn: {
id: 'msg-unique-123',
return: 'headers',
notify: ['success', 'failure'],
recipient: '[email protected]',
},
});
Control the MAIL FROM address independently from the visible From header, useful for tracking bounces:
await strapi
.plugin('email')
.service('email')
.send({
from: 'Newsletter <[email protected]>',
to: '[email protected]',
subject: 'Newsletter',
text: 'Hello!',
envelope: {
from: '[email protected]',
to: '[email protected]',
},
});
Send interactive AMP-powered emails (supported by Gmail):
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Interactive Email',
text: 'Fallback for non-AMP clients',
html: '<p>Fallback for non-AMP clients</p>',
amp: `<!doctype html>
<html ⚡4email>
<head>
<meta charset="utf-8">
<style amp4email-boilerplate>body{visibility:hidden}</style>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>
<p>This is an interactive AMP email!</p>
</body>
</html>`,
});
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Re: Project Update',
text: 'Thanks for the update!',
inReplyTo: '<[email protected]>',
references: ['<[email protected]>', '<[email protected]>'],
});
await strapi.plugin('email').service('email').send({
from: 'CEO <[email protected]>',
sender: '[email protected]',
to: '[email protected]',
subject: 'Quarterly Report',
text: 'Please find the report attached',
});
Override transport-level DKIM settings for a specific message:
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Signed Email',
text: 'This email has a specific DKIM signature',
dkim: {
domainName: 'special.example.com',
keySelector: 'mail2026',
privateKey: process.env.DKIM_PRIVATE_KEY_SPECIAL,
},
});
Provide multiple representations of the same content (email clients pick the best match):
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Multi-format Email',
text: 'Plain text version',
html: '<p>HTML version</p>',
alternatives: [
{
contentType: 'text/x-web-markdown',
content: '**Markdown** version',
},
],
});
Send a pre-built MIME message directly (skips all message generation):
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'ignored',
text: 'ignored',
envelope: {
from: '[email protected]',
to: '[email protected]',
},
raw: 'From: [email protected]\r\nTo: [email protected]\r\nSubject: Raw\r\n\r\nPre-built body',
});
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Newsletter',
html: '<p>Logo: </p>',
attachments: [
{
filename: 'logo.png',
path: '/path/to/logo.png',
cid: 'logo@company',
},
],
});
For services like Gmail or Outlook, you can use OAuth2 instead of passwords:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
type: 'OAuth2',
user: env('SMTP_USER'),
clientId: env('OAUTH_CLIENT_ID'),
clientSecret: env('OAUTH_CLIENT_SECRET'),
refreshToken: env('OAUTH_REFRESH_TOKEN'),
},
},
settings: {
defaultFrom: env('SMTP_USER'),
defaultReplyTo: env('SMTP_USER'),
},
},
},
});
You can send emails on behalf of different users through a single transporter. Configure the transporter with shared OAuth2 credentials, then pass user-specific tokens per message:
// config/plugins.js - shared transporter with OAuth2
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
type: 'OAuth2',
clientId: env('OAUTH_CLIENT_ID'),
clientSecret: env('OAUTH_CLIENT_SECRET'),
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
// Send as a specific user
await strapi
.plugin('email')
.service('email')
.send({
to: '[email protected]',
subject: 'Hello from user',
text: 'Sent on behalf of a specific user',
auth: {
user: '[email protected]',
refreshToken: userRefreshToken,
accessToken: userAccessToken,
},
});
See nodemailer OAuth2 documentation for details.
For better performance when sending multiple emails:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST'),
port: 465,
secure: true,
pool: true,
maxConnections: 5,
maxMessages: 100,
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
Add DKIM signatures to improve email deliverability:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST'),
port: 587,
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
dkim: {
domainName: 'example.com',
keySelector: 'mail',
privateKey: env('DKIM_PRIVATE_KEY'),
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
Limit the number of emails sent per time interval to avoid being flagged as spam:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST'),
port: 465,
secure: true,
pool: true,
maxConnections: 5,
maxMessages: 100,
rateDelta: 1000, // Time interval in ms (1 second)
rateLimit: 5, // Max messages per rateDelta interval
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
Route SMTP connections through a SOCKS or HTTP proxy:
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST'),
port: 465,
secure: true,
proxy: env('SMTP_PROXY', 'socks5://127.0.0.1:1080'), // or 'http://proxy:3128'
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
For SOCKS proxy, install the socks package: yarn add socks.
Force TLS encryption and refuse to send if the server doesn't support it:
providerOptions: {
host: env('SMTP_HOST'),
port: 587,
requireTLS: true, // Fail if STARTTLS is not available
auth: { user: env('SMTP_USERNAME'), pass: env('SMTP_PASSWORD') },
},
Restrict file and URL access at the transport level (applies to all messages):
module.exports = ({ env }) => ({
email: {
config: {
provider: 'nodemailer',
providerOptions: {
host: env('SMTP_HOST'),
port: 587,
auth: {
user: env('SMTP_USERNAME'),
pass: env('SMTP_PASSWORD'),
},
disableFileAccess: true,
disableUrlAccess: true,
},
settings: {
defaultFrom: '[email protected]',
defaultReplyTo: '[email protected]',
},
},
},
});
You can also set these per message when processing content from untrusted sources:
await strapi.plugin('email').service('email').send({
to: '[email protected]',
subject: 'User-generated content',
html: userProvidedHtml,
disableUrlAccess: true,
disableFileAccess: true,
});
Security note: This provider uses an explicit allowlist for all message fields. Unknown or unsupported properties are silently dropped, preventing property injection attacks.
Verify your SMTP configuration without sending an email:
const emailProvider = strapi.plugin('email').provider;
try {
await emailProvider.verify();
console.log('SMTP connection is working');
} catch (error) {
console.error('SMTP configuration error:', error.message);
}
This tests DNS resolution, TCP connection, TLS upgrade (if applicable), and authentication.
Check if the transporter has available capacity (useful with connection pooling):
if (emailProvider.isIdle()) {
// Safe to send more emails
}
Close all connections gracefully (recommended when using connection pooling):
// On application shutdown
emailProvider.close();
This package includes RFC-compliant utilities for parsing and formatting email addresses.
import {
parseEmailAddress,
formatEmailAddress,
parseMultipleEmailAddresses,
isValidEmail,
decodeRfc2047,
encodeRfc2047Base64,
} from '@strapi/provider-email-nodemailer/utils';
Parse email addresses in various RFC 5322 formats:
// Simple email
parseEmailAddress('[email protected]');
// { name: null, email: '[email protected]', original: '...' }
// Name with angle brackets
parseEmailAddress('John Doe <[email protected]>');
// { name: 'John Doe', email: '[email protected]', original: '...' }
// Quoted name (RFC 5322)
parseEmailAddress('"Doe, John" <[email protected]>');
// { name: 'Doe, John', email: '[email protected]', original: '...' }
// RFC 2047 encoded name (non-ASCII characters)
parseEmailAddress('=?UTF-8?B?TcO8bGxlcg==?= <[email protected]>');
// { name: 'Müller', email: '[email protected]', original: '...' }
// Comment format (RFC 5322)
parseEmailAddress('[email protected] (Support Team)');
// { name: 'Support Team', email: '[email protected]', original: '...' }
Create properly formatted email address strings:
// Simple format
formatEmailAddress('John Doe', '[email protected]');
// 'John Doe <[email protected]>'
// Auto-quotes special characters
formatEmailAddress('Doe, John', '[email protected]');
// '"Doe, John" <[email protected]>'
// Auto-encodes non-ASCII characters (RFC 2047)
formatEmailAddress('Müller', '[email protected]');
// '=?UTF-8?B?TcO8bGxlcg==?= <[email protected]>'
// Skip encoding if needed
formatEmailAddress('Müller', '[email protected]', { encodeNonAscii: false });
// 'Müller <[email protected]>'
Parse comma-separated email addresses (handles quoted strings with commas):
parseMultipleEmailAddresses('[email protected], "Doe, John" <[email protected]>');
// [
// { name: null, email: '[email protected]', ... },
// { name: 'Doe, John', email: '[email protected]', ... }
// ]
Handle MIME encoded-words for non-ASCII characters:
// Decode Base64 or Quoted-Printable
decodeRfc2047('=?UTF-8?B?U3RyYXBp?=');
// 'Strapi'
decodeRfc2047('=?UTF-8?Q?M=C3=BCller?=');
// 'Müller'
// Encode for MIME headers
encodeRfc2047Base64('Müller');
// '=?UTF-8?B?TcO8bGxlcg==?='
isValidEmail('[email protected]'); // true
isValidEmail('invalid'); // false
All email addresses are automatically normalized to lowercase when parsed or formatted (per RFC 5321 section 2.4):
parseEmailAddress('[email protected]');
// { name: null, email: '[email protected]', original: '[email protected]' }
formatEmailAddress('Admin', '[email protected]');
// 'Admin <[email protected]>'
normalizeEmail('[email protected]');
// '[email protected]'
| RFC | Description |
|---|---|
| RFC 5321 | SMTP protocol - email address case-insensitive normalization |
| RFC 5322 | Internet Message Format (name <email>, quoted strings, comments) |
| RFC 2047 | MIME encoded-words (=?charset?encoding?text?=) |
| RFC 2369 | List-* headers (List-Unsubscribe, List-Help, etc.) |
| RFC 3461 | Delivery Status Notifications (DSN) |
| RFC 6376 | DomainKeys Identified Mail (DKIM) signatures |
| RFC 6531 | Internationalized Email (UTF-8 in local parts) |
| RFC 6532 | Internationalized Email Headers (UTF-8 in header fields) |
Check your firewall to ensure that requests are allowed. If it doesn't work with
port: 465,
secure: true
try using
port: 587,
secure: false
to test if it works correctly.