---
title: "Allow and block lists, digests, spam traps, country blocking and impersonation"
url: https://www.hmailserver.co.uk/topic/28249/allow-and-block-lists-digests-spam-traps-country-blocking-and-impersonation
author: "Progressiverobot"
published: 2026-09-27T19:31:13.731Z
updated: 2026-09-27T19:31:13.731Z
category: "Anti-spam & message filtering"
tags: ["spam","security","administration","rest-api"]
site: hMailServer Community Forum
---

# Allow and block lists, digests, spam traps, country blocking and impersonation

*Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) reads a link's host that is not ASCII by UTS 46, and an HTML part's character references as a browser does, for the URL entries of the allow and block lists, so a message whose links escaped a list that way can now be tagged or held; a URL entry written in Unicode is stored in its `xn--` form and covers a link written either way, and one with a label UTS 46 refuses is refused when it is saved. In the Control Panel the Spam filtering group's six allow and block lists are under Lists, and Ctrl+K takes every old title to its page. See [the 6.3.7 announcement](/topic/28277).*

This topic covers five layers that sit beside the score-based spam tests rather than inside them: the allow and block lists at three levels, the quarantine digest, spam traps, country blocking and impersonation protection. All five are new since 6.3.4, and the one thing to know first is that every one of them is off, or inert, until an administrator sets it: a server upgraded to 6.3.4 and left alone has no list entries, sends no digest, has no traps, judges nothing by country and checks no display name.

### The allow and block list: one table, three levels

*Since 6.3.4 (schema 6046).* The older lists (the anti-spam white list, **Blocked senders**, the greylisting white list) act during the SMTP conversation, for every recipient at once. The allow and block list is per recipient. It is one table, `hm_senderlist`, with a scope on each row: each account's own safe and blocked senders, a domain's list, and the server's.

| Entry | Covers |
|---|---|
| `someone@example.com` | That one address, in any case. No wildcards |
| `example.com` | `example.com` and every subdomain; not `notexample.com`, not `example.com.attacker.net` |
| `@example.com` | The same as `example.com`, and stored as `example.com` |
| `https://bad.example/anything` | A URL: stored as `bad.example/`, covering a link in the message to that host or a subdomain, whoever sent it |

An entry covers a message when it matches **either** the envelope sender **or** the address in the `From` header, because mail sent through a mailing service carries the service's address in the envelope and the brand's in the header. A message with the null sender is never decided by any list.

**URL entries are in 6.3.4.** CHANGELOG's 6.3.4 section lists them, and they are consulted only when neither address decided anything. The closing section of [docs/SenderListsAndDigests.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SenderListsAndDigests.md), which says "URLs are not list entries", is older text left in the file.

**What allow and block do.** The lists are read at local delivery, once per recipient, after the spam tests have run.

| | The message was classified as spam | It was not |
|---|---|---|
| **Allowed** | Delivered unmarked for this recipient: the spam flag, the `X-hMailServer-Spam` and reason headers and the subject tag are removed from this copy | Delivered as it is |
| **Blocked** | Held in the quarantine for this recipient when the quarantine is on; marked as spam and delivered when it is not | The same |

A block **never deletes**, and no score is recorded on a blocked message, because nothing measured it. A message released from the quarantine is never held again by a list. The other recipients of the same message are not affected.

**Precedence is nearest first**: the account, then its domain, then the server. The first of the three that has anything to say about the sender is the only one consulted, so an account's allow beats a domain's block. That is the reverse of Microsoft 365's order, and deliberate: the person the message is addressed to can tell whether they wanted it. An administrator who needs a sender stopped for everyone still has **Blocked senders**, which nobody's own list overrules. Inside one list the more specific entry wins (an address beats a domain, a longer domain beats a shorter one), and on an exact tie the block wins.

**Neither address is proof.** Both are whatever the sender wrote, so an allow entry is as forgeable as the address it names. The document's advice: keep allow entries to correspondents whose domains publish DMARC, and prefer a single address to a whole domain.

| List | Webmail | Control Panel (Windows) | Control Deck | REST |
|---|---|---|---|---|
| The account's own | Settings → **Safe senders** | none | none | `/api/v1/me/sender-list` |
| A domain's | none | Domain → **Allow and block list** tab | Domain editor → **Allow and block list** | `/api/v1/domains/{domain}/sender-list` |
| The server's | none | Spam & virus filtering → **Allow and block list** | Lists → **Allow and block list** | `/api/v1/sender-list` |

Over COM (Windows only) the domain's list is `Domain.SenderList` and the server's `Settings.AntiSpam.SenderList`. There is no administrator path to an account's own list. **Block this sender** and **Report phishing** in the webmail now add the sender to the account's blocked senders; they used to write a rule into the Sieve script, and rules written that way are left where they are. A list holds at most a thousand entries; adding a value already there rewrites the entry (`200` rather than `201`). Expiry can be set over REST, in the Deck and in the Control Panel's two list editors (a switch and a date); the webmail does not offer it. [docs/SenderListsAndDigests.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SenderListsAndDigests.md) still says the Control Panel has no expiry; that passage predates the change, which the repository's roadmap records as made after 6.3.3.

### The quarantine digest

Once a day, one plain-text message goes to each account of a domain that has something **newly** held for it, listing each message's subject, sender, when it was held and why, with a link that delivers each entry, a link that delivers everything listed, and a footer link that stops the digest (also the `List-Unsubscribe` header). An empty digest is never sent, and an account that has had one is not sent another for twenty hours. The message comes from the domain's postmaster with the null envelope sender and `Auto-Submitted: auto-generated`, and never asks for a password.

| Setting | Default |
|---|---|
| `QuarantineDigestBaseUrl`, the address the links point at, such as `https://mail.example.com` | empty: **no digest is sent** |
| `QuarantineDigestLinkHours`, 1 to 720 | 72 |
| The domain's switch and hour (`quarantine_digest`, `quarantine_digest_hour`; 0 to 23, local time), which an account may override | off; 7 |

All three are in the database, none in `hMailServer.ini`. The quarantine itself (`QuarantineEnabled`) has to be on, and the address must be an absolute `http://` or `https://` URL of the listener the webmail is served from.

**When it is sent.** The task wakes every five minutes and, within the digest hour, sends only its share, so a domain of a thousand accounts is spread across the hour. At most five hundred digests go in one run. Over COM (Windows only), `AntiSpam.SendQuarantineDigests()` sends now, ignoring the hour and the twenty-hour rule and nothing else.

**The links.** Each is 32 random bytes recorded against one account and one entry, usable once, and no longer valid after `QuarantineDigestLinkHours`. Following a link does not act on its own: it opens a page with one button, because mail security products follow every link in a message, and a link that released mail on its own would have one of them releasing every held message. Stopping the digest does not stop mail being held.

### Spam traps

*Since 6.3.4 (schema 6058).* A trap is an address that was never real and never given to anybody, so a message to one comes from a harvested, bought or guessed list. At `RCPT TO` a trap is refused **exactly as any unknown address is**, `550 Unknown user`, made by the same code, so the sender cannot tell. The check runs before the delivery lookups, so a catch-all, greylisting or a backup-MX callout cannot answer first.

| | What happens | Setting |
|---|---|---|
| The connecting address | Banned through the server's own auto-ban, as an expiring range named `Auto-ban: spam trap <address>` | `SpamTrapBanMinutes`, 1440. 0 bans nobody |
| The envelope sender | Added to the server's block list with an expiry, **only when its domain's SPF passes for the connecting address** | `SpamTrapBlockDays`, 30. 0 blocks nobody |
| The hit | Recorded in `hm_spamtraphits` and counted in `hmailserver_spam_trap_hits_total` | `SpamTrapHitRetentionDays`, 90 |

The application log gets one line per hit and one per ban:

```
"Auto-ban: 203.0.113.9 is banned for 1440 minutes after addressing the spam trap never-used@example.com"
```

[The packaged fail2ban filter](/topic/28220) matches that line, so a Linux installation that runs fail2ban with the packaged jail turned on bans the address at its own firewall as well. The jail ships disabled; the wiki's Installing on Linux page turns it on with:

```
printf '[hmailserver]\nenabled = true\n' > /etc/fail2ban/jail.d/hmailserver.local
systemctl reload fail2ban
```

**The SPF condition.** As first written, in the source tree, a trap put the envelope sender of every message sent to it on the block list, whatever that sender was; spam traps are new in 6.3.4, which carries the fix, so no release did. CHANGELOG records why that changed: the envelope is whatever the client types, so one connection naming a bank's address could have had the bank's real mail held or marked for every mailbox. Now only an SPF **pass** blocks; the address is banned either way. The wiki's summary in Stopping Spam §10.15 does not mention the condition; [docs/SpamTraps.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SpamTraps.md) and CHANGELOG do.

**Who is never banned:** an authenticated session, an address whose IP range does not run the spam tests, an address or sender the anti-spam white list covers, an incoming relay, and an address on `AutoBanNeverBan`. The sender is not blocked for the null sender, for a sender claiming a domain this server hosts, or where the server's allow list already names it.

**Training.** A trap hit trains nothing by itself, because no content arrives. With `SpamTrapTeachSpamAssassin` on (off by default; it needs SpamAssassin enabled and a spamd started with `--allow-tell`), a message sent to a real mailbox in the same transaction is taught to spamd as spam.

**Declaring a trap.** It must be an address in the domain it is declared on, and one nobody uses: an account, alias or list with that address is refused, and none can be given a trap's address while the trap exists.

| Where | How |
|---|---|
| Control Deck | Domain editor → **Spam traps**; the **Spam traps** view for the record across every domain |
| REST | `GET`/`POST /api/v1/domains/{domain}/spam-traps`, `DELETE /api/v1/spam-traps/{address}`, `GET`/`DELETE /api/v1/spam-traps/hits` |
| Command line | `hmctl spamtrap create --domain example.com --set address=never-used@example.com`, `hmctl traphits list` |
| PowerShell | `Add-HmSpamTrap`, `Get-HmSpamTrapHit` |
| COM (Windows only) | `Domain.AddSpamTrap`, `Utilities.GetSpamTrapHits` |

A good trap is an address never published anywhere, or one published once, long ago, and unused for years (give it a year of refusals first). A bad trap is a typo of a real address such as `saels@`, or any address in a domain with a catch-all: there every other unknown address is accepted and the trap is not, which is the difference a sender can measure. Clearing the record of hits undoes nothing; the bans and blocks stay until they expire or are removed. The traps are in the backup with their domain; the record of hits is not.

### Country blocking

*Since 6.3.4 (schema 6052).* Two kinds of policy, in one grammar. A policy **per listener family** (SMTP, IMAP, POP3, and the HTTP listeners) is applied when a connection is accepted: before the greeting, before the TLS handshake and before anything touches the database. A policy **per domain** is applied when somebody signs in to an account of that domain, on any protocol, and covers OAuth2 bearer tokens, client certificates, Kerberos tickets, passkeys and single sign-on as well as passwords.

| Value | What it means |
|---|---|
| *(empty)* | Nothing is judged by country. The default everywhere |
| `allow:GB,IE,FR` | Only these countries may connect, or sign in |
| `block:CN,RU` | These countries may not connect, or sign in |

**The database is yours to supply.** Point `GeoIpDatabaseFile` at a MaxMind `.mmdb` file, such as `/var/lib/GeoIP/GeoLite2-Country.mmdb` or `C:\GeoIP\GeoLite2-Country.mmdb`. The server ships none and never fetches one. Replace the file in place and the server reloads it within five minutes. `GeoIpDatabaseMaxAgeDays` (45) is how old the file's build date may be before the log says so; a stale database is still used.

**What is exempt.** `CountryBlockingExemptRanges` defaults to the loopback addresses and every private network:

```
127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,fe80::/10,fc00::/7
```

There is no second list compiled into the server. If you empty it, do so with `country_policy_http` off, or the Control Deck is the next thing you lose. An open session is never disturbed; a policy takes effect on the next connection.

**Which way it fails.** With no database at all (none configured, or the file missing or unreadable) **everything is allowed**, whatever the policies say, and the reason is logged once. With a database loaded that has no country for an address, an allow list refuses it and a block list allows it.

**What a refused client is told: nothing.** A refused connection is closed with no banner. A refused sign-in gets the ordinary invalid-credentials reply, byte-identical to a wrong password, and is counted against neither the account's lockout nor the address's auto-ban, since the person refused is very often the account's real owner, travelling. Your copy is in the application log:

```
Country blocking: refused SMTP from 203.0.113.9 (CN) for connection.
Country blocking: refused sign-in from 203.0.113.9 (CN) for example.com.
```

The counters are `hmailserver_country_blocked_total` and `hmailserver_country_blocked_by_country_total{country="CN"}`, and the report is `GET /api/v1/reports/countries`.

**Setting it up.** In the Control Deck, **Settings → Server** holds the server's settings and **Domains → (a domain) → Country blocking** the domain's policy. From a shell:

```
hmctl settings set --set geoip_database_file=/var/lib/GeoIP/GeoLite2-Country.mmdb
hmctl settings set --set country_policy_smtp=block:CN,RU
hmctl domain update example.com --set active=true --set country_policy=allow:GB,IE
```

The REST keys are `country_policy_smtp`, `country_policy_imap`, `country_policy_pop3`, `country_policy_http` and, on a domain, `country_policy`. Over COM (Windows only) it is `Settings.CountryPolicy("smtp")` and `Domain.CountryPolicy`.

**What it does not do.** It is not a per-message control. It says nothing about a proxy: [behind HAProxy with the PROXY protocol](/topic/28240), the address judged is the proxy's, so put the policy on the proxy. And it is only as true as the database; it is not an authentication control.

### Impersonation protection

*Since 6.3.4 (schema 6087).* Per domain, off on every existing and new domain. It runs at local delivery on the recipient's own copy, only on mail from **outside**, and makes two checks.

**A borrowed name.** The `From` display name, folded, equals the name of an active account of the recipient's domain or of a **protected person**, while the address is not theirs.

**A lookalike domain.** The `From` domain, and the envelope sender's when it differs, is equal to a hosted domain, an alias domain or a **protected partner** after the confusable folding while not equal before it (`examp1e.test`, `exarnple.test`), or is one Damerau-Levenshtein edit away inside one label (`examplee.test`, `exampel.test`). Distance 1 is the rule and 2 is not. A DMARC pass changes nothing here: the attacker publishes the lookalike's records exactly as a legitimate domain would.

```
X-hMailServer-Impersonation: name "Ada L0velace" matches Ada Lovelace (ada@example.test); from ada.lovelace@evil.test
X-hMailServer-Impersonation: domain examp1e.test looks like example.test; from invoices@examp1e.test
```

**Windows and Linux differ in one step of the folding.** Unicode NFKC normalisation, which turns a full-width or ligature spelling into its letters, runs on Windows. It does **not** run on Linux, which has no Unicode tables in this build; there the step is skipped and the confusable table does the rest. The other steps run on both.

**What slips past.** Word order and decorations are not folded: `Holloway, Chris` and `Chris Holloway (CEO)` are not `Chris Holloway`. A name carried only in the local part of the address is not compared. And a domain with an account named **Support** or **Info** will have every vendor's "Support" flagged.

| `impersonation_action` | What happens |
|---|---|
| `off` (0) | Nothing is read |
| `flag` (1) | The copy gets the `X-hMailServer-Impersonation` header. The subject is left alone and the sender's DKIM signature is untouched. The webmail shows a warning bar and a **Suspicious sender** mark |
| `quarantine` (2) | The copy goes to the quarantine with the reason and score 0. A release does not re-check. With `QuarantineEnabled` off, the domain gets flag instead |

**What is exempt:** a sender the recipient's own safe-sender list allows (or the domain's or server's allow list, where nothing nearer decides), and a `From` domain that is itself hosted here or a protected partner. Nothing else: a person's own name from their personal address at another provider is flagged until that address is on the safe-sender list.

The setting is on the **Impersonation protection** tab of the Control Panel's domain dialog (Windows only), in the Deck's domain editor, and on `PUT /api/v1/domains/{domain}` as `impersonation_action`, `protected_persons` and `protected_partners`. Each list holds at most 2,000 characters.

### Where to read more

The score-based pipeline these layers sit beside is in [the anti-spam stack end to end](/topic/28248), the outside-sender tag in [the topic on mail flow rules](/topic/28251), and the auto-ban a trap uses in [IP ranges, auto-ban, lockout and tarpits](/topic/28239). For the Deck itself, see [The Control Deck](/topic/28192).

- Wiki, §10.10a and §10.15: https://gitlab.com/hmailserver/hmailserver/-/wikis/Stopping-Spam
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SenderListsAndDigests.md
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SpamTraps.md
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/CountryBlocking.md
- https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ImpersonationProtection.md
- Public documentation: https://www.progressiverobot.com/hmailserver-documentation/#104-white-lists
