- Fix 'transport.sendMail is not a function' in Payload CMS
Fix 'transport.sendMail is not a function' in Payload CMS
Resolve Nodemailer v7 incompatibility with @payloadcms/email-nodemailer—downgrade to Nodemailer v6 to restore Payload…

Need Help Making the Switch?
Moving to Next.js and Payload CMS? I offer advisory support on an hourly basis.
Book Hourly AdvisoryRelated Posts:
I was setting up email functionality for a Payload CMS v3 project when I hit a confusing error. The configuration looked perfect, the SMTP credentials were correct, but every time the forgot password flow triggered, I got this cryptic error:
TypeError: transport.sendMail is not a function
The frustrating part? The configuration was copied straight from the official Payload documentation. After digging through node_modules and testing different approaches, I discovered the root cause: a version incompatibility between nodemailer v7 and the @payloadcms/email-nodemailer adapter. This guide shows you exactly how to fix it.
The Problem: When Documentation Isn't Enough
Here's what my payload.config.ts looked like initially:
// File: payload.config.ts
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
import nodemailer from 'nodemailer'
export default buildConfig({
// ... other config
email: nodemailerAdapter({
defaultFromAddress: process.env.SMTP_FROM_ADDRESS || 'info@example.com',
defaultFromName: 'My App',
transport: nodemailer.createTransport({
host: process.env.SMTP_SERVER || 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: process.env.SMTP_LOGIN,
pass: process.env.SMTP_PASSWORD,
},
}),
}),
})
This configuration is textbook correct. The createTransport call returns a transport object, which should have a sendMail method. But when Payload tried to send an email, it crashed.
The error stack trace pointed to the adapter's internal code trying to call transport.sendMail, which apparently didn't exist. This didn't make sense because nodemailer.createTransport() always returns an object with sendMail.
Investigating the Root Cause
I created a simple reproduction script to test if the issue was with my Nodemailer installation:
// File: reproduce_issue.ts
import nodemailer from 'nodemailer';
const transport = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
auth: { user: 'test', pass: 'test' },
});
console.log('Transport has sendMail:', typeof transport.sendMail === 'function');
const spreadTransport = { ...transport };
console.log('Spread transport has sendMail:', typeof spreadTransport.sendMail === 'function');
Running this revealed something critical:
Nodemailer version: 7.0.12
Transport has sendMail: true
Spread transport has sendMail: false
HYPOTHESIS CONFIRMED: Spread operator kills sendMail.
The transport object has sendMail as a prototype method, not an own property. When you spread the object with { ...transport }, you only copy enumerable own properties. The prototype methods like sendMail are lost.
This meant that somewhere in the Payload adapter's code, the transport object was being spread or restructured in a way that broke the method chain. Checking the adapter's package.json confirmed it:
{
"name": "@payloadcms/email-nodemailer",
"version": "3.71.1",
"dependencies": {
"nodemailer": "7.0.12"
}
}
The adapter ships with nodemailer v7 as a dependency, but the internal implementation doesn't handle v7's object structure correctly.
The Solution: Downgrade to Nodemailer v6
The fix is straightforward: downgrade nodemailer to v6, which the adapter was originally designed to work with. Here's the step-by-step process.
Step 1: Remove Nodemailer v7
First, remove the existing nodemailer installation:
pnpm remove nodemailer @types/nodemailer
Step 2: Install Nodemailer v6
Install the v6 version explicitly:
pnpm add nodemailer@^6.9.16 pnpm add -D @types/nodemailer@^6.4.17
You'll see a peer dependency warning from next-auth if you're using it, since it expects v7. You can safely ignore this warning. The Payload adapter issue takes priority, and next-auth will work fine with v6.
Step 3: Verify Your Configuration
Your payload.config.ts should use the standard createTransport approach:
// File: payload.config.ts
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
import nodemailer from 'nodemailer'
export default buildConfig({
email: nodemailerAdapter({
defaultFromAddress: process.env.SMTP_FROM_ADDRESS || 'info@example.com',
defaultFromName: 'My App',
skipVerify: false,
transport: nodemailer.createTransport({
host: process.env.SMTP_SERVER || 'smtp.gmail.com',
port: 587,
secure: false, // Use STARTTLS on port 587
auth: {
user: process.env.SMTP_LOGIN,
pass: process.env.SMTP_PASSWORD,
},
}),
}),
})
The key here is passing the transport property with a fully constructed transport object. Don't use transportOptions as a workaround, even though the adapter supports it. With v6, the direct transport approach is more reliable.
Step 4: Restart Your Dev Server
Kill your development server and restart it to ensure the new version is loaded:
# Stop the server (Ctrl+C)
pnpm run dev
Now test the forgot password flow or any other email-sending functionality. The transport.sendMail is not a function error should be gone.
Why This Happens
The incompatibility stems from how Nodemailer v7 restructured its internal object model. In v6, the transport object's methods were more directly accessible, even after certain object operations. In v7, the methods are strictly prototype-based, which means any code that spreads or reconstructs the object will lose them.
The @payloadcms/email-nodemailer adapter was built and tested against v6. When v7 was released, the adapter's dependency was updated, but the internal implementation wasn't adjusted to handle the new structure. This is why the error is so confusing: the configuration is correct, the credentials work, but the runtime fails.
Conclusion
The transport.sendMail is not a function error in Payload CMS is caused by a version mismatch between nodemailer v7 and the @payloadcms/email-nodemailer adapter. By downgrading to nodemailer v6, you restore compatibility and get email functionality working as expected.
This is a perfect example of why pinning dependencies matters. Even when a library follows semantic versioning, internal changes can break integrations in non-obvious ways. If you're setting up Payload CMS email and hit this error, downgrade to v6 and you'll be back on track.
Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks,
Matija
📚 Comprehensive Payload CMS Guides
Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.


