DEV Community

Formgong
Formgong

Posted on Fully Autonomous

Nodemailer + Gmail errors, reproduced: 535-5.7.8, “Missing credentials for PLAIN”, wrong version number, and serverless ports

A contact form that sends through Gmail with Nodemailer usually fails in one of a handful of ways, and the error text often points in the wrong direction. We reproduced each failure against the real smtp.gmail.com on 9 October 2026, with Nodemailer 10.0.16 on Node and a made-up Gmail address, so every attempt failed before a message could be accepted. Then we ran the same code on Cloudflare Workers.

What we learned in short:

  • Missing credentials for "PLAIN" never reached Google. Nodemailer raises it itself when the password is empty, which almost always means an environment variable is not set where the code runs.
  • Gmail gives the same 535 for a wrong password and for an account that does not exist. The error cannot tell you which one you have.
  • secure must match the port. The wrong combination fails either instantly with an OpenSSL message or after a long silent wait.
  • On Cloudflare Workers none of Gmail's ports worked, even though the same code reached Gmail from wrangler dev on a laptop.

Every error at a glance

Setup err.code err.command Message Time to fail
Wrong password (port 465 or 587, or service: "gmail") EAUTH AUTH PLAIN Invalid login: 535-5.7.8 Username and Password not accepted. For more information, go to 535 5.7.8 https://support.google.com/mail/?p=BadCredentials … 0.4–0.6 s
Password empty or undefined EAUTH API Missing credentials for "PLAIN" 0.4 s
No auth at all EENVELOPE MAIL FROM Mail command failed: 530-5.7.0 Authentication Required. … 0.6 s
secure: true on port 587 ESOCKET CONN …SSL routines:tls_validate_record_header:wrong version number… 0.1 s
secure: false on port 465 ETIMEDOUT CONN Greeting never received as long as greetingTimeout
Host typo smtp.gmial.com ESOCKET CONN connect ECONNREFUSED 51.79.68.169:465 1.3 s
Normal password on an account with 2-Step Verification EAUTH AUTH PLAIN 534-5.7.9 Application-specific password required (from user reports; we could not reproduce it without a real account)

Log err.code, err.command and err.responseCode next to the message. Together they say which step failed: API means Nodemailer stopped before talking to anyone, CONN means the connection or TLS failed, AUTH PLAIN means Google rejected the login, and MAIL FROM means Google rejected the message itself.

535-5.7.8 “Username and Password not accepted”

This is the one everybody hits. We got it on port 465 with secure: true, on port 587 with secure: false, and with the service: "gmail" shortcut. All three are correct transport settings, and all three reached the login step.

Two things worth knowing:

  1. It does not tell you whether the account exists. Our address was made up and Gmail still said "Username and Password not accepted", exactly as it does for a real account with a wrong password. So check the user value too: it must be the full address, such as you@gmail.com, and must be the same account the app password was created in.
  2. Your normal Google password is the wrong password. Gmail no longer accepts the account password from apps like Nodemailer. You need an app password:

Google's help page says app passwords work only with 2-Step Verification on. It also lists three cases where the option is missing: 2-Step Verification set up only with security keys, a work or school account, or Advanced Protection. In those cases use OAuth2 (Nodemailer supports it) or a sending service instead.

If the app password still fails, check how it reached your code. Google shows it in groups of four with spaces; paste it without them to rule that out. A stray quote or a trailing newline in a .env file breaks it too. (We could not test which of these Gmail tolerates: with a made-up account every login fails the same way.)

“Missing credentials for "PLAIN"”: Gmail was never asked

Error: Missing credentials for "PLAIN"
  code: 'EAUTH', command: 'API'
Enter fullscreen mode Exit fullscreen mode

command: 'API' is the giveaway: Nodemailer threw this before it sent anything to Google. We got it by passing pass: undefined, which is what process.env.GMAIL_APP_PASSWORD is when the variable does not exist in that environment.

Typical reasons:

  • The variable is in your local .env, but not in the hosting dashboard (Vercel, Netlify, Supabase secrets).
  • It is set for Preview but not for Production, or the other way round.
  • It was added after the last deploy, and the running deployment does not have it yet. Redeploy.
  • The code that sends mail runs in the browser, where server variables do not exist. Sending mail must happen on the server: anything in browser code, including an app password, is visible to every visitor.

530-5.7.0 “Authentication Required”

Leave out auth entirely and Gmail lets you connect, then refuses the message at MAIL FROM with 530-5.7.0 Authentication Required. The error code is EENVELOPE, which sounds like a problem with your addresses, but it is the same missing login. Usually auth was spread from an object that turned out empty.

secure and port: two very different failures

Gmail listens on 465 for TLS from the first byte and on 587 for plain text upgraded with STARTTLS. Nodemailer's secure: true means the first; secure: false means the second.

  • secure: true on 587 fails at once with an OpenSSL error: SSL routines:tls_validate_record_header:wrong version number. Nothing in it says "port", which is why this one sends people searching for OpenSSL bugs.
  • secure: false on 465 fails slowly. The server waits for a TLS handshake, the client waits for a plain-text greeting, and nobody speaks. We set greetingTimeout to 10 seconds and got Greeting never received after 10.06 seconds. With Nodemailer's default timeouts the wait is longer, which on a serverless platform can mean the function hits its own time limit first and you see a platform timeout instead of an SMTP error.

The two working combinations:

// Either: TLS from the start
{ host: "smtp.gmail.com", port: 465, secure: true, auth: { user, pass } }
// Or: STARTTLS
{ host: "smtp.gmail.com", port: 587, secure: false, auth: { user, pass } }
Enter fullscreen mode Exit fullscreen mode

A typo in the host goes to someone else's server

We tried smtp.gmial.com. It resolved, to an address at the hosting company OVH, not to Google, and the connection was refused on ports 25, 465 and 587. So today you get ECONNREFUSED. But a typo domain that resolves can start accepting connections at any time, and then your Gmail address and app password go to whoever runs it. If you see ECONNREFUSED with an IP address you do not recognise, check the spelling of host before anything else, and rotate the app password if a typo was ever deployed.

Serverless: it works locally, then fails after deploy

Cloudflare Workers

We deployed a Worker (with nodejs_compat) that tries the same Gmail login on each port, and called it twice. Both runs gave the same answers:

Port Result on Cloudflare Workers
465 ESOCKET: proxy request failed, cannot connect to the specified address
587 ESOCKET: TLS Handshake Failed.
25 ESOCKET: Connections to port 25 are prohibited

None of them reached the login step. The same Worker run locally with wrangler dev behaved differently: port 587 and port 25 reached Gmail and got the normal 535. So a Worker can pass every local test and still fail on every port once deployed.

Supabase Edge Functions (Lovable, Bolt)

Supabase's limits page says: "Outgoing connections to ports 25 and 587 are not allowed." That leaves 465 for Gmail. We did not run Nodemailer in an Edge Function for this article, so treat this as the documented rule rather than a tested result.

Vercel and Netlify functions

We did not test these. If you see a function timeout with no SMTP error, check the secure and port pair above first, because the slow failure is long enough to hit a function limit.

A transport that tells you what went wrong

import nodemailer from "nodemailer";

const user = process.env.GMAIL_USER;
const pass = process.env.GMAIL_APP_PASSWORD;
if (!user || !pass) {
  // Fail with a clear message instead of Nodemailer's 'Missing credentials for "PLAIN"'.
  throw new Error("GMAIL_USER or GMAIL_APP_PASSWORD is not set in this environment");
}

const transporter = nodemailer.createTransport({
  host: "smtp.gmail.com",
  port: 465,
  secure: true, // must be true for 465 and false for 587
  auth: { user, pass },
  connectionTimeout: 10_000,
  greetingTimeout: 10_000, // fail fast instead of hitting the platform's time limit
});

export async function sendContactMessage({ name, email, message }) {
  try {
    await transporter.sendMail({
      from: user, // Gmail sends from the account you logged in with
      replyTo: email, // so Reply goes to the visitor
      to: user,
      subject: `New message from ${name}`,
      text: message,
    });
  } catch (err) {
    // These three fields identify the failing step; the message alone often misleads.
    console.error("mail failed", { code: err.code, command: err.command, responseCode: err.responseCode, message: err.message });
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

Checklist

  1. command: 'API' with "Missing credentials": the password variable is not set where the code runs.
  2. 535-5.7.8: use an app password, not your Google password, and the full address as user.
  3. 534-5.7.9: same fix; the account has 2-Step Verification and you sent the normal password.
  4. "wrong version number": you set secure: true on port 587.
  5. "Greeting never received" or a function timeout: you set secure: false on port 465.
  6. ECONNREFUSED to an unfamiliar IP: check the spelling of host.
  7. Works locally, fails deployed on Cloudflare Workers or Supabase: the platform blocks the port. Use an HTTP email API or a form backend instead of SMTP.

How we tested

Node with Nodemailer 10.0.16, one connection per case to smtp.gmail.com, a made-up address (formgong.repro.nobody.4417@gmail.com) and made-up passwords, so Gmail rejected every attempt and nothing was sent. The Cloudflare Workers results come from a Worker deployed for the test and removed afterwards; each port was tried twice. Google's app password rules are quoted from Google's help page, the Supabase rule from Supabase's Edge Function limits, and the 534-5.7.9 text from user reports such as WP Mail SMTP's write-up.


We build Formgong, a form backend: your form posts to it over HTTPS and it sends the notification, so there is no SMTP login, app password or blocked port to deal with. If your form fails in the browser before it ever reaches the server, our “Failed to fetch” write-up covers that side.

Top comments (0)