Score Overrides
Score Overrides
Admin path: Content Checks > Score Overrides
(view_score_overrides.cfm,
inc/update_spamassassin_config_files.cfm,
inc/update_amavis_config_files.cfm,
inc/restart_spamassassin.cfm,
inc/restart_amavis.cfm).
This page tunes the per-rule scores that SpamAssassin contributes to each message's total. SpamAssassin ships with thousands of named rules; each rule that matches a message adds (or subtracts) a default score, and the message is tagged or quarantined when the running total crosses the global threshold configured on Anti-Spam Settings. Score Overrides is where the operator says "this rule should weigh more / less / not at all for our mail." The threshold itself is not changed here.
Every entry written on this page lands in SpamAssassin's local.cf
as a score <RULE_NAME> <value> line. SpamAssassin reads local.cf
on daemon start, and the override takes precedence over the
shipped default the rule was defined with.
Where Score Overrides sits
+---------------------------------------+
inbound msg ------->| Amavis content-filter pass |
| - ClamAV (virus verdict pre-empts) |
| - SpamAssassin SCORING |
| rule_A 0.3 |
| rule_B 1.2 |
| rule_C 4.0 <-- per-rule |
| ... weights set |
| SUM = N here |
+---------------------------------------+
|
v
+---------------------------------------+
| Anti-Spam Settings thresholds |
| sa_tag_level |
| sa_tag2_level <-- cutoff points |
| sa_kill_level set there |
+---------------------------------------+
Score Overrides tunes the contributions; Anti-Spam Settings
tunes the cutoffs. A message reaches quarantine because the
sum of contributions crosses the cutoff — moving either side of
that equation changes behavior, and they are independent knobs.
What an override actually changes
| Override value | Effect on the rule | Use it when |
|---|---|---|
Positive (e.g. 3.5) |
Adds more to the spam score on match | A rule catches a genuine pattern your senders see often but the default score is too low to flag |
0 |
Rule still runs but contributes nothing | A rule produces too many false positives in your mail mix and you want to neuter it without ripping it out of the database |
Negative (e.g. -2.0) |
Subtracts from the spam score on match | The rule indicates legitimacy in your environment (e.g. a trusted-relay heuristic) and you want it to act as a bonus |
Setting a score to 0 is the safe equivalent of "disable this rule"
— SpamAssassin still evaluates it (so the test name still appears
in X-Spam-Status and you can confirm it fired), but the message
total is unaffected. Removing the override does not delete the
underlying SpamAssassin rule; it only stops Hermes's local.cf
from overriding the shipped default.
The page
A collapsible scoring helper (the same text the operator gets in the in-page guide), a hard-locked "DKIM and SPF rules are not evaluated" warning, an Add Override modal, a DataTable of current overrides, and an Edit / Delete modal pair.
Add Override modal
| Field | Stored as | Notes |
|---|---|---|
| Test Name | spam_settings.parameter |
The SpamAssassin rule name, uppercase with underscores (e.g. BAYES_99, HTML_MESSAGE, FREEMAIL_FROM) |
| Score | spam_settings.value |
Numeric, validated -999 <= value <= 999. Set to 0 to neuter the rule |
| Description | spam_settings.description |
Free-text label that surfaces in the DataTable; optional |
Add validates: Test Name non-blank, Score numeric and in range, the
(parameter) natural key not already present, and the rule name
not in the SPF / DKIM / ADSP plugin family (see warning below). On
success: INSERT row with spamfilter='1', active='1',
applied='1'; then immediately regenerate local.cf and
reload the engine — same chain Save uses.
Score Overrides DataTable
| Column | Source |
|---|---|
| (checkbox) | Selection for bulk Delete Selected |
| Test Name | spam_settings.parameter |
| Score | spam_settings.value |
| Description | spam_settings.description |
| Edit | Per-row pencil button -> Edit modal |
System-managed rows (system_managed = 1) get a lock icon instead
of a checkbox, a "System-managed" badge next to the test name, and
a disabled Edit button. They are filtered out of any DELETE
generated by the page even if a forged POST targets them
(AND system_managed = 0 is part of the delete query). The lock
exists for rules that encode a Hermes architectural decision — for
example, the per-rule scores Hermes maintains for the trusted-relay
Return Path lookups.
Edit Modal
Test Name is read-only — changing it is semantically a different rule and would orphan the override. Only Score and Description are editable. Save runs the same regen + reload chain as Add.
DKIM / SPF / ADSP overrides are silently meaningless
The page mounts a warning callout flagging that any override
targeting a DKIM, SPF, or ADSP rule has no effect in Hermes, and
the Add handler rejects them with alert m = 13. The rule families
covered:
DKIM_*(e.g.DKIM_INVALID,DKIM_VALID,DKIM_ADSP_ALL)SPF_*(e.g.SPF_PASS,SPF_FAIL,SPF_HELO_SOFTFAIL)- Any rule whose name contains
ADSP
The SpamAssassin DKIM and SPF plugins are intentionally not loaded
in Hermes's init.pre — the authoritative DKIM verdict is the
Authentication-Results: header that OpenDKIM writes at :25, and
the authoritative SPF verdict is the Received-SPF: header that
postfix-policyd-spf-python writes at envelope time. SpamAssassin's
in-content re-check would otherwise produce false-positive failures
against Hermes-modified bodies (External Sender Banner, disclaimer,
signature insertion) and could pick up the wrong upstream IP from
the Received chain in multi-hop scenarios (federal mail, M365 GOV
cloud, etc.). Letting an operator write an override for a rule that
literally cannot fire would silently mislead them, so the guard
runs at the Add handler.
The block is case-insensitive (UCase + Left / FindNoCase) so
mixed-case rule names cannot sidestep it.
Save and apply flow
1. View page submits action="add" | "edit" | "delete"
2. view_score_overrides.cfm validates the row (per-action rules above)
3. INSERT / UPDATE / DELETE on spam_settings (spamfilter='1'),
guarded by system_managed=0 on UPDATE and DELETE
4. update_spamassassin_config_files.cfm:
a. Read /opt/hermes/conf_files/local.cf.HERMES (template)
b. Substitute USE-BAYES, USE-DCC, USE-PYZOR, USE-RAZOR2, and
bayes_auto_learn placeholders from their own spam_settings rows
c. SELECT every spamfilter='1' active='1' row -> tmp/_sa_tests file:
score <parameter> <value>
(one line per row)
d. Substitute the #CUSTOM-TESTS placeholder in local.cf with the
rendered score list
e. Render Message Rules into the #CUSTOM-MESSAGE-RULES placeholder
f. Back up /etc/spamassassin/local.cf -> local.cf.HERMES.BACKUP,
move the rendered file into place
g. UPDATE spam_settings SET applied='1' WHERE applied='2'
5. update_amavis_config_files.cfm:
- Regenerate Amavis 50-user from template (subject tags, destinies,
DKIM-verification toggle, file rules) so a SA setting change that
also affects Amavis takes effect in the same write
6. restart_spamassassin.cfm:
- docker exec hermes_mail_filter /usr/bin/spamassassin --lint
(validation; abort on failure)
- Then docker container restart hermes_mail_filter
7. restart_amavis.cfm: same docker container restart hermes_mail_filter
(idempotent; the engine is back from step 6)
8. session.m = 1 / 7 / 8 -> success alert with "regenerated" wording
The restart in step 6 is a full container restart — hermes_mail_filter
runs SpamAssassin, ClamAV, Amavis, and Fangfrisch, all of which
re-initialize together. Inbound mail held in Postfix's queue during
the restart is retried on the next queue run; no message is lost.
Failure semantics
| Alert | Trigger |
|---|---|
m = 1 |
Add succeeded and SpamAssassin reloaded |
m = 2 |
Test Name blank |
m = 3 |
Test Name already exists |
m = 4 |
Score out of -999..999 range |
m = 5 |
Score blank |
m = 6 |
Score not numeric |
m = 7 |
Edit succeeded and SpamAssassin reloaded |
m = 8 |
Delete succeeded and SpamAssassin reloaded |
m = 10 |
Delete clicked with no rows selected |
m = 11 |
The Apply chain (regen + restart) threw — DB write may already have happened |
m = 12 |
Attempt to edit or delete a system_managed = 1 row (forged POST defense; the UI hides the action) |
m = 13 |
Add of a DKIM / SPF / ADSP family rule — rejected because the underlying plugin is disabled |
m = 11 is the partial-failure case: the DB row has already been
inserted / updated / deleted but local.cf regen or the lint /
restart step failed. The page does not roll back the DB write —
the next successful save will re-render local.cf from the current
table state, so the system is self-healing on the next click.
Finding rule names
The page guide gives the lookup steps that work for any received message:
- From Message History, open any message
and view headers; the
X-Spam-Status:header lists every rule that fired and its score - SpamAssassin rule names are uppercase with underscores
(e.g.
BAYES_99,HTML_MESSAGE,FREEMAIL_FROM,RDNS_NONE,URIBL_BLOCKED) - To see the default score and description for a rule:
docker exec hermes_mail_filter spamassassin --debug rules 2>&1 | grep -i <RULE_NAME>
Files and containers touched
| Path | Owner | Role |
|---|---|---|
config/hermes/var/www/html/admin/2/view_score_overrides.cfm |
hermes_commandbox |
The page (validation + alerts + DataTable) |
config/hermes/var/www/html/admin/2/inc/update_spamassassin_config_files.cfm |
hermes_commandbox |
Renders local.cf from template + score rows + message rules |
config/hermes/var/www/html/admin/2/inc/update_amavis_config_files.cfm |
hermes_commandbox |
Re-renders Amavis 50-user (called in the same chain to keep SA-related Amavis flags in sync) |
config/hermes/var/www/html/admin/2/inc/restart_spamassassin.cfm |
hermes_commandbox |
Lints the new local.cf then restarts hermes_mail_filter |
config/hermes/var/www/html/admin/2/inc/restart_amavis.cfm |
hermes_commandbox |
Calls restart_mail_filter.cfm |
config/hermes/opt/hermes/conf_files/local.cf.HERMES |
hermes_commandbox (read) -> hermes_mail_filter (live /etc/spamassassin/local.cf) |
Canonical template with ##CUSTOM-TESTS and ##CUSTOM-MESSAGE-RULES placeholders |
/etc/spamassassin/local.cf |
hermes_mail_filter |
Live file SpamAssassin reads at engine start |
/etc/spamassassin/local.cf.HERMES.BACKUP |
hermes_mail_filter |
Pre-write backup taken every save |
spam_settings table, spamfilter = '1' |
hermes_db_server (hermes DB) |
Source of truth for every override (and for the Bayes / DCC / Razor / Pyzor / threshold values used by Anti-Spam Settings) |
hermes_mail_filter container |
— | Hosts SpamAssassin, ClamAV, Amavis, Fangfrisch — restarted as a unit on every save |
Related
- Anti-Spam Settings — sets the GLOBAL spam
thresholds (
sa_tag_level,sa_tag2_level,sa_kill_level); the cutoffs that the per-rule contributions tuned here are summed against - Message Rules — custom SpamAssassin rules
(header / body / regex) written into the same
local.cfvia the##CUSTOM-MESSAGE-RULESplaceholder during the same regen cycle - Antivirus Settings — ClamAV runs in the same Amavis pass; a virus verdict pre-empts any spam-score result
- Perimeter Checks — SMTP-time rejects that fire before SpamAssassin ever sees the message
- File Extensions / File Expressions / File Rules — Amavis attachment filtering that runs alongside SpamAssassin scoring in the same pass
- DMARC Settings / ARC Settings — every rule in the DKIM / SPF family is the authoritative verifier whose verdict the warning callout refers back to
- Scheduled Tasks — Bayes auto-learn and signature refresh cadence are scheduled here, not on the Score Overrides page
- System Logs — every rule fire and
its score appears in
mail.logunder theamavis[...]:lines, prefixedtests=...