Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Payload + Nodemailer: Free and Extensible Email Integration

Published On
Payload + Nodemailer
Payload + Nodemailer
Email is one of those prerequisites to every project that you never remember to handle thoroughly, and it can be easy or hard depending on what tools you’re working with. Payload tries to make this as easy as possible.

With Payload, you don't have to abandon what you're already familiar with. Stick to your trusty SMTP if you like to keep things simple. Or, if you're a fan of Gmail or Outlook, go ahead and integrate them with ease. You can even bring in other powerful email service tools like SendGrid, Resend, HubSpot and more.


Integrating email with Payload is free, flexible and highly extensible. No matter what kind of emails you need to send – from newsletters, transactional and marketing emails, to those crucial authentication emails – we've got you covered.


In this post, we’ll walk through the process of configuring email with Payload and cover everything you need to get up and running. Let’s dive in!

How It Works

Payload utilizes Nodemailer to produce a versatile email transporter which can then be used anywhere in your application.

For those who are new to Nodemailer, it is a powerful module in the Node.js ecosystem that greatly simplifies the process of sending email. We recommend taking a look at the Nodemailer docs if you want to learn more.

If you have used Nodemailer before, this process will be familiar. Simply create a new transport and pass it to the email property in  your payload.init() function.

Once you add your email configuration, you can send emails from anywhere in your application simply by calling Payload.sendEmail({}). Neat, huh?

Configuration

The email property takes the following options:

  • fromName* - required
  • fromAddress* - required
  • logMockCredentials - will output your credentials to the console on startup
  • transportOptions - pass in your options and let Payload create the transport for you
  • transport - manual create a transporter using nodemailer.createTransport({})

There are two ways to create a Nodemailer-compatible transport:

  1. Use by passing transportOptions and let Payload do it for you
  2. Pass a full transport if you want to create it yourself or are using a separate package to do this for you

If you are on version 1.7.0 or later, you can add your email options directly in to your payload.config.

For versions < 1.7.0 you will need to pass your email options to payload.init() which is usually found in the server.ts file. After adding your email options, your payload.init() or payload.config should look something like this:

1
export const email = {
2
fromName: 'Admin',
3
fromAddress: 'admin@example.com',
4
logMockCredentials: true,
5
6
// Use either transportOptions or transport, you will not need both
7
transportOptions: {},
8
transport: {},
9
}
10
11
const start = async (): Promise<void> => {
12
await payload.init({
13
secret: process.env.PAYLOAD_SECRET,
14
mongoURL: process.env.MONGODB_URI,
15
express: app,
16
17
// Here we pass in your email object
18
email,
19
})
20
21
app.listen(8000)
22
}
23
24
start()

Mock email handler

If you do not provide a transport or transportOptions, Payload will initialize an ethereal capture service. Ethereal is a free email caching service which captures all outbound emails. Using this service can be really useful for testing emails when you’re working in a development environment.

To use this service, logMockCredentials must be set to true. This will output the ethereal credentials to your console after startup, you will then use these to login to ethereal.email and view any emails that are sent during development.

transportOptions

Pass any valid Nodemailer options to transportOptions and Payload will create the transporter for you.

You can use transportOptions to configure:

1. SMTP

1
export const email = {
2
fromName: 'Admin',
3
fromAddress: 'admin@example.com',
4
transportOptions: {
5
host: process.env.SMTP_HOST,
6
auth: {
7
user: process.env.SMTP_USER,
8
pass: process.env.SMTP_PASS
9
},
10
port: 587,
11
secure: false,
12
}
13
}
14

2. An email service 

Nodemailer will automatically provide the connection details (host, port, etc) for several well known email services. For example if you want to use Gmail, you simply need to provide the service name like this:

1
export const email = {
2
fromName: 'Admin',
3
fromAddress: 'admin@example.com',
4
transportOptions: {
5
service: 'gmail',
6
auth: {
7
user: process.env.GMAIL_USER,
8
pass: process.env.GMAIL_PASS,
9
},
10
}
11
}
12

3. An external transport,  a nodemailer plugin or similar

Nodemailer has created packages that integrate popular email vendors for you, such as SendGrid:

1
import nodemailerSendgrid from 'nodemailer-sendgrid'
2
3
export const email = {
4
fromName: 'Admin',
5
fromAddress: 'admin@example.com',
6
transportOptions: nodemailerSendgrid({
7
apiKey: process.env.SENDGRID_API_KEY,
8
}),
9
}
10

transport

This option allows you to manually create a transport, this supports SMTP and email services. 

You can make use of nodeMailer.createTransport({}) for support of well known email services and browse this list of options that you can define.

More examples of using nodeMailer.createTransport({}) can be found in the Nodemailer documentation.

1
import nodemailer from 'nodemailer'
2
import payload from 'payload'
3
4
const transport = await nodemailer.createTransport({
5
service: 'outlook',
6
auth: {
7
user: process.env.OUTLOOK_USER,
8
pass: process.env.OUTLOOK_PASS,
9
},
10
})
11
12
const email = {
13
fromName: 'Admin',
14
fromAddress: 'admin@example.com',
15
logMockCredentials: true,
16
// Pass your custom transport
17
transport,
18
}
19

Sending Email

Once you have configured your transporter, you can start sending emails from anywhere inside your Payload project by calling payload.sendEmail({})

payload.sendEmail({}) takes properties of to, from, subject, and html.

1
import payload from 'payload'
2
3
payload.sendEmail({
4
from: 'sender@example.com',
5
to: 'receiver@example.com',
6
subject: 'Message subject title',
7
html: '<p>HTML based message</p>',
8
})
9

Dynamic Email Content

There are many ways to include data directly from your project into your emails. Whether it is using hooks, making API requests, fetching data from globals or anything else you can think of.

For example, sending order details when there is a new submission to the Orders collection:

1
import payload from 'payload'
2
import type { CollectionConfig } from 'payload/types'
3
4
const Orders: CollectionConfig = {
5
slug: 'orders',
6
hooks: {
7
afterChange: [
8
({ doc, operation, req }) => {
9
const { customerEmail, items, total } = doc
10
if (operation === 'create') {
11
req.payload.sendEmail({
12
to: customerEmail,
13
from: 'sender@example.com',
14
subject: 'Welcome To Payload',
15
html: `<h1>Thank you for your order!</h1>
16
<p>Here is your order summary:</p>
17
<ul>
18
${items.map(item => `<li>${item.name} - ${item.price}</li>`)}
19
</ul>
20
<p>Total: ${total}</p>
21
`,
22
})
23
}
24
},
25
],
26
},
27
fields: [],
28
}
29
30
export default Orders

Automatically trigger email dispatch

Payload’s collection and field hooks allow you to define specific conditions which will trigger an email to be sent. 

Like sending an email every time you receive a newsletter signup:

1
import payload from 'payload'
2
import type { CollectionConfig } from 'payload/types'
3
4
const NewsletterSignups: CollectionConfig = {
5
slug: 'newsletter-signups',
6
hooks: {
7
afterChange: [
8
({ doc, operation, req }) => {
9
if (operation === 'create') {
10
req.payload.sendEmail({
11
to: doc.email,
12
from: 'sender@example.com',
13
subject: 'You have joined our newsletter list!',
14
html: '<p>Thanks for signing up</p>',
15
})
16
}
17
},
18
],
19
},
20
fields: [],
21
}
22
23
export default NewsletterSignups

Or sending a welcome email to new users:

1
import payload from 'payload'
2
import type { CollectionConfig } from 'payload/types'
3
4
const Users: CollectionConfig = {
5
slug: 'users',
6
auth: true,
7
hooks: {
8
afterChange: [
9
({ doc, operation }) => {
10
if (operation === 'create') {
11
payload.sendEmail({
12
to: doc.email,
13
from: 'sender@example.com',
14
subject: 'Welcome To Payload',
15
html: '<b>Hey there!</b><br/>Welcome to Payload!',
16
})
17
}
18
},
19
],
20
},
21
fields: [],
22
}
23
24
export default Users

Authentication Emails

Payload makes auth-enabled collections super simple to integrate with email by handling forgotPassword and verify for you. 

Each auth-enabled collection has forgotPassword and verify options that you can pass generateEmailSubject and generateEmailHTML functions to. The function accepts one argument containing { req, token, user }.

1
import payload from 'payload'
2
import type { CollectionConfig } from 'payload/types'
3
4
const Users: CollectionConfig = {
5
slug: 'users',
6
auth: {
7
verify: {
8
generateEmailSubject: () => 'Verify your email',
9
generateEmailHTML: ({ token }) => `<p>Verify your account here ${process.env.PAYLOAD_PUBLIC_SITE_URL}/verify?token=${token}.</p>`,
10
},
11
forgotPassword: {
12
generateEmailSubject: () => 'Reset your password',
13
generateEmailHTML: ({ token }) => `<p>Reset your password here ${process.env.PAYLOAD_PUBLIC_SITE_URL}/reset-password?token=${token}.</p>`,
14
},
15
},
16
},
17
fields: [],
18
}
19
20
export default Users

Templates

Payload doesn't ship a default HTML templating engine, so you are free to add whatever suits you best. Make your email templates highly dynamic by using Handlebars, a templating language that combines HTML, plain text and expressions. The expressions are included in the html template surrounded by double curly braces.

1
<table border="0" width="100%">
2
<tbody>
3
<td>
4
<!-- HEADLINE -->
5
<h1>{{headline}}</h1>
6
</td>
7
</tr>
8
<tr>
9
<td>
10
<!-- CONTENT -->
11
{{{content}}}
12
</td>
13
</tr>
14
</tbody>
15
</table>
16

Here is a simple but powerful example that ties everything together in one function:

1
import fs from 'fs'
2
import Handlebars from 'handlebars'
3
import inlineCSS from 'inline-css'
4
import path from 'path'
5
import payload from 'payload'
6
7
const template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf8')
8
const getHTML = Handlebars.compile(template)
9
10
export const sendEmailWithTemplate = async (args): Promise<any> => {
11
const { from, to, subject, data } = args
12
13
const templateData = {
14
...data,
15
apiURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
16
siteURL: process.env.PAYLOAD_PUBLIC_SITE_URL,
17
}
18
const preInlinedCSS = getHTML(templateData)
19
20
const html = await inlineCSS(preInlinedCSS, {
21
url: ' ',
22
removeStyleTags: false,
23
})
24
25
await payload.sendEmail({
26
from,
27
to,
28
subject,
29
html,
30
})
31
32
return null
33
}
34

The template.html file that is being used in sendEmailWithTemplate can be any HTML file of your choice. You can find this template in our email example, feel free to use this as a starter template and add your own custom CSS.

Example 

We have an example repo where you can see these code snippets being used and try them out in real time. 

Wrap Up

Payload provides a super flexible solution for integrating email functionality, in production and development. Using NodeMailer allows email sending to be much simpler and cleaner. A basic setup can be configured with just a few lines of code, and you can absolutely extend it to accommodate more advanced features.

With this post and the example repo you will have tools you need to start working with email in Payload - as always, feel free to reach out if you have any questions or feedback.

Learn More

Like what we're doing? Give us a star on GitHub

We're trying to change the CMS status quo by delivering editors with a great experience, but first and foremost, giving developers a CMS that they don't absolutely despise working with. All of our new features are meant to be extensible and work simply and sanely.