← All posts Infrastructure

Your GoDaddy VPS Is Lying About Outbound SMTP

Every outbound SMTP port on a GoDaddy VPS is transparently redirected to a local mail relay. The connection succeeds, so the usual diagnostics all say everything is fine — and none of your mail authenticates.

Outbound email from a GoDaddy VPS fails in an unusually annoying way. The connection succeeds. The port is open. The banner comes back. Every diagnostic you would normally reach for says the network is fine — and your mail still does not send as the mailbox you configured.

The reason is that you are not connected to the server you dialed.

What is actually happening

GoDaddy's shared and managed VPS hosts transparently NAT-redirect every outbound SMTP port — 25, 465 and 587 — to a local Exim relay running on the host itself. This is standard cPanel anti-spam posture and it is not documented anywhere you would think to look.

The critical detail is that this is a redirect, not a block. A firewall drop would refuse the connection, your probe would time out, and you would immediately know what kind of problem you had. Instead the connection is accepted, a greeting banner is returned, and everything downstream of "can I open a socket?" reports success.

So the naive question — is port 587 open? — returns yes, and points you in entirely the wrong direction.

Proving it takes about five seconds

The tell is the banner. Open a socket to several unrelated mail providers and read the greeting from each:

$targets = [
    ['smtp.office365.com',    587],
    ['smtp.office365.com',     25],
    ['smtp.office365.com',    465],
    ['smtp-mail.outlook.com', 587],
    ['smtp.gmail.com',        587],
    ['localhost',              25],
    ['graph.microsoft.com',   443],
];

foreach ($targets as [$host, $port]) {
    $fp = @fsockopen($host, $port, $errno, $errstr, 5.0);
    if (!$fp) { printf("%-28s :%-4s CLOSED (%s)\n", $host, $port, $errstr); continue; }
    stream_set_timeout($fp, 2);
    printf("%-28s :%-4s %s\n", $host, $port, trim((string) fgets($fp, 512)));
    fclose($fp);
}

Run against our box on May 12, 2026, every single smtp.* target — Microsoft's, Outlook's, Google's — answered with the same Exim 4.99.2 banner, naming a host ending in secureserver.net.

Three different companies' mail servers cannot all be running the same Exim instance on GoDaddy's network. Every one of those connections terminated in the same place: a relay one hop away, on the same machine.

Only port 443 behaved normally.

Why the usual fixes all fail

Once you know what is happening, the standard advice becomes obviously useless, and it is worth naming it explicitly because these are the things you will otherwise try in order:

  • Switching ports. 25, 465 and 587 are all redirected. There is no third option to fall back to.
  • Changing TLS settings. STARTTLS negotiation fails because the server on the other end is not the one your certificate expectations were built around. Fixing the TLS config cannot fix talking to the wrong server.
  • Re-checking credentials. They are almost certainly correct. The relay has no idea what your Microsoft 365 mailbox password is and no reason to care.
  • Asking support to open the port. The port is not closed.

A PHPMailer client pointed at smtp.office365.com:587 reproduces this exactly: it receives the local Exim banner, and the STARTTLS handshake then fails.

What works: send over HTTPS

Port 443 is not redirected, so the answer is to stop using SMTP entirely and send through the Microsoft Graph API instead.

Two requests. First, get a token using the OAuth client-credentials flow:

POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token

client_id={client}
client_secret={secret}
scope=https://graph.microsoft.com/.default
grant_type=client_credentials

Then send as the mailbox you actually want the mail to come from:

POST https://graph.microsoft.com/v1.0/users/{address}/sendMail
Authorization: Bearer {token}
Content-Type: application/json

{
  "message": {
    "subject": "...",
    "body": { "contentType": "HTML", "content": "..." },
    "toRecipients": [ { "emailAddress": { "address": "..." } } ]
  }
}

A successful send returns HTTP 202, not 200 — Graph accepts the message for delivery rather than confirming delivery.

Two practical notes from running this in production. Tokens last 60 minutes, so cache them; we expire our cache at 50 to leave headroom. And log the timing of the token fetch separately from the send, because when something does go wrong it is almost always the token half.

What about PHP's mail()?

It works, and that is its own kind of trap. mail() hands the message to the local Exim relay, which is precisely what the relay exists to do, so the mail goes out and the function returns true.

But it cannot authenticate as a specific mailbox. The message is not genuinely from the address you want it to be from, with everything that implies for SPF, DKIM and whether it lands in a spam folder. It was also roughly a minute per send in our case, against sub-second over Graph.

If you only need "a message left the building", mail() is fine. If you need mail that is authenticated as your domain and arrives reliably, it is not.

The shape of the lesson

The reason this costs people a day is not that the fix is hard. It is that the failure presents as a configuration problem when it is a topology problem, and every tool in the standard debugging sequence confirms the wrong hypothesis.

When a connection succeeds but the conversation makes no sense, stop checking whether you can reach the server and start checking which server answered. The banner is free and it will tell you in one line.