# Antispam Settings

# Antispam Settings

Admin path: **Content Checks > Antispam Settings**
(`view_antispam_maintenance.cfm`, `inc/get_spam_settings.cfm`,
`inc/spam_settings_save.cfm`, `inc/update_amavis_config_files.cfm`,
`inc/update_spamassassin_config_files.cfm`, `inc/restart_amavis.cfm`,
`inc/restart_spamassassin.cfm`, `inc/antispam_init_pyzor.cfm`,
`inc/antispam_init_razor.cfm`, `inc/antispam_clear_bayes.cfm`).

This page configures the SpamAssassin engine that Amavis calls inside
`hermes_mail_filter` for every message that clears the SMTP-time
perimeter, plus the Amavis-level handling policies that decide what
happens to a message once it has been scored or otherwise classified.
Per-rule weight adjustments live on [Score Overrides](https://docs.deeztek.com/books/administrator-guide/page/score-overrides);
this page is engine settings and quarantine destiny only.

## Where SpamAssassin sits in the flow

```
                  +-----------------------------------+
   inbound msg -->| Perimeter Checks pass             |
                  +---------------+-------------------+
                                  |
                                  v
                  +-----------------------------------+
                  |  Postfix smtpd_proxy_filter       |
                  |    -> hermes_mail_filter:10024    |
                  +---------------+-------------------+
                                  |
                                  v
                  +-----------------------------------+
                  |  Amavis (hermes_mail_filter)      |
                  |   - ClamAV virus scan             |
                  |   - SpamAssassin scoring          |
                  |       DCC / Razor / Pyzor net DBs |
                  |       Bayes statistical engine    |
                  |       custom rules + scores       |
                  |   - banned-file checks            |
                  |   - final_*_destiny -> quarantine/DSN/discard
                  +---------------+-------------------+
                                  |
                                  v
                  +-----------------------------------+
                  |  Re-inject -> hermes_postfix_dkim:10026
                  +-----------------------------------+
```

A virus verdict from ClamAV always pre-empts the spam score; the
`final_virus_destiny` setting on this page decides what Amavis does
with that already-classified virus. The `final_spam_destiny`,
`final_banned_destiny`, and `final_bad_header_destiny` settings work
the same way for the other three Amavis verdict categories.

## Container and tool placement

| Component | Detail |
| --- | --- |
| Container | `hermes_mail_filter` (IPv4 `.105`) |
| Engine | SpamAssassin (`spamd` / `Mail::SpamAssassin` Perl modules called from Amavis) |
| Amavis config | `/etc/amavis/conf.d/50-user` (rendered from `/opt/hermes/conf_files/50-user.HERMES` on every save) |
| SpamAssassin config | `/etc/spamassassin/local.cf` (rendered from `/opt/hermes/conf_files/local.cf.HERMES` on every save) |
| Bayes DB | Lives in the SpamAssassin user dir inside `hermes_mail_filter` (`sa-learn --dump magic` reports the actual path) |
| Network plugin state | `/etc/razor/identity` (Razor), Pyzor's per-user config dir, DCC's local socket, all inside `hermes_mail_filter` |
| Reload mechanism | `spamassassin --lint` + `docker container restart hermes_mail_filter` on every save |

The container exposes **no host ports**. Amavis is reached only by
Postfix internally at `hermes_mail_filter:10024` and re-injects to
`hermes_postfix_dkim:10026`.

## Spam Detection Plugins card

Three boolean toggles enable third-party network-aware spam DBs.
Storage: `spam_settings.value` for parameters `use_dcc`, `use_razor2`,
`use_pyzor` (each row keyed by `parameter`, value `0` or `1`).

| Plugin | What it does | Maintenance action |
| --- | --- | --- |
| DCC (Distributed Checksum Clearinghouse) | Fuzzy-checksum bulk-mail detection; matches a message against a network of receivers' checksum counters | None. `cdcc` runs as part of the SpamAssassin call chain |
| Razor2 (Vipul's Razor v2) | Collaborative spam catalog; checksum + signature lookup against the Razor network | **Initialize Razor** (see Maintenance) before first use |
| Pyzor | Collaborative digest-based spam detection | **Initialize Pyzor** before first use |

Each toggle substitutes into `local.cf` via the placeholders `USE-DCC`,
`USE-PYZOR`, `USE-RAZOR2` -> `use_dcc 0|1`, `use_pyzor 0|1`,
`use_razor2 0|1`.

> **Operational consequence: network DB connectivity.** All three
> plugins make outbound queries (DCC over UDP, Razor and Pyzor over
> TCP) at scan time. If outbound to the public Internet is blocked
> from `hermes_mail_filter`, the plugins quietly time out per
> message and add measurable per-scan latency. Disable plugins the
> gateway cannot actually reach.

### Defaults

**All three ship disabled from v260807.** They are effective, but each one
transmits a digest or checksum of every scanned message to a third-party
network. Whether that is acceptable is a decision for the operator, so
Hermes does not make it silently. Installs predating v260807 had all three
enabled in the database while Razor was never registered, so it returned no
result regardless.

Enabling a plugin the gateway has not been provisioned for is harmless but
useless; see the maintenance actions above.

### DCC is not included in the published image

DCC is **absent from the shipped `hermes_mail_filter` image**, so leaving
`use_dcc` enabled has no effect unless you add it yourself.

This is a licensing constraint, not a technical one. The DCC licence is free
only to organisations that "do not sell filtering devices or services except
to their own users," and it does not permit redistributing binaries. Most
self-hosted operators filtering their own mail **do** qualify; Deeztek sells
Hermes Pro and does not, so we cannot ship the binaries on your behalf.

To add it, uncomment the `build:` block for `hermes_mail_filter` in
`docker-compose.yml` and rebuild:

```bash
docker compose build hermes_mail_filter
docker compose up -d hermes_mail_filter
```

Then set **DCC = Enabled** on this page. The build fetches DCC directly from
Rhyolite, so your relationship with that licence is your own.

> **Re-run the rebuild after every Hermes upgrade.** The update orchestrator
> pulls images, which replaces your locally built one and removes DCC until
> you rebuild.

## Subject Tagging card

Single field, `sa_spam_subject_tag` in `spam_settings`. Substitutes
into `50-user` via the `sa-spam-subject-tag` placeholder, which sets
Amavis's `$sa_spam_subject_tag`. Default `[SUSPECTED SPAM]`. Required
(empty value rejected with error 2). Only applied when
`sa_spam_modifies_subj = 1` (a fixed value in `spam_settings`, not
exposed in the UI).

## Message Handling Policies card

Four radio pairs, one per Amavis verdict category. Each row stores
`D_DISCARD` or `D_BOUNCE` in `spam_settings.value` and substitutes
into `50-user` via `final-<category>-destiny`. Amavis acts on the
value as follows:

| Setting | DB row | "Quarantine Only" (`D_DISCARD`) | "Quarantine & Send DSN" (`D_BOUNCE`) |
| --- | --- | --- | --- |
| Virus Messages | `final_virus_destiny` | Message goes to quarantine; no DSN | Message goes to quarantine; DSN sent to envelope sender |
| Banned File Messages | `final_banned_destiny` | Same as above for banned-file matches | DSN sent |
| Spam Messages | `final_spam_destiny` | Quarantined silently | DSN sent |
| Bad-Header Messages | `final_bad_header_destiny` | Quarantined silently | DSN sent |

The labels are deliberately conservative; `D_DISCARD` does **not**
delete the message, it routes it to Amavis's quarantine where Message
History can review and release it. Defaults: virus + banned send DSN;
spam + bad-header quarantine silently.

> **Operational consequence: Send DSN on spam.** Setting
> `final_spam_destiny = D_BOUNCE` means Hermes will deliver a
> non-delivery report to the envelope sender of every quarantined
> spam. Because the envelope sender is almost always forged on spam,
> the DSN will either bounce, contribute to backscatter against
> innocent third parties, or land in a victim's spam folder. The
> safe default for spam is `D_DISCARD`; reserve DSN for virus and
> banned-file (where the sender is more likely to be legitimate).

## Bayes Database card

SpamAssassin's per-installation statistical learning engine. Three
controls, stored in `spam_settings`:

| Field | DB row | Substitution placeholder | Effect |
| --- | --- | --- | --- |
| Enable Bayes Database | `use_bayes` | `USE-BAYES` -> `use_bayes` followed by `0` or `1` | Master switch; when off, Bayes rules contribute no score |
| Enable Auto-Learning | `bayes_auto_learn` | `BAYES-AUTO-LEARN` -> `bayes_auto_learn` followed by `0` or `1` | When on, SpamAssassin trains the Bayes DB automatically based on the message's final score relative to the thresholds below |
| Spam Threshold | `bayes_auto_learn_threshold_spam` | `BAYESAUTOLEARN-SPAM` -> `bayes_auto_learn_threshold_spam <value>` | Final score above which auto-learn treats the message as spam. Must be numeric and in the range `0.01 .. 999` |
| Non-Spam Threshold | `bayes_auto_learn_threshold_nonspam` | `BAYESAUTOLEARN-HAM` -> `bayes_auto_learn_threshold_nonspam <value>` | Final score below which auto-learn treats the message as ham. Must be numeric and in the range `-999 .. -0.01` |

The thresholds are SpamAssassin's `bayes_auto_learn_threshold_spam`
and `bayes_auto_learn_threshold_nonspam` directives. JavaScript on
the page collapses the thresholds when Bayes or auto-learning is
disabled.

### Defaults and first use

From v260807, `use_bayes` ships **enabled** and `bayes_auto_learn` ships
**disabled**. Bayes is local-only (it transmits nothing anywhere), so
leaving it on costs nothing, but it is trained deliberately rather than
automatically for the reason described below.

The database ships **empty**. SpamAssassin applies no Bayes score at all
until it has learned roughly **200 spam and 200 ham** messages; below that
threshold it stays silent rather than guessing from thin evidence. A new
gateway therefore shows Bayes as enabled and contributing nothing, which is
correct behaviour and not a fault.

Train it from Quarantine and Message History with **Train as Spam** and
**Train as Ham**. Mailbox users can train from their own portal as well, so
the corpus grows from real classification decisions rather than from the
rule set's guesses.

> **Installs predating v260807** shipped with a pre-trained Bayes corpus
> built from unrelated mail between 2020 and 2025. Because SpamAssassin
> merges all learning into a single token store, anything trained locally
> since was inseparable from it. The v260807 upgrade clears the database
> once so the gateway learns from its own traffic.

Auto-learning remains available but is not recommended for general use.

> **Operational consequence: Bayes poisoning.** Auto-learning
> trusts the final score (which already includes Bayes's own
> contribution) to decide whether to train. A bad spam wave that
> sneaks past the score threshold can train Bayes to think more spam
> is ham, which lowers detection on the next batch. If detection
> quality regresses noticeably after enabling auto-learning, use the
> Clear Bayes Database action and re-train manually or via a
> known-good corpus before re-enabling.

## Save flow

```
1. View page submits action="save_settings" (all four cards in one POST)
2. spam_settings_save.cfm validates:
     - sa_spam_subject_tag non-empty (error 2)
     - if bayes_auto_learn=1:
         spam threshold numeric (error 5), > 0 and <= 999 (error 4),
                 non-empty (error 3)
         non-spam threshold numeric (error 10), < 0 and >= -999 (error 8),
                 non-empty (error 7)
3. On valid input, UPDATEs 13 rows in spam_settings (sa_spam_subject_tag,
   four final_*_destiny, use_bayes, bayes_auto_learn, both thresholds,
   use_dcc, use_razor2, use_pyzor)
4. cfinclude update_amavis_config_files.cfm:
     - Reads /opt/hermes/conf_files/50-user.HERMES
     - Substitutes SERVER-NAME, SERVER-DOMAIN, sa-spam-subject-tag,
       final-{virus,banned,spam,bad-header}-destiny,
       enable-dkim-{verification,signing},
       HERMES-USERNAME, HERMES-PASSWORD,
       FILE-RULES-GO-HERE (from file_rule_components table),
       DKIM-KEYS-GO-HERE (from dkim_sign table)
     - Backs up /etc/amavis/conf.d/50-user -> 50-user.HERMES.BACKUP
     - Moves rendered file into place
5. cfinclude update_spamassassin_config_files.cfm:
     - Reads /opt/hermes/conf_files/local.cf.HERMES
     - Substitutes USE-DCC, USE-PYZOR, USE-RAZOR2, USE-BAYES,
       BAYES-AUTO-LEARN, BAYESAUTOLEARN-SPAM, BAYESAUTOLEARN-HAM
     - Appends per-rule score lines (from spam_settings where spamfilter=1)
     - Appends custom message rules (from message_rules table)
     - Backs up /etc/spamassassin/local.cf -> local.cf.HERMES.BACKUP
     - Moves rendered file into place
6. cfinclude restart_amavis.cfm -> restart_mail_filter.cfm:
     - docker container restart hermes_mail_filter
7. cfinclude restart_spamassassin.cfm:
     - docker exec hermes_mail_filter /usr/bin/spamassassin --lint
     - docker container restart hermes_mail_filter
8. session.m = 1 -> green "Anti-spam settings have been saved and applied" alert
9. cflocation back to view_antispam_maintenance.cfm
```

The same container is restarted twice (once for Amavis, once for
SpamAssassin) because the restart includes are intentionally
independent helpers used elsewhere; both calls resolve to the same
`docker container restart hermes_mail_filter`. Outbound mail queues
briefly during the restart cycle (typically a few seconds); Postfix
will retry.

## Maintenance card group

Three buttons, each running a single `docker exec` against
`hermes_mail_filter` and surfacing stdout/stderr to the operator.

### Initialize Pyzor

Action handler: `antispam_init_pyzor.cfm`

```
docker exec -u amavis hermes_mail_filter /usr/bin/pyzor ping
```

Pings the Pyzor servers; success is detected by the literal string
`200` in the output. It runs as **amavis** because that is the user
SpamAssassin invokes pyzor as, so it tests the identity that actually
matters rather than root's.

This is a connectivity check, not a registration step. Pyzor writes no
per-user configuration here and reaches `public.pyzor.org` from a
built-in default, so enabling `use_pyzor = 1` is sufficient on its own.
Contrast Razor, which does need a one-time registration and stores an
identity file that SpamAssassin locates via `razor_config`.

### Initialize Razor

Action handler: `antispam_init_razor.cfm`

```
docker exec hermes_mail_filter /bin/bash -c \
  'rm -f /etc/razor/identity && razor-admin -create && razor-admin -register'
```

Deletes the existing Razor identity, creates a fresh config, and
registers the gateway with the Razor network. Success is detected by
`Register successful` or `created` in the output. Re-run if Razor
queries start failing (typically after the identity is rotated or the
network rejects the existing identity).

### Clear Bayes Database

Action handler: `antispam_clear_bayes.cfm`

```
docker exec -u amavis hermes_mail_filter /usr/bin/sa-learn --clear
```

Every `sa-learn` call in Hermes runs as **amavis**, in the admin console, the
user portal and the upgrade hook alike. SpamAssassin reads and writes the Bayes
database as amavis, so training as root left root-owned files in a directory
amavis must write to. It worked only because `bayes_file_mode 0777` made them
world-writable.

Wipes the learned spam/ham corpus. SpamAssassin will need to re-learn
from scratch before Bayes rules contribute meaningful scores again.
Use only when the database is known-poisoned or when migrating between
servers without preserving training. The button is gated behind a
JavaScript `confirm()` and renders inside a yellow warning card.

You do **not** need to run this after upgrading to v260807; that upgrade
clears the previously shipped corpus once, automatically. Fresh v260807
installs start with an empty database and are unaffected.

## Failure semantics

| Failure | Behavior |
| --- | --- |
| Empty `sa_spam_subject_tag` | session.m=2, red alert, no save |
| Bayes spam threshold empty | session.m=3 |
| Bayes spam threshold not numeric | session.m=5 |
| Bayes spam threshold <= 0 or > 999 | session.m=4 |
| Bayes non-spam threshold empty | session.m=7 |
| Bayes non-spam threshold not numeric | session.m=10 |
| Bayes non-spam threshold >= 0 or < -999 | session.m=8 |
| Any cfcatch during the save -> apply chain | session.m=9, red alert with `session.saveError` showing `cfcatch.message` |
| `spamassassin --lint` failure during restart | `error.cfm` cfabort with the lint failure message; the rendered `local.cf` is already in place but Amavis is not restarted further |
| Pyzor ping output without `200` | session.m=12, red alert; full output shown in a `<pre>` for diagnosis |
| Razor init output without `Register successful` or `created` | session.m=14, similar surfacing |
| Bayes clear `cfcatch` | session.m=16 with the catch message |

`spamassassin --lint` is the canonical pre-restart sanity check:
when a custom rule (added via Score Overrides or message rules) has
invalid syntax, the lint catches it before the container restart
finishes and prevents Amavis from starting against a broken config.

## Files and containers touched

| Path | Owner | Role |
| --- | --- | --- |
| `config/hermes/var/www/html/admin/2/view_antispam_maintenance.cfm` | `hermes_commandbox` | The page |
| `config/hermes/var/www/html/admin/2/inc/spam_settings_save.cfm` | `hermes_commandbox` | Validation + UPDATE + apply chain |
| `config/hermes/var/www/html/admin/2/inc/get_spam_settings.cfm` | `hermes_commandbox` | Loads current `spam_settings` rows |
| `config/hermes/var/www/html/admin/2/inc/update_amavis_config_files.cfm` | `hermes_commandbox` | Renders `50-user` from template + DB |
| `config/hermes/var/www/html/admin/2/inc/update_spamassassin_config_files.cfm` | `hermes_commandbox` | Renders `local.cf` from template + DB |
| `config/hermes/var/www/html/admin/2/inc/restart_amavis.cfm` / `restart_spamassassin.cfm` / `restart_mail_filter.cfm` | `hermes_commandbox` | `docker container restart hermes_mail_filter` |
| `config/hermes/var/www/html/admin/2/inc/antispam_init_pyzor.cfm` / `antispam_init_razor.cfm` / `antispam_clear_bayes.cfm` | `hermes_commandbox` | Maintenance docker-exec helpers |
| `config/hermes/opt/hermes/conf_files/50-user.HERMES` | template (read) -> `hermes_mail_filter` (live `/etc/amavis/conf.d/50-user`) | Amavis directives template |
| `config/hermes/opt/hermes/conf_files/local.cf.HERMES` | template (read) -> `hermes_mail_filter` (live `/etc/spamassassin/local.cf`) | SpamAssassin directives template |
| `/etc/amavis/conf.d/50-user.HERMES.BACKUP` | `hermes_mail_filter` | Pre-write backup, refreshed each save |
| `/etc/spamassassin/local.cf.HERMES.BACKUP` | `hermes_mail_filter` | Pre-write backup, refreshed each save |
| `spam_settings` table | `hermes_db_server` (`hermes` DB) | Source of truth for every UI value on this page; also holds per-rule scores (`spamfilter=1` rows) for Score Overrides |
| `message_rules` table | `hermes_db_server` | Custom header/body/full message rules; rendered into `local.cf` |
| `file_rule_components` / `files` tables | `hermes_db_server` | Banned-file rules; rendered into `50-user` |
| `dkim_sign` table | `hermes_db_server` | Per-domain DKIM keys; rendered into `50-user` for outbound signing |

## Related

- [Antivirus Settings](https://docs.deeztek.com/books/administrator-guide/page/antivirus-settings) -- the ClamAV engine
  that runs in the same Amavis pass and whose virus verdict always
  pre-empts the spam score
- [Malware Feeds](https://docs.deeztek.com/books/administrator-guide/page/malware-feeds) -- third-party ClamAV signature
  feeds; orthogonal to spam scoring but consumed in the same scan
- [Score Overrides](https://docs.deeztek.com/books/administrator-guide/page/score-overrides) -- per-rule SpamAssassin
  weight adjustments (the `spamfilter=1` rows in `spam_settings`);
  this page sets the engine knobs, that page sets the rule weights
- [Message Rules](https://docs.deeztek.com/books/administrator-guide/page/message-rules) -- custom header / body / full
  message regex rules that ride into `local.cf` on every save here
- [SVF Policies](https://docs.deeztek.com/books/administrator-guide/page/svf-policies) -- per-sender and per-recipient
  spam-handling overrides that apply before the engine-wide
  `final_*_destiny` settings on this page
- [Perimeter Checks](https://docs.deeztek.com/books/administrator-guide/page/perimeter-checks) -- the SMTP-time gate;
  every check on this page runs only after a connection clears the
  perimeter
- [ARC Settings](https://docs.deeztek.com/books/administrator-guide/page/arc-settings) -- seals over the body Amavis
  passed, so a high spam score (and any quarantine action) naturally
  pre-empts the seal
- [DMARC Settings](https://docs.deeztek.com/books/administrator-guide/page/dmarc-settings) -- a DMARC-fail verdict can
  promote a message to a higher spam score via SpamAssassin's DMARC
  rule weights (tunable on Score Overrides)
- [Scheduled Tasks](https://docs.deeztek.com/books/administrator-guide/page/scheduled-tasks) -- `sa-update`
  for the SpamAssassin rule set runs on its own Ofelia schedule; the
  Bayes DB is per-installation and not updated by `sa-update`
- [Email flow](https://docs.deeztek.com/books/installation-reference/page/hermes-seg-email-flow) -- full pipeline diagram