What the log proves
code: EAUTH- Nodemailer classified the failure as authentication. Do not begin by changing SPF or DMARC.
responseCode: 535- The SMTP server issued a permanent authentication-failure reply for this attempt.
command: AUTH PLAIN- DNS, TCP, the SMTP greeting, and any required TLS negotiation progressed far enough to attempt an auth mechanism.
Error verifying Nodemailer transport- With Payload's Nodemailer adapter, this can be the adapter's startup verification, not a customer message send.
A failed verify() does not prove that the application attempted a real message. A successful verify() proves DNS, connection, TLS, and authentication only; it does not prove that a sender address will be accepted or that a recipient will receive the message.
Why it appears during next build
Payload CMS loads payload.config while Next.js builds application routes. In @payloadcms/email-nodemailer 3.86.0, constructing the adapter calls Nodemailer's verify() unless skipVerify is true. The adapter catches and logs a verification error, so the log line alone does not prove that the build failed.
This creates two separate problems:
- The SMTP credentials or endpoint may be wrong.
- A deterministic static build now depends on a live external SMTP service.
Fix both. Do not hide invalid credentials merely to make the build green.
Check the rejected authentication boundary
- Credential type: for Mailgun, use a domain SMTP credential. A Mailgun API key or Control Panel password is not the SMTP password.
- Exact login: use the full SMTP username shown for the sending domain, commonly
postmaster@mailer.example.com. Do not substitute the visible From address. - Region: use
smtp.mailgun.orgfor a US-region domain andsmtp.eu.mailgun.orgfor an EU-region domain. Mailgun credentials are region-bound. - Port mode: use port 587 or 2525 with STARTTLS and
secure: false; use port 465 with implicit TLS andsecure: true. - Secret transport: confirm that the CI runner and deployed container receive the same bytes. A generated dotenv line can truncate or reinterpret passwords containing
#, whitespace, quotes, backslashes, dollar signs, or a newline.
Never print the password. Compare only the presence flag, byte length, and a short SHA-256 fingerprint calculated independently in CI and in the runtime container. Remove those diagnostics after the mismatch is resolved.
Remove SMTP from the build, then add a runtime gate
The adapter supports skipVerify. Use it to prevent configuration import from opening a live SMTP connection, then run an explicit verification command against the deployed runtime environment before declaring the release healthy.
const transport = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: Number(process.env.SMTP_PORT) === 465,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
export default nodemailerAdapter({
defaultFromAddress: "mailer@example.com",
defaultFromName: "Example",
transport,
skipVerify: true,
})
For the release gate, construct the same transport from the runtime environment and call await transport.verify() once. Fail the release or readiness check on EAUTH; log only the error class, SMTP code, response code, command, host label, port, and credential fingerprint. Do not expose this check as a public endpoint with detailed errors.
await nodemailer.createTransport(...) is unnecessary because createTransport() is synchronous. Removing the await improves clarity but does not fix a 535 response.
Prove a real send separately
- Pass the runtime
verify()gate with the intended host, port, user, and secret fingerprint. - Send one consented test message using the same application path and visible From identity as production.
- Record the provider message ID and SMTP submission result without storing the recipient in analytics.
- Check provider events for accepted, delivered, deferred, bounced, rejected, suppressed, or complained.
- Record recipient-server acceptance and inbox visibility as separate evidence.
If authentication succeeds but the sender is rejected, work on the verified sending domain or From identity. If submission succeeds but delivery fails, then investigate authentication alignment, reputation, suppression, receiver replies, and inbox placement.
A practical reading of the public case
The public build log in sooke-radio/sookelive issue 13 contains EAUTH, 535 Authentication failed, AUTH PLAIN, and Payload's transport-verification message. The repository config uses Mailgun SMTP and loads the mail adapter from payload.config. That is enough to prioritize the authentication and build-import boundaries above; it is not enough to identify which secret byte, Mailgun region, or account state is wrong.
Primary references
- Payload 3.86.0 Nodemailer adapter source
- Payload email adapter documentation
- Nodemailer SMTP transport and
verify() - Nodemailer error fields and
EAUTH - Mailgun SMTP submission, credentials, ports, and TLS
- Mailgun SMTP login format
- Mailgun US and EU service endpoints
- Next.js environment variable guidance