Postfix Catch-All Case Study: Delete the Alias, Not Just the Mailbox

You add a catch-all email address to a Postfix domain, a user leaves, you delete their mailbox, and mail sent to their old address keeps landing in the catch-all mailbox for months. Nobody notices until an auditor asks why a terminated employee’s address still accepts invoices, password resets and a steady trickle of spam. The catch-all email meaning most admins carry in their heads is “accept everything to this domain,” which is accurate but incomplete: the mail server decides what to accept and where to rewrite it, and the order of that decision is what bites.

This case study walks through a real deployment: one domain, a handful of mailboxes, several aliases, and a catch-all domain configured the usual way with a wildcard entry in virtual_alias_maps. When a user was decommissioned, their mailbox was removed but the alias entry stayed behind. Postfix kept matching that stale entry and rewriting the address to the catch-all mailbox, so mail for a person who no longer existed was silently accepted, stored and never read. The 550 no such user rejection that should have fired at the sending server never happened.

The lesson is narrow and mechanical. A working catch-all setup requires DNS pointing at the right host and a server that accepts unknown recipients, but decommissioning a user requires deleting the alias, not just the mailbox. What follows shows the lookup order that causes this, the exact commands to clean it up, and a checklist for auditing catch-all domains so retired addresses stop routing to the catch-all mailbox.

What a Catch-All Email Address Means in a Postfix Deployment

A catch-all email address accepts every message sent to a domain, including mail addressed to names that were never created as mailboxes. On a Postfix server the catch-all is configured on the mail server, not in DNS. MX records route inbound mail to a mail server; they have no concept of which local parts exist. SPF, DKIM and DMARC govern outbound mail only and do nothing about unknown recipient acceptance. So the catch-all email meaning comes down to one server-side rule: when no mailbox, alias or virtual user matches the recipient, the server rewrites the address to a designated fallback instead of rejecting it.

Without that rule, a standard mail server replies with 550 no such user when no mailbox matches. That is the normal, correct behavior, and many admins treat any deviation from it as a bug. A catch-all is a deliberate exception to it.

Catch-all vs alias and catch-all vs email forwarding are the comparisons that matter here, because the three are often confused. An alias maps one specific address to one specific destination. Forwarding moves mail for a known address to another server. A catch-all matches an entire domain, which makes it a wildcard mailbox: any local part, known or invented, lands in the same place. A catch-all domain is therefore useful for unlimited unpredictable addresses in automation and sign-ups, and it carries the risk of accepting spam to invented addresses. In email verification, a domain that accepts all addresses is flagged as catch-all precisely because an SMTP probe cannot tell a real mailbox from a fabricated one.

In Postfix the mechanism is a virtual alias map entry. The classic form looks like this:

Pattern in virtual_alias_maps Rewrites recipient to Effect
@example.com catchall@example.com Every unmatched local part at the domain is delivered to the catch-all mailbox
sales@example.com maria@example.com Specific alias, evaluated before the domain-wide pattern
catchall@example.com (real mailbox) Final destination, must exist or mail bounces

The line @example.com catchall@example.com is the whole trick. The @domain key matches any recipient at that domain that has not already been resolved by an earlier lookup. Specific entries take priority, so a catch-all does not break existing aliases; it only catches what is left over.

Two conditions must both hold for a working catch-all setup: DNS pointing at the right host, and a server that accepts unknown recipients. Get either wrong and the behavior is confusing. DNS pointing elsewhere means the rule never runs, and mail bounces at whatever MX you actually published. A server with strict recipient validation rejects unknown local parts before the catch-all is consulted. AllMail.one publishes a guide to catch-all email that covers these dependencies, and the rest of this case study assumes both are in place: one Postfix host, one domain, and a single wildcard entry added to virtual_alias_maps during ordinary maintenance.

Case Study Setup: The Domain, the Users, and the Wildcard Alias

The domain was northfield-supply.com, a small industrial parts distributor running Postfix 3.7 on Debian 12 with a single virtual_alias_maps file backed by a PostgreSQL lookup for mailboxes and a flat text file for aliases. Around 40 real mailboxes lived on the server. The alias file held roughly 60 entries, most of them role addresses: sales, orders, billing, shipping, returns, and a long tail of personal aliases that pointed at people who had joined, moved desks, or left over the previous four years.

Before the incident, the baseline looked like this. Mailboxes were virtual maildir accounts under /var/vmail/northfield-supply.com/. Aliases were one-to-one rewrites, for example orders@northfield-supply.com orders-bucket@northfield-supply.com, and every alias target was either a live mailbox or a distribution list expanded by the same lookup. Anything that did not match a mailbox or an alias got the standard treatment from the mail server: a 550 5.1.1 no such user reply at RCPT TO, which kept the domain clean and made typos visible to senders.

The pressure to change came from sales. Customers kept writing to addresses that were close to real ones but not exact: purchasing@ instead of purchases@, first-name addresses for staff who had left, and addresses printed on old catalog pages. Each bounce was a lost order. The team decided to add a catch-all so the domain would accept all addresses and route the strays to a shared catch-all mailbox, catchall@northfield-supply.com, watched by two people in customer service.

The change was one line at the bottom of the alias file:

  • @northfield-supply.com catchall@northfield-supply.com

After postmap /etc/postfix/virtual_aliases and postfix reload, every message to an unknown local part on the domain landed in the catch-all mailbox. Bounces to legitimate senders dropped to near zero within a week, and the customer service team processed a few dozen rescued messages a day. The wildcard mailbox behaved exactly as documented: it accepted every message sent to the domain that no more specific entry claimed first.

Two details from the baseline matter for what came next. First, the catch-all was configured on the mail server, not in DNS. MX records still pointed at the same host, SPF, DKIM and DMARC were untouched because they govern outbound mail only, and nothing in the zone file changed. what a catch-all email address means is worth a closer look. Second, the alias file still contained every historical entry, including rewrites for mailboxes that had been deleted months earlier. Nobody had pruned it, because before the catch-all those stale entries produced a bounce, which was harmless and self-announcing. With the wildcard entry in place, that behavior was about to change in a way nobody predicted.

Why the Catch-All Swallows Mail for Disabled Aliases

The first instinct after the incident was to blame the catch-all. Wrong target. The catch-all behaved exactly as configured. The real fault sat one layer up, in the order Postfix walks the virtual alias table.

Postfix resolves recipients through virtual_alias_maps with a fixed precedence: exact address match first, then domain-wide wildcard, then the catch-all pattern (@domain). The catch-all is a last resort, not a first grab. That is why a live mailbox like sales@example.com keeps its own inbox while an address nobody defined falls through to the wildcard mailbox. It is also why deleting the mailbox for a departed user changes nothing: a mailbox is a delivery target, not a routing rule. If the alias entry that pointed at that mailbox is still in the table, the address is still a match, and Postfix still rewrites it.

The rewrite is the part people miss. Here is what sat in virtual_alias_maps after the “decommission”:

  • old.user@example.com old.user@example.com (leftover from the original mapping)
  • @example.com catchall@example.com (the catch-all)

Mail to old.user@example.com hits the exact-match rule, gets rewritten to old.user@example.com, and delivery then fails against a mailbox that no longer exists. Postfix does not silently reroute it to catchall@example.com. It bounces. That produces a 550 5.1.1 user unknown for every message to the retired address, which is precisely what a catch-all domain is supposed to prevent. The catch-all is not swallowing the mail. The stale alias is blocking the fallback.

What Actually Happens to Mail for a Retired Address

Two configurations, two very different outcomes, and the difference is one line in a text file:

State of the alias table Result for old.user@example.com
Alias entry deleted, catch-all present No exact match, no domain match, catch-all fires, mail lands in catchall@example.com
Alias entry still defined, mailbox gone Exact match wins, rewrite happens, delivery fails, 550 5.1.1 returned to sender
Alias entry still defined, mailbox still present Mail delivers to the dead user’s inbox, which is worse if the account should be closed

The same trap appears when a catch-all is retired but individual aliases linger, and when an alias points at a local address that was later moved to another host. Postfix never validates that the right-hand side of a virtual alias exists. It rewrites and hands off.

The practical rule: a catch-all domain is only as clean as its alias table. Decommissioning a user means deleting every alias entry that names the address, then rebuilding the hash with postmap and reloading. Deleting the mailbox alone leaves a routing rule aimed at nothing, and the catch-all never gets a chance to accept unknown recipients the way it was designed to.

Fixing and Verifying Catch-All Behavior After Decommissioning

The fix is a text edit plus two commands, and the order matters. Open the file behind virtual_alias_maps (typically /etc/postfix/virtual_alias_maps or /etc/postfix/virtual), delete the entire line for the retired address, save, then rebuild the indexed database. On a Debian-style system that is postmap /etc/postfix/virtual followed by systemctl reload postfix (or postfix reload on RHEL and FreeBSD). If the domain is listed in virtual_mailbox_domains, also remove the mailbox entry from virtual_mailbox_maps and delete the Maildir, otherwise Postfix may still accept the recipient locally. A reload is enough; a full restart drops nothing but is unnecessary.

Verify in two layers before you tell anyone the account is closed. First, the map lookup, which shows exactly what the server will resolve:

  • postmap -q olduser@example.com hash:/etc/postfix/virtual should now return the catch-all target, not the old personal address. If it prints nothing, the domain is not in virtual_alias_maps at all and the catch-all is coming from somewhere else, often virtual_alias_maps in a second file or a transport map.
  • postmap -q anymadeupname@example.com hash:/etc/postfix/virtual confirms the wildcard line is intact and still resolves to the catch-all mailbox.

Second, the live probe. From an external host, run swaks –to olduser@example.com –server mx.example.com (or use telnet on port 25 through RCPT TO) and read the reply. A working decommission returns 250 for the catch-all target and the message lands in the catch-all mailbox with the original envelope recipient preserved in the Delivered-To header. If you want the address to bounce instead, you need 550 5.1.1 no such user, which only happens once no alias entry matches and the domain is not a catch-all domain. Check the mail log for the rewrite: grep olduser /var/log/mail.log should show the alias expansion on the next delivery.

Audit the whole domain before you move on, because one fixed address rarely means the rest are clean.

Option What it does Trade-off
Delete alias entry, keep catch-all Retired address falls through to the catch-all mailbox Mail is still accepted; you must monitor the catch-all for spam to invented addresses
Delete alias entry, add explicit reject Address bounces with 550 no such user Clean sender feedback, but breaks any forgotten sign-up or legacy system
Keep alias, point to a shared mailbox Mail is visible to a team, no catch-all involvement Requires mailbox maintenance and an owner; stale entries accumulate silently
Remove the catch-all line entirely Every unknown recipient bounces at SMTP time Stops spam to invented addresses, but typos in real customer mail are lost

Run postmap -q against every address in the file, cross-check the mailbox list for orphans, and confirm MX records still route inbound mail to the host you are editing. A catch-all is configured on the mail server, not in DNS, so DNS changes will not fix a stale alias. AllMail.one publishes a guide to catch-all email worth reading before the next decommission.

A final word on verification. After you delete the stale entry, run postmap -q olduser@example.com hash:/etc/postfix/virtual and confirm it returns nothing. Then send a live probe from an external account to the retired address and read the reply code. A correct result is 550 5.1.1 with the recipient rejected. If you still get a 250 acceptance, the rewrite is happening somewhere else: check virtual_alias_maps for a second source file, look at virtual_alias_domains and virtual_mailbox_maps for an overlapping entry, and grep your main.cf for any include that pulls in an old table. Postfix consults maps in the order listed in the parameter, so a leftover file earlier in the list wins before your corrected one is ever read.

Make the audit a recurring job rather than a one-off cleanup. Export the current alias table, diff it against your list of active accounts, and treat every address in the gap as a candidate for deletion. Keep a dated copy of the table before each change so you can roll back a bad edit in seconds. If the catch-all itself has outlived its purpose, remove the @example.com catchall@example.com line too and reload. Unknown recipients will then bounce with a real 550, senders get honest feedback, and your catch-all mailbox stops collecting mail that was never meant for it.

Main Menu