Day 153: The Timeout That Was Really a Firewall
Setting up email on a self-hosted app has a failure that wastes an afternoon. You enter your mailbox credentials, hit Test, and get “Connection timed out.” So you do the reasonable thing: assume you typed something wrong. You re-check the password, the server name, the port, the TLS setting. You try again. Same timeout. Nothing you’re looking at is wrong, and the error is pointing you at the one place the problem isn’t.
Two legs, one misleading error
Because “test the mailbox” is really two separate connections, and they fail for different reasons. There’s the receiving side, IMAP, which reads your inbox. And there’s the sending side, SMTP, which sends mail out. PR #847 starts by pulling those apart, running each leg on its own and reporting them separately, instead of collapsing both into one pass-or-fail. That alone tells you which half is unhappy. But the real fix is what it does when the sending leg fails at the connection level.
Here’s the thing almost nobody setting up email on a rented server knows going in: cloud hosts block outbound SMTP by default. Hetzner, DigitalOcean, the big three, they all firewall the mail ports to stop their machines being turned into spam cannons. So a mailbox that defaults to sending on port 465, which is a perfectly correct setting, times out from your server not because anything is misconfigured but because your host will not let that port out. The credentials are right. The port is right. The host is the wall. “Connection timed out” is technically true and practically a lie, because it names the symptom and hides the disease, and the disease is a firewall, not a mistake.
Diagnose, then hand the fix over
So on an SMTP connection failure, Pinchy now probes raw TCP reachability on the common mail ports, 465, 587, and 25, from the same container that would actually send the mail, so the answer reflects that box’s real network and not a guess. Then it tells you what it found. If 465 is blocked but 587 is open, the dialog says exactly that and offers a “switch to 587 and retry” banner. If everything’s blocked, it says so and links to a new docs section on ports, TLS, and cloud firewalls, including how to ask your host to unblock them. The banner is a button you click, never an automatic switch, in the same spirit as the Day 139 rule about failures that need to reach the person: Pinchy diagnoses and proposes, you decide and confirm.
There’s a small honest wrinkle in the plumbing worth noting: the diagnostic comes back as an HTTP 200 with a structured body rather than a 400, because the frontend’s error path throws away the response body on a non-2xx status, and the body is the entire point here. The whole value is in the details of the failure, so the failure had to stop being a bare error and start being data.
Day 153
This is the same idea as Day 149, where the sandbox accused an agent of an attack that was really a missing folder. A generic error doesn’t just withhold help, it actively misdirects, because a person reads “timed out” and starts debugging their own settings, which is the most natural and most wrong response. The work of legible failure is rarely adding an error message. It’s replacing a true-but-useless one with the specific cause and, where you can, the specific next step. “Something failed” makes the user your debugger. “Your host blocks this port, here’s an open one” makes the software do the diagnosing, which is whose job it was all along.