This time, our debugging adventure takes us deep into the world of SMTP, where an elusive Invalid HELO error threatened to derail email sending. Let’s dive in!
The Mystery Begins
It was a Friday evening, and I was ready to log off and enjoy the weekend. But just as I was packing up, the customer-facing team reported an issue: a user couldn’t send emails via SMTP from our platform.
Sighing, I pulled out my laptop, checked the logs, and found this cryptic error:
550 Requested action not taken: Invalid HeloHost (#5.5.0)
"responseCode":550, "command":"MAIL FROM"
Not exactly the “Have a great weekend!” message I was hoping for.
The First Hypothesis
This was a new error for me, so I did some quick Googling. My first suspicion? The user might have IP restrictions on their SMTP server.
We asked them to whitelist our server’s IP, hoping that would fix it. But when we retried, the error persisted. Time for a deeper dive.
Digging Deeper
Things rarely work as expected on the first try. So, I turned to Google and ChatGPT for insights. Here’s what I learned:
-
Error Code
550
: A permanent failure response. -
Message Meaning: The recipient’s mail server rejected the
MAIL FROM
command due to an issue with the HELO/EHLO handshake. -
Potential Fix: Ensure the sending server uses a fully qualified domain name (FQDN) in the HELO/EHLO command (e.g.,
EHLO mymailserver.example.com
).
Understanding the SMTP Handshake
To get a better grasp of the issue, I reviewed the SMTP handshake process:
-
Client Introduces Itself (EHLO/HELO)
- The sender’s server initiates the connection.
-
Server Responds
- The recipient’s server acknowledges and lists supported features.
-
Authentication (If Required)
- If needed, credentials are exchanged.
-
Email Transaction Begins
- The sender specifies
MAIL FROM
andRCPT TO
. - The email content is sent (
DATA
command).
- The sender specifies
-
Server Confirms & Closes Connection
- The recipient server acknowledges (
250 OK
). - The sender closes the connection (
QUIT
command).
- The recipient server acknowledges (
More details? Check the official spec here.
The Breakthrough
Armed with this knowledge, I dug into our SMTP implementation. We were using Nodemailer, so I checked how it sends the EHLO command.
Inside the _actionGreeting function, I found this:
this._sendCommand('EHLO ' + this.name);
Then, I traced how this.name
is set:
this.name = this.options.name || this._getHostname();
Since we hadn’t explicitly set options.name
, it defaulted to _getHostname()
, which is defined as:
_getHostname() {
let defaultHostname;
try {
defaultHostname = os.hostname() || '';
} catch (err) {
defaultHostname = 'localhost';
}
if (!defaultHostname || !defaultHostname.includes('.')) {
defaultHostname = '[127.0.0.1]';
}
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(defaultHostname)) {
defaultHostname = `[${defaultHostname}]`;
}
return defaultHostname;
}
The Culprit: Private IP in EHLO
Our server was running inside a Kubernetes pod, where _getHostname()
returned the pod’s private IP.
According to RFC 5321, SMTP servers may verify the domain name in the EHLO command against the client’s IP, but they must not reject a message solely on a failed verification:
An SMTP server MAY verify that the domain name argument in the EHLO
command actually corresponds to the IP address of the client.
However, if the verification fails, the server MUST NOT refuse to
accept a message on that basis.
However, for some reason, this particular customer’s server was rejecting the handshake, leading to the Invalid HeloHost error.
The Fix
To resolve the issue, we explicitly set a public hostname for EHLO by passing a valid domain when initializing Nodemailer:
const transporter = nodemailer.createTransport({
host: "smtp.example.com",
port: 587,
secure: false,
auth: {
user: "user@example.com",
pass: "password"
},
name: "public.hostname.com" // <-- Fix: Use a valid hostname
});
With this change, the SMTP connection worked flawlessly, and emails started sending without any issues!
Case Closed
Did it solve the problem? Yes but it also introduced a new one! I'll unravel that mystery in the next post.
Found this helpful? Clap 👏 and follow for more debugging adventures!
Top comments (0)