When I switched this site's outgoing mail from a missing local sendmail to proper SMTP submission on my own mail server, the first test send failed. Authentication worked, STARTTLS worked, and then the server rejected the recipient:
450 4.2.0 <...>: Recipient address rejected: Greylisted
The recipient was my own address, in a domain whose mail is handled by that same server. My own mail server was greylisting a client that had just logged in to it, sending mail to me.
What greylisting is for
Greylisting is an anti-spam technique. The first time the server sees a particular combination of sending client, sender address and recipient address, it answers with a temporary failure, a 450, and remembers the combination. A legitimate mail server queues the message and tries again a few minutes later, and the retry is accepted. A lot of spam software never retries. My server uses postgrey for this, running as a policy service on 127.0.0.1:10023.
It makes sense for mail arriving from the internet. It makes no sense for an authenticated user submitting mail. Someone who has just logged in with a valid password is not an anonymous spam bot, and making them wait buys nothing.
Two ports, two sets of rules
Postfix accepts authenticated mail on two ports here: 465 (smtps, TLS from the first byte) and 587 (submission, plain connection upgraded with STARTTLS). Each is its own entry in master.cf, and each can override settings from main.cf with -o options.
The 465 entry overrides the important ones:
-o smtpd_recipient_restrictions=-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
The 587 entry only overrode smtpd_client_restrictions, plus enabling SASL and enforcing TLS. It didn't touch recipient restrictions at all, so 587 inherited the global ones from main.cf:
smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10023, permit_sasl_authenticated, permit_mynetworks, ...
Postfix evaluates a restriction list in order and stops at the first entry that decides. The policy check, postgrey, comes first. It saw a new combination and answered "defer" before Postfix ever got to permit_sasl_authenticated, the entry that would have let an authenticated client straight through.
So every authenticated client on port 587 was greylisted for every new combination of sender and recipient, for as long as this configuration has existed. Port 465 never was.
Why nothing had noticed
Desktop mail clients handle a temporary failure gracefully. They retry a few minutes later, and the retry is accepted. From the user's side, a message sometimes takes a little longer to send, which nobody investigates.
Drupal doesn't retry. Its mailer sends once, and a 450 is a failure, reported immediately. The first client of this server that didn't quietly retry was also the first one to expose the problem. The setting had been wrong all along. Every client before Drupal had been covering for it.
The fix
One command, to give 587 the same rule 465 already had for authenticated users:
postconf -P 'submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject'
Then postfix reload. The -P flag edits a service's override in master.cf without hand-editing the file. On port 587, authentication is now required and sufficient: an authenticated client is permitted before any policy check runs, and anyone else is rejected. Greylisting still applies where it belongs, on mail arriving from other servers on port 25.
The retest went through, and the site's mail has been working since.
One thing I couldn't check
I wanted to confirm delivery in the mail log, and found that /var/log/mail.log on that server has been empty since November 2020. Postfix is clearly still running and still logging somewhere, just not there. I haven't tracked down where yet, which is why this write-up relies on what the SMTP conversation and postconf show rather than on log lines. That's a small problem of its own, and one of the reasons this one went unnoticed: if Postfix's own log had been somewhere I looked, a steady trickle of authenticated clients being greylisted on 587 would have been visible long before a website tripped over it.
The general point
A client that retries automatically hides server-side misconfigurations. That's usually a feature: mail gets delivered despite transient trouble. But it means the server's real behavior is only visible to clients that don't retry, and if all your clients do, the misconfiguration can sit there indefinitely. When I add a strict, non-retrying client to an old setup, I now expect it to find things.
Leave a comment
Comments are reviewed before they appear.