Mail Client Stops Receiving: Fail2ban IMAP Self-Ban¶
🛑 Problem¶
A mail client stops receiving new email on one device while other devices (e.g., phone on cellular) continue to work normally. The mail server is healthy — Postfix is delivering to maildir and Dovecot is running — but the affected device receives no new messages and is never prompted for credentials.
Or the confusing variant: a device reports "cannot connect to the server" while still showing mail and still sending successfully. That is not a contradiction and not a client bug — it is a partial ban, where the jail's action blocks the IMAP/POP ports but leaves SMTP submission open. Same root cause, same fix; see Step 6 and the port-scope note below.
🔍 Diagnosis¶
Step 1 — Confirm the mail server is delivering¶
If you see status=sent (delivered to maildir), the server is working. The issue is on the client side.
Step 2 — Check if the client is connecting at all¶
If there are zero results, the client is not reaching the server at all — not a credentials or sync issue.
Step 3 — Get the client's current public IP¶
Run this on the affected machine:
Step 4 — Check if that IP is banned by Fail2ban¶
ssh root@<mailserver> "fail2ban-client status postfix-sasl"
ssh root@<mailserver> "fail2ban-client status dovecot-invalid"
Look for the IP in the Banned IP list. If it's there, that's your problem.
Step 5 — Find when the ban was applied¶
This shows the exact timestamps of auth failures and the ban event.
Step 6 — If the IP was supposedly whitelisted, check whether the whitelist was working at the time¶
A whitelist that is correct right now can still have failed at the moment of the ban. If ignoreip uses a hostname, grep the same log for how each filter pass decided:
Two different verbs, and the transition between them is the whole story:
| Log line | Meaning |
|---|---|
[jail] Ignore <ip> by dns |
The hostname in ignoreip resolved to this IP — the whitelist worked on this pass. |
[jail] Found <ip> |
The whitelist did not apply on this pass; the failure was counted toward maxretry. |
A run of Ignore ... by dns that abruptly becomes Found ..., then Ban, means the whitelist itself lapsed — almost always a transient DNS resolution failure, not a config error. Confirm by resolving the name after the fact from the mail server and from a public resolver:
If both return the banned IP, the config was never wrong — it just wasn't durable. Fix per the "list both forms" correction below, not by re-editing the hostname.
✅ Fix¶
Unban the IP from all relevant jails:
Mail should resume immediately without restarting any services.
Permanent fix — whitelist the trusted IP (ignoreip)¶
Unbanning is temporary: if the client keeps failing auth (wrong password, stale token), the same IP gets re-banned within minutes. For a known, trusted network (e.g. your home egress IP) add it to Fail2ban's ignoreip so it can never be banned:
# /etc/fail2ban/jail.local — [DEFAULT] section, applies to ALL jails
ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10 <home_ip>
fail2ban-client reload
fail2ban-client get postfix-sasl ignoreip # confirm the IP is listed
[!warning] Check where
ignoreipis actually sourced from — the obvious file may be dead config This article previously stated that majormail codifiesignoreipviafail2ban_ignoreipinhost_vars/. That is no longer true. Under the parity-capture design thefail2banrole copies each host's captured config verbatim fromroles/fail2ban/files/hosts/<host>/and consumes no role variables. Thefail2ban_ignoreipvar still sitting inhost_varsis inert: editing it commits cleanly, deploys cleanly, and changes nothing.The live file is
roles/fail2ban/files/hosts/majormail/jail.local. Before editing any Ansible-managed config, confirm which mechanism actually renders it — a stale variable is worse than a missing one, because it looks like the fix worked.⚠️
ignoreiptakes a public egress IP, which may be dynamic. If your ISP reassigns it, the whitelist points at a stale address and bans can return — recheck the egress IP first. Use a subnet only if you trust the whole range.[!note] Confirmed 2026-08-08 — the dynamic-IP warning above is not theoretical majormail's
ignoreipwas found carrying136.56.0.55, a home egress IP that had rotated to a new address weeks earlier. The whitelist had been silently inert the entire time — no error, no alert — and was simultaneously whitelisting whatever unrelated ISP customer now held the old address. It was only noticed because a stale client credential triggered a ban that the whitelist should have prevented.A hardcoded residential IP in
ignoreipis a fix with an expiry date nobody sets a reminder for. Re-verify the egress IP whenever bans reappear on a "whitelisted" network. ~~Prefer a DDNS hostname if your Fail2ban version supports it~~ — see the correction immediately below: a DDNS hostname alone trades this failure mode for a worse one.[!danger] Correction 2026-08-09 — a DDNS hostname alone is not the answer; list both forms Acting on the advice above, majormail's whitelist was switched from a literal IP to the DDNS hostname (
ignoreip = ... majorshouse.com). One day later it caused a worse outage than the problem it solved.Fail2ban re-resolves hostnames in
ignoreipon every filter pass. It skipped the home IP correctly all morning —Ignore <ip> by dnsinfail2ban.log, every cycle — then the name failed to resolve for two consecutive cycles. Those passes fell through to the normal filter, the queued auth failures counted as real,maxretrytripped, and the household was banned. DNS was healthy before and after; the exposure window was two filter cycles wide.A literal IP has a silent expiry date. A hostname has no failure margin — any resolver hiccup is an open door. Neither form is sufficient alone, so list the network twice:
# /etc/fail2ban/jail.local — [DEFAULT] # Literal holds when DNS is unavailable; hostname survives a WAN IP change. ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10 <home_ip> <ddns_hostname> # Cache positive ignore results so a transient resolver failure can't # reclassify a whitelisted network as an attacker mid-incident. ignorecache = key="<ip>", max-count=100, max-time=6h
ignorecacheis the load-bearing half — without it, every filter pass re-decides the question and re-exposes you to the same blip. Validate before reloading:fail2ban-client -t(or-c <dir> -tagainst a scratch copy) must returnOK: configuration test is successful.
🔁 Why This Happens¶
| Symptom | Cause |
|---|---|
| One device stops getting mail, others don't | Each device has a different public IP. Only the banned IP is blocked. |
| No auth prompt in the mail client | Fail2ban issues a TCP REJECT — the client sees a connection error, not an auth failure, so it doesn't prompt for new credentials. It silently retries on a timer. |
| Multiple failed attempts before ban | The mail client retried with a previously valid session token or stale auth state, triggering the maxretry threshold. |
Ban on postfix-sasl blocks IMAP (port 993) |
The postfix-sasl jail monitors ports 25, 465, 587, 143, 993, 110, and 995 — not just SMTP. A failed SMTP auth attempt can ban access to IMAP as well. |
| "Can't connect to the server" but mail still arrives / still sends | The opposite port scope. A dovecot-* jail's ban action may cover only 993/143/995/110, leaving SMTP 587/465 untouched — so the client fails to log in for IMAP while sending works and previously-downloaded mail still displays. Reads like a client bug; it's a partial ban. |
| Every device on one network breaks at once | The ban is on the shared public egress IP, not the device. Behind NAT the server cannot tell your devices apart — one bad client bans the whole household, and the loudest complaint usually comes from an innocent device. |
⚠️ Key Notes¶
- Fail2ban
postfix-saslcovers all mail ports — a single jail can lock out SMTP and IMAP simultaneously. - Check
bantimebefore assuming the ban will expire — and check it per jail, not in[DEFAULT]. Withbantime = -1the ban is permanent; it will not lift no matter how long you wait. On majormail (2026-08-08) four failed SMTP submissions from one stale client produced a permanent ban across all seven mail ports, taking every device in the house offline until it was manually cleared. The trap:jail.local's[DEFAULT] bantimeis only the fallback. Any jail that sets its ownbantimeinjail.d/overrides it, and jails in the same file often differ — on this hostdovecot-invalidis3600whilepostfix-saslinherits-1. Reading[DEFAULT]alone will tell you an hour-long ban is permanent, or worse, that a permanent one will expire. Ask the daemon, not the file:The same "which mechanism actually wins" caution applies tofor j in $(fail2ban-client status | grep "Jail list" | sed 's/.*:\s*//;s/,/ /g'); do printf '%-24s %s\n' "$j" "$(fail2ban-client get $j bantime)" doneignoreip(see the dead-config warning above) — in both cases the file you're reading may not be the one in effect. - A purely outgoing misconfiguration can kill incoming mail. Because the jail covers all mail ports, an SMTP-only credential problem bans IMAP too. The giveaway is a client whose inbound mail worked fine right up to the moment it tried to send.
- iOS stores incoming and outgoing credentials separately. Updating an account password fixes IMAP and silently leaves the SMTP password stale — the classic source of the "it was working a second ago" ban. Check
Outgoing Mail Server (SMTP)settings independently, including whether the username is the full address or a bare name. - Unbanning treats the symptom; the failing client is the disease. After unbanning, watch the log for a minute — if failures are still arriving on a steady cadence (e.g. 3 attempts every ~45–60s), a client is still retrying with a stale credential and the ban will return. Successes and failures interleaved from the same IP is the signature of one broken client among several working ones.
- Behind NAT you cannot identify which device is failing from the server alone — every device shares the egress IP, and Dovecot logs no client identity for a failed login (
auth_verboseis off by default). Work from the other end: which client was recently repointed, reinstalled, or had its password rotated. A device that is complaining is usually the victim of the ban, not the cause of it. - Know your ban action's port scope before you diagnose.
banaction = firewallcmd-rich-ruleson a dovecot jail may reject only 993/143/995/110. Confirm what is actually blocked rather than inferring it:nft list ruleset | grep <IP>(oriptables -L -n | grep <IP>) shows the exact ports and is the fastest way to explain a half-working mail setup. - The affected device's IP is never logged in Dovecot once banned — the rejection happens at the iptables/Fail2ban layer before Dovecot sees the connection.
- Tailscale doesn't help if the mail client connects via the public interface — the ban applies to the public IP, not the Tailscale IP.
- Check when the ban happened:
grep '<IP>' /var/log/fail2ban.logshows exact timestamps.
🔎 Quick Diagnostic Commands¶
# Get your current public IP (run on affected machine)
curl -s https://api.ipify.org
# Check all Fail2ban jail statuses
fail2ban-client status
# Check a specific jail for a banned IP
fail2ban-client status postfix-sasl
fail2ban-client status dovecot-invalid
# Unban from a specific jail
fail2ban-client set postfix-sasl unbanip <IP>
fail2ban-client set dovecot-invalid unbanip <IP>
# Unban from all jails at once
for jail in $(fail2ban-client status | grep "Jail list" | sed 's/.*://;s/,/ /g'); do
fail2ban-client set $jail unbanip <IP> 2>/dev/null && echo "Unbanned from $jail"
done
# Find when a specific IP was banned
grep '<IP>' /var/log/fail2ban.log | tail -20
# Did the whitelist apply on each pass? ("Ignore ... by dns" vs "Found ...")
grep '<IP>' /var/log/fail2ban.log | grep -E 'Ignore|Found|Ban' | tail -30
# What is ACTUALLY blocked right now (ports matter — a ban may be IMAP-only)
nft list ruleset | grep '<IP>' # or: iptables -L -n | grep '<IP>'
# Confirm the runtime whitelist as the daemon sees it (not just the file)
fail2ban-client get dovecot-invalid ignoreip
# Is a client still failing after the unban? (watch for a steady cadence)
journalctl -u dovecot --since '10 min ago' | grep '<IP>' | grep -c 'auth failed'
journalctl -u dovecot --since '10 min ago' | grep '<IP>' | grep -c 'Logged in'
# Validate config changes BEFORE reloading — never edit jail.local blind
cp -a /etc/fail2ban /tmp/f2btest && $EDITOR /tmp/f2btest/jail.local
fail2ban-client -c /tmp/f2btest -t # must print: OK: configuration test is successful