• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Payload
  4. 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…

12th January 2026·Updated on:22nd February 2026·MŽMatija Žiberna·
Payload
Fix 'transport.sendMail is not a function' in Payload CMS

Need Help Making the Switch?

Moving to Next.js and Payload CMS? I offer advisory support on an hourly basis.

Book Hourly Advisory

Related Posts:

  • •PayloadCMS in Production? Turn Off Push and Go Migration‑Only (Zero‑Downtime Guide)
  • •Payload CMS Auth Plugins: Which One Should You Use?
  • •Build Payload CMS Custom Array Fields: Spreadsheet UI

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.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

PayloadCMS in Production? Turn Off Push and Go Migration‑Only (Zero‑Downtime Guide)
PayloadCMS in Production? Turn Off Push and Go Migration‑Only (Zero‑Downtime Guide)

9th August 2025

Payload CMS Auth Plugins: Which One Should You Use?
Payload CMS Auth Plugins: Which One Should You Use?

6th March 2026

Build Payload CMS Custom Array Fields: Spreadsheet UI
Build Payload CMS Custom Array Fields: Spreadsheet UI

11th January 2026

Table of Contents

  • The Problem: When Documentation Isn't Enough
  • Investigating the Root Cause
  • The Solution: Downgrade to Nodemailer v6
  • Step 1: Remove Nodemailer v7
  • Step 2: Install Nodemailer v6
  • Step 3: Verify Your Configuration
  • Step 4: Restart Your Dev Server
  • Why This Happens
  • Conclusion
On this page:
  • The Problem: When Documentation Isn't Enough
  • Investigating the Root Cause
  • The Solution: Downgrade to Nodemailer v6
  • Why This Happens
  • Conclusion
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved