# 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.

## 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.

> **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 hermes_mail_filter /usr/bin/pyzor ping
```

Pings the Pyzor servers; success is detected by the literal string
`200` in the output. The command both verifies connectivity and writes
the per-user Pyzor config the first time it runs. Required before
`use_pyzor = 1` returns meaningful results.

### 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 hermes_mail_filter /usr/bin/sa-learn --clear
```

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.

## 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