The two failures that produce backscatter
You wake up to 40,000 "Delivery Status Notification (Failure)" messages in a mailbox that never sent a single one of the originals. You've been joe-jobbed — and your own infrastructure may be making it worse.
A joe-job is trivial to pull off: a spammer puts your domain in the SMTP envelope sender — MAIL FROM:<[email protected]> — and blasts a few million messages at receivers all over the world. Per RFC 5321 §6, when a receiving server can't deliver one of those messages it's supposed to notify the *reverse-path* — the envelope sender. That's you. Multiply by every dead mailbox, every over-quota account, every "user unknown" on the planet, and the misdirected non-delivery reports (NDRs) all converge on your domain. That convergence is backscatter: not spam you sent, but auto-generated bounces and vacation replies for mail you never touched.
Two architectural sins turn a forged-sender storm into a reputation catastrophe, and both live on *your* side of the wire:
- Accept-then-bounce for recipients you can't confirm exist. Your MX answers
250 OKatRCPT TO, queues the message, and only discovers at delivery time that there's no such user. Now you own a message you can't deliver, so you generate a fresh NDR back to the forged sender. - Generating bounces for mail that failed content checks after acceptance. You accepted the DATA, an anti-spam or antivirus pass flagged it, and instead of rejecting in-session you quarantine-and-bounce.
The whole article reduces to one sentence: reject during the live SMTP session, never after. If the decision happens before you take responsibility for the message, the failure notice is the *connecting* server's problem. If it happens after, you just became a spam source with your own name on the return address.
Why "accept then bounce" is the whole disease
Walk the transaction. A sending MTA connects and you exchange HELO/EHLO, then MAIL FROM, then RCPT TO, then DATA; the message is queued, then handed to Dovecot over LMTP for final delivery.
There is exactly one clean place to say no: at `RCPT TO`, before `DATA`. Up to that point the message doesn't exist yet. You've been told who it's *for*, and you can answer 550 5.1.1 <addr>: Recipient address rejected: User unknown inside the same TCP session. The connecting server receives that code and, by the rules of SMTP, *it* is now responsible for informing its sender. Nothing new leaves your infrastructure.
Broken setups make the decision after 250. Once you've accepted, the only way to signal failure is to *emit a new message* — the NDR — addressed to the reverse-path you were handed. For legitimate mail that's correct behavior. For a joe-job, that reverse-path is a forged victim, and you've just relayed spam on the spammer's behalf, original headers quoted helpfully in the bounce body.
The reputation math is brutal. Backscatter looks exactly like spam to the receivers getting hit: unsolicited bulk mail from your IP. You land on Backscatterer.org (run by the UCEPROTECT Network), possibly on general-purpose RBLs too, and suddenly the mail you *do* want to send starts drawing 450 deferrals from everyone who consults those lists. For catch-all and disposable-inbox domains the blast radius is worse: a catch-all by definition accepts *every* local part, so a joe-job that sprays random recipients at your MX gets 250 for all of them, and every one that can't ultimately be delivered turns into a bounce.
Reject at RCPT TO: recipient validation done right
The core fix is making Postfix authoritative about which local parts exist, so it can answer 550 at RCPT TO without ever queuing. The restriction stack in main.cf:
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_unlisted_recipient,
reject_unverified_recipient
unverified_recipient_reject_code = 550- `reject_unauth_destination` stops you being an open relay — you only accept mail for domains you're responsible for.
- `reject_unlisted_recipient` is the workhorse for domains where Postfix knows the full recipient list. It rejects any address not present in your local or virtual maps, *at RCPT time*.
For evilmail's virtual, DB-backed users, "knows the full list" means Postfix must be able to query the virtual_users table live:
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf-- mysql-virtual-mailbox-maps.cf query
SELECT 1 FROM virtual_users
WHERE email='%s' AND active=1With that map wired in, RCPT TO:<[email protected]> for a local part that isn't in the table gets 550 5.1.1 User unknown before DATA — no queue, no bounce, no backscatter. reject_unlisted_recipient is what enforces it.
`reject_unverified_recipient` covers the case where the recipient database *isn't* local — a relay host or backup MX. Postfix runs an address-verification probe against the next hop (MAIL FROM:<> then RCPT TO:<addr>), reads the 250/550, and caches the verdict so you're not probing on every message:
address_verify_map = btree:/var/lib/postfix/verifyFor a secondary or relay MX serving virtual domains, point relay_recipient_maps at a mirror of the primary's recipient list — same principle, different knob.
Before you flip any of this into production, protect yourself with soft-bounce mode, which converts every 5xx into a 4xx so a misconfiguration defers mail instead of permanently rejecting it:
soft_bounce = yes # test only — nothing is lost while you validateWatch the logs, confirm real users still get accepted and bogus ones get deferred with Recipient address rejected, then set it back to no.
Kill the bounce, kill the backscatter
Recipient validation closes the first hole. The second is content filtering that runs *after* acceptance and then bounces. The rule is identical: if you're going to refuse spam or a virus, refuse it with a 5xx before DATA completes, so the sending server issues the failure. Never quarantine-then-bounce.
The clean way is a pre-queue milter. Rspamd sits inline and returns a reject verdict during DATA:
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
milter_default_action = accept
milter_protocol = 6When Rspamd's score crosses the reject threshold, it answers 5xx in-session; Postfix never queues the message, so there's nothing to bounce. milter_default_action = accept means a milter outage fails open rather than blackholing mail — a deliberate availability trade-off.
The classic foot-gun is the backup MX. A secondary with an empty relay_recipient_maps accepts *everything* for the domain, because it has no way to know which local parts are real. It queues that mail, then bounces whatever the primary later rejects — a pure backscatter engine bolted onto your own DNS. Two acceptable fixes: mirror the primary's recipient map on the secondary, or delete the secondary MX record entirely. In 2026, for a small-to-mid mail platform, a backup MX buys you almost nothing — legitimate senders retry for days — and costs you a standing liability. Dropping it is usually the right call.
Defense in depth so you're a harder target
None of the above stops a spammer from *typing* your domain into MAIL FROM. What it stops is you amplifying the attack. To cut the volume of forged mail that ever reaches an accept-then-bounce stage on other people's servers, publish authentication that lets *them* reject the forgery outright:
; SPF — hard fail so receivers can reject, not just soft-flag
evilmail.pro. IN TXT "v=spf1 mx a:mail.evilmail.pro -all"
; DMARC — reject + aggregate reports as early-warning radar
_dmarc.evilmail.pro. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1; adkim=s; aspf=s"Use -all, not ~all. Softfail invites receivers to accept-and-file forged mail, which keeps the backscatter machine running elsewhere; hardfail authorizes an outright reject. p=reject tells DMARC-enforcing receivers to drop unaligned mail, and the rua aggregate reports are your radar — they show the *volume* of forgery hitting DMARC-aware receivers even though you can't stop the spammer from trying. DKIM-sign all outbound so your own legitimate mail stays aligned and survives the policy. If you forward mail anywhere, add SRS so forwarded envelopes don't break SPF — but only where you actually forward.
Operator's checklist
- Authoritative recipient map.
virtual_mailbox_maps(orlocal_recipient_maps) covers every real address, andreject_unlisted_recipientis on. Bogus local parts get550 5.1.1at RCPT. - No catch-all unless every possible address genuinely resolves to a mailbox. On disposable-inbox domains, a blanket catch-all is a backscatter accelerator.
- Content rejection is pre-queue via
smtpd_miltersto Rspamd. Reject with5xxduring DATA; never quarantine-then-bounce. - Backup MX mirrors the recipient map or doesn't exist. An empty
relay_recipient_mapssecondary is a standing liability — mirror it or delete the MX record. - SPF `-all`, DKIM signing on all outbound, DMARC `p=reject` with a live `rua`. Read the aggregate reports; they're your forgery-volume gauge.
# reverse your IP, then query — a hit means you're already leaking NDRs
dig +short 210.69.22.212.ips.backscatterer.org
# count in-band rejects (good) vs self-generated bounces (bad)
grep 'NOQUEUE: reject' /var/log/mail.log | grep -c 'Recipient address rejected'
grep 'status=bounced' /var/log/mail.log | grep -vc 'said: 5'- Blunt the storm with greylisting and per-connection rate limits, so a burst of forged-sender traffic defers (
450 4.x) instead of hammering your queue. - Test it yourself with swaks against a recipient you know doesn't exist:
swaks --server mail.evilmail.pro \
--to [email protected] \
--from [email protected]A correct server answers 550 5.1.1 at RCPT, inside the session. A broken one says 250 and then bounces — the exact behavior that lands your domain on Backscatterer.org the next time someone forges your name into a million envelopes.


