We were building the booking system for a medical conference. Someone reserves a seat, the system saves the booking, and an email goes back to confirm it — the digital version of a receipt. Two days of work, and almost all of it went smoothly. The save-the-booking part was done and tested.
Then we got to the email. That one step ate an afternoon.
The part that looked finished but wasn't
There are two common ways to send email from a system like this. One is to hand your message to a mail server and let it relay the message onward — the classic post-office approach. The other is to send the message through the email provider's web service, a single request straight to them. We started with the post-office approach, because it's the standard one.
It looked like it was working. The connection opened, the handshake went fine, every early step came back clean. Then, right at the last moment — the part where you prove who you are with a password — it refused us. "Authentication failed."
So began the loop every engineer knows. Check the password. Try a different one. Make sure we're allowed to send from this machine. Try the account name instead. We went through three different sets of credentials. Every single one was rejected at that same final step.
Here is the part that gave it away. At the exact same moment, from the exact same machine, the other method — sending straight to the provider's web service — worked first time. Same account. Same place. One door slammed in our face; the door right next to it opened without a fuss.
That told us the problem wasn't us. It wasn't a wrong password and it wasn't a blocked machine. The post-office path itself was broken for this account, in a way no amount of fiddling with passwords was ever going to fix.
The fix
So we stopped fighting it and switched the system over to send email through the provider's web service instead. One test message went out, landed in a real inbox, and the whole chain ran end to end — reserve a seat, save it, send the confirmation, see it arrive.
The password wasn't wrong and the machine wasn't blocked. One way of sending refused us; the other accepted us — from the same place, at the same moment.
The lesson
For event registration, the confirmation email is the product. It's the receipt the attendee is sitting there waiting for, and if it doesn't arrive they're asking for help within minutes. So this is exactly the wrong place to use the method with the most ways to fail quietly.
And that's the real difference between the two. The post-office method has a long list of things that can go wrong, most of them nothing to do with your actual code: permissions, blocked ports, handshake settings, account states the provider never shows you. Every one of those waits for the worst possible moment to announce itself — the moment real bookings start coming in. Sending straight to the provider's web service folds most of that away into one request you can test on its own and reason about plainly.
This isn't a complaint about the old way. It's a complaint about choosing the path with the most silent failure modes for the one part of the system where silence costs you the most. When the email arriving is the entire point, pick the way with the fewest places to hide — and watch a real message land in a real inbox before you call it done. A passing test that only proves the first step isn't proof of anything.
Under the hood
The stack was deliberately unglamorous. A self-hosted Supabase instance held the registrations. A self-hosted Mautic 5 instance — running on a fresh server provisioned programmatically via a server API, reached over a Cloudflare tunnel — handled segments and the confirmation campaign. The reserve-to-database chain was already green; the only thing left was getting Mautic to post a mail on reservation.
The plan was SMTP relay: point Mautic at the relay host on port 587, feed it credentials, send a test. The banner came back. EHLO, STARTTLS, AUTH LOGIN — all clean. Then the password step returned 535 authentication failed.
We re-vaulted the key, tried a different key, confirmed the sending IP was on the provider's authorised list (it was — the same container address authenticated fine against the provider's HTTP API minutes later), and tried the account email as the login. Three fresh SMTP credentials and the API key, every one a 535 — while the exact same account and the exact same egress IP returned a clean 201 over the HTTP API. The relay path itself was broken on that account, independent of credentials or IP.
The pivot was to switch the mailer to the provider's transactional HTTP API. That meant dropping the right Symfony mailer bridge into the container — the version matching the framework Mautic is built on, not the newer one that conflicts — and changing one DSN line from an smtp:// transport to an API transport. Test send succeeded; the full reserve → Supabase → Mautic → confirmation-email chain ran end to end into a real inbox.
One operational wrinkle worth keeping: the bridge installs into the application's vendor/ directory, which a container redeploy wipes. The durable fix is a small reconciler that re-installs it after a redeploy, not a one-time manual command. If a transactional dependency lives somewhere ephemeral, assume it will vanish and plan the heal.