Back to Strapi

@strapi/provider-email-nodemailer

packages/providers/email-nodemailer/README.md

5.45.023.6 KB
Original Source

@strapi/provider-email-nodemailer

A feature-rich Nodemailer email provider for Strapi with support for DKIM, OAuth2, connection pooling, calendar invitations, newsletters, and more.

Features

CategoryFeatures
SendingPriority, custom headers, attachments, embedded images, AMP4Email
SecurityDKIM signing, OAuth2, requireTLS, file/URL access restrictions
PerformanceConnection pooling, rate limiting
DeliverabilityList-Unsubscribe headers (Gmail/Outlook), DSN bounce tracking, custom envelope
Rich ContentCalendar invitations (iCalendar), AMP4Email interactive emails
ConnectivitySOCKS/HTTP proxy support, NTLM and custom auth mechanisms
UtilitiesRFC 5322/2047/6531 email address parsing and formatting

Resources

Installation

bash
# using yarn
yarn add @strapi/provider-email-nodemailer

# using npm
npm install @strapi/provider-email-nodemailer --save

Example

Path - config/plugins.js

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/

Development mode

You can override the default configurations for specific environments. E.g. for NODE_ENV=development in config/env/development/plugins.js:

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.

Custom authentication mechanisms

It is also possible to use custom authentication methods. Here is an example for a NTLM authentication:

js
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]',
      },
    },
  },
});

Usage

:warning: The Shipper Email (or defaultfrom) may also need to be changed in the Email Templates tab on the admin panel for emails to send properly

To send an email from anywhere inside Strapi:

js
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:

FieldDescription
Core
fromEmail address of the sender
toComma separated list or an array of recipients
ccComma separated list or an array of recipients
bccComma separated list or an array of recipients
replyToEmail address to which replies are sent
senderAddress for the Sender: field (for "on behalf of" emails)
subjectSubject of the email
Content
textPlaintext version of the message
htmlHTML version of the message
watchHtmlApple Watch specific HTML version
ampAMP4Email content for interactive emails
attachmentsArray of attachment objects. See: https://nodemailer.com/message/attachments/
alternativesArray of alternative content (e.g. Markdown alongside HTML)
Headers & Meta
headersCustom SMTP headers object (e.g. { 'X-Custom': 'value' })
priorityEmail priority: 'high', 'normal', or 'low'
messageIdCustom Message-ID (random generated if not set)
dateCustom Date value (current UTC if not set)
xMailerControl X-Mailer header (false to remove, string to override)
Threading
inReplyToMessage-ID of the email being replied to
referencesMessage-ID list this email references (array or space-separated string)
Encoding
textEncodingForce content-transfer-encoding: 'quoted-printable' or 'base64'
encodingEncoding for the message content
normalizeHeaderKeyFunction to normalize header key casing
Advanced
icalEventCalendar event invitation (iCalendar format)
listRFC 2369 List-* headers - enables one-click unsubscribe in Gmail/Outlook
envelopeCustom SMTP envelope for bounce handling (MAIL FROM / RCPT TO)
dkimPer-message DKIM signing options (overrides transport-level DKIM)
attachDataUrlsConvert data: URIs in HTML to embedded CID attachments (true/false)
dsnDelivery Status Notification - request bounce/success reports
authPer-message OAuth2 credentials for multi-user sending
Security
disableUrlAccessFail if content tries to load from a URL (true/false)
disableFileAccessFail if content tries to load from a file path (true/false)
Raw MIME
rawPre-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.

Sending with priority and custom headers

js
await strapi
  .plugin('email')
  .service('email')
  .send({
    to: '[email protected]',
    subject: 'Urgent',
    text: 'Please respond ASAP',
    priority: 'high',
    headers: {
      'X-Custom-Header': 'my-value',
    },
  });

Calendar invitations

js
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`,
    },
  });

Newsletter with List-Unsubscribe

When sending newsletters, include List-Unsubscribe headers so Gmail and Outlook show a one-click "Unsubscribe" button:

js
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',
    },
  });

Delivery Status Notifications (DSN)

Request bounce reports or delivery confirmations:

js
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]',
    },
  });

Custom SMTP Envelope (Bounce Handling)

Control the MAIL FROM address independently from the visible From header, useful for tracking bounces:

js
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]',
    },
  });

AMP4Email (Interactive Emails)

Send interactive AMP-powered emails (supported by Gmail):

js
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>`,
  });

Email threading (replies & conversations)

js
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]>'],
  });

Sending on behalf of (Sender field)

js
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',
});

Per-message DKIM signing

Override transport-level DKIM settings for a specific message:

js
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,
    },
  });

Alternative content formats

Provide multiple representations of the same content (email clients pick the best match):

js
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',
      },
    ],
  });

Raw MIME passthrough

Send a pre-built MIME message directly (skips all message generation):

js
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',
  });

Embedded images

js
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',
      },
    ],
  });

Advanced configuration

OAuth2 authentication

For services like Gmail or Outlook, you can use OAuth2 instead of passwords:

js
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'),
      },
    },
  },
});

Per-message OAuth2 (multi-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:

js
// 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]',
      },
    },
  },
});
js
// 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.

Connection pooling

For better performance when sending multiple emails:

js
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]',
      },
    },
  },
});

DKIM signing

Add DKIM signatures to improve email deliverability:

js
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]',
      },
    },
  },
});

Rate limiting

Limit the number of emails sent per time interval to avoid being flagged as spam:

js
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]',
      },
    },
  },
});

Proxy support

Route SMTP connections through a SOCKS or HTTP proxy:

js
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.

Require TLS

Force TLS encryption and refuse to send if the server doesn't support it:

js
providerOptions: {
  host: env('SMTP_HOST'),
  port: 587,
  requireTLS: true, // Fail if STARTTLS is not available
  auth: { user: env('SMTP_USERNAME'), pass: env('SMTP_PASSWORD') },
},

Security options

Restrict file and URL access at the transport level (applies to all messages):

js
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:

js
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.

Provider methods

verify

Verify your SMTP configuration without sending an email:

js
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.

isIdle

Check if the transporter has available capacity (useful with connection pooling):

js
if (emailProvider.isIdle()) {
  // Safe to send more emails
}

close

Close all connections gracefully (recommended when using connection pooling):

js
// On application shutdown
emailProvider.close();

Email Address Utilities

This package includes RFC-compliant utilities for parsing and formatting email addresses.

Import

js
import {
  parseEmailAddress,
  formatEmailAddress,
  parseMultipleEmailAddresses,
  isValidEmail,
  decodeRfc2047,
  encodeRfc2047Base64,
} from '@strapi/provider-email-nodemailer/utils';

Parsing Email Addresses

Parse email addresses in various RFC 5322 formats:

js
// 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: '...' }

Formatting Email Addresses

Create properly formatted email address strings:

js
// 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]>'

Multiple Addresses

Parse comma-separated email addresses (handles quoted strings with commas):

js
parseMultipleEmailAddresses('[email protected], "Doe, John" <[email protected]>');
// [
//   { name: null, email: '[email protected]', ... },
//   { name: 'Doe, John', email: '[email protected]', ... }
// ]

RFC 2047 Encoding/Decoding

Handle MIME encoded-words for non-ASCII characters:

js
// 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==?='

Validation

js
isValidEmail('[email protected]'); // true
isValidEmail('invalid'); // false

Normalization

All email addresses are automatically normalized to lowercase when parsed or formatted (per RFC 5321 section 2.4):

js
parseEmailAddress('[email protected]');
// { name: null, email: '[email protected]', original: '[email protected]' }

formatEmailAddress('Admin', '[email protected]');
// 'Admin <[email protected]>'

normalizeEmail('[email protected]');
// '[email protected]'

Supported RFC Standards

RFCDescription
RFC 5321SMTP protocol - email address case-insensitive normalization
RFC 5322Internet Message Format (name <email>, quoted strings, comments)
RFC 2047MIME encoded-words (=?charset?encoding?text?=)
RFC 2369List-* headers (List-Unsubscribe, List-Help, etc.)
RFC 3461Delivery Status Notifications (DSN)
RFC 6376DomainKeys Identified Mail (DKIM) signatures
RFC 6531Internationalized Email (UTF-8 in local parts)
RFC 6532Internationalized Email Headers (UTF-8 in header fields)

Troubleshooting

Check your firewall to ensure that requests are allowed. If it doesn't work with

js
port: 465,
secure: true

try using

js
port: 587,
secure: false

to test if it works correctly.