File Expressions
File Expressions
Admin path: Content Checks > File Expressions
(view_file_expressions.cfm,
inc/get_file_expressions.cfm,
inc/update_amavis_config_files.cfm).
This page maintains the catalogue of regex patterns that Amavis
can match against attachment filenames. Where
File Extensions is a one-extension-per-row list
(.exe, .docm, .iso), File Expressions is the free-form regex
sibling — any Perl-compatible pattern that should fire on the
attachment name: double-extension traps (^.+\.(exe|scr)\.[a-z0-9]+$),
disguised-archive patterns (^invoice.*\.pdf\.zip$), or any
project-specific filename signature an extension list can't express.
The page itself does not block anything — it only registers patterns.
The block / allow decision is taken by a File Rule
that bundles expressions (and extensions, file types, MIME types)
into a named ruleset, which is then bound to recipient traffic via
an SVF policy on Anti-Spam Settings.
The expression catalogue is entirely operator-driven — Hermes
ships no system-managed expressions. The shipped High-Risk catch-all
("Double Extensions in File Name") and the Windows Class ID block
live on the File Extensions page as
type = 'FILE-HIGH' rows. Everything on the File Expressions page
is something the operator added.
Where File Expressions sits
+---------------------------------------+
File Expressions | files table |
(this page) -----> | id, file ("\.exe$"), |
| description ("Executable files"), |
| type ("CUSTOM-EXPRESSION"), |
| system ("NO"), |
| allow ("[qr'\.exe$'i => 0]"), |
| ban ("[qr'\.exe$'i => 1]") |
+---------------+-----------------------+
|
v
+---------------------------------------+
| File Rules |
| bundle expressions + extensions |
| into named rulesets with per-item |
| allow / ban / priority |
+---------------+-----------------------+
|
v
+---------------------------------------+
| Anti-Spam Settings (SVF Policies) |
| bind a File Rule to recipient(s) |
| via policy.banned_rulenames |
+---------------+-----------------------+
|
v
+---------------------------------------+
| Amavis 50-user.HERMES |
| @banned_filename_re emitted per |
| rule on every save chain |
+---------------------------------------+
The rendered @banned_filename_re block is enforced at
content-filter time inside hermes_mail_filter. A matched expression
triggers Amavis's final_banned_destiny action (D_BOUNCE,
D_DISCARD, or D_PASS — set globally on
Anti-Spam Settings).
How the pattern is wrapped
The textarea takes a raw Perl regex. On save the handler wraps it
into Amavis's qr// syntax with the i (case-insensitive) modifier
and stores both the allow and ban form on the row:
[qr'\.exe$'i => 0] (allow form, stored in files.allow)
[qr'\.exe$'i => 1] (ban form, stored in files.ban)
Whether the allow or ban form gets rendered into Amavis's
@banned_filename_re is decided at File Rule time, not here. The
File Expressions page does not have an allow/ban toggle — both forms
are stored so the same expression can serve allow-rules and
ban-rules without re-typing.
There is no case-sensitive variant on this page. Every File
Expression is stored with the i modifier. Operators who need
strict case have to drop down to the File Rule's per-component
selection or use a regex character class on the pattern itself
(\.[Ee][Xx][Ee]$).
The page
A page guide callout, an Expression Helper card (build / pick / test), an Add Expressions card with a bulk textarea, and a single DataTable listing every custom expression. The DataTable is flat — system vs. custom does not apply because the catalogue is all-custom by design.
Expression Helper card
A three-section utility, collapsed by default, that exists so operators don't need to know regex to add common patterns.
| Section | Purpose |
|---|---|
| Build an Expression | Pick a match mode (Ends with / Starts with / Contains / Exact), enter plain text, click Build. The helper regex-escapes the input, wraps it with the appropriate anchors (^…, …$, ^…$), and shows the generated pattern with a plain-English explanation |
| Quick Select Common Patterns | A dropdown of pre-built patterns (\.exe$, \.bat$, ^invoice, \.(exe|bat|cmd|scr|pif)$, etc.) — click Use to drop the pattern into the Add form |
| Test a Pattern | A pattern + filename pair with a Test button — runs new RegExp(pattern, 'i').test(filename) in the browser and reports Match / No match / Invalid regex. Lets the operator sanity-check before saving |
The Build helper escapes . * + ? ^ $ { } ( ) | [ ] \ in the user
input before wrapping, so a builder entry of invoice.pdf becomes
invoice\.pdf$, not invoice.pdf$.
Add File Expressions card
| Field | Stored as | Notes |
|---|---|---|
| File Expressions | files.file (the regex) + files.description |
One per line; format is regex_pattern description where the first space separates pattern from label. A pattern with no space becomes its own description (useful for self-documenting patterns like \.docm$) |
The handler line-splits the textarea on LF or CRLF, strips
whitespace, and inserts each non-blank entry. Per entry it
checks one thing: that no row already exists in files with the
same file value under type = 'CUSTOM-EXPRESSION'. Duplicates
are skipped and surfaced in the partial-success alert
("Duplicate: \.exe$"); the rest still insert.
There is no regex-validity check on save — the regex is stored as-typed and any syntax error is exposed at Amavis reload time, not in the alert. Use the Test a Pattern section of the helper before saving to catch malformed patterns first.
File Expressions DataTable
| Column | Source |
|---|---|
| (checkbox) | Selection for bulk Delete Selected |
| Regex Pattern | files.file (rendered inside a <code> block) |
| Description | files.description |
| Actions | Per-row Delete button (single-row confirm) |
The DataTable shows only type = 'CUSTOM-EXPRESSION' rows. No
edit-in-place — to change a pattern the operator deletes it and
re-adds.
Foreign-key guard on delete
A custom expression cannot be deleted while it is referenced by any File Rule. The single-row Delete handler runs:
SELECT COUNT(*) AS cnt FROM file_rule_components
WHERE file_id = :id
If cnt > 0, the delete is refused with alert m = 40 and the
DataTable shows the offending rule name(s) ("This expression is
referenced by the following File Rule(s): Block-Disguised-Exe").
The operator's path is to open File Rules, remove the expression
from the rule, then come back here and delete it.
Bulk Delete applies the same guard per-id and accumulates partial
results — alert m = 41 reports "N deleted, M blocked" with the
blocked rows' pattern and rule names attached, so the operator knows
exactly what to unwire first.
Save and apply flow
1. View page submits action="add_entries" | "delete" | "bulk_delete"
2. For each valid entry:
a. Generate ban string: "[qr'<pattern>'i => 1]"
b. Generate allow string: "[qr'<pattern>'i => 0]"
c. INSERT INTO files (file, description, type, system, allow, ban)
with type='CUSTOM-EXPRESSION' and system='NO'
3. If at least one row was added or deleted:
a. update_amavis_config_files.cfm:
- Read /opt/hermes/conf_files/50-user.HERMES (template)
- Substitute the SERVER/destiny/DKIM/MySQL-credential
placeholders from spam_settings and creds files
- Render every File Rule's components into an
@banned_filename_re block (per-rule, in priority order,
using the allow/ban regex stored on each files row -
including the CUSTOM-EXPRESSION rows this page creates)
- Back up /etc/amavis/conf.d/50-user -> 50-user.HERMES,
move rendered file into place
b. docker exec hermes_mail_filter /etc/init.d/amavis force-reload
(30-second timeout)
4. session.m = 1 (add) | 2 (single/bulk delete) | 30 (empty submit)
| 40 (FK refused) | 41 (bulk partial)
Amavis is reloaded with force-reload rather than restarted — the
daemon re-reads 50-user without dropping connections, and mail in
flight is not interrupted. The reload step is wrapped in
cftry/cfcatch and the catch block is intentionally silent: if
the reload itself fails the DB rows are already in place, and the
next save (or a manual force-reload) will re-render. The page
does not roll back on reload failure.
Failure semantics
| Alert | Trigger |
|---|---|
m = 1 |
Add Expressions completed (with entries_added / entries_skipped / entry_errors set on session for the per-row breakdown) |
m = 2 |
Single Delete succeeded; Amavis reloaded |
m = 30 |
Add submitted with an empty textarea |
m = 31 |
Pattern field empty (legacy edit path, no longer reachable from the current UI) |
m = 32 |
Duplicate pattern (legacy edit path) |
m = 40 |
Single Delete refused — the expression is wired into at least one File Rule (rule names surfaced in the alert) |
m = 41 |
Bulk Delete partial — deleted_count rows removed, blocked_count rows refused (the per-row pattern + rule-name list is HTML-rendered into the alert body) |
The per-row error list is HTML-rendered into alert m = 1 so the
operator sees every duplicate at once. No row is silently dropped
without an explanation.
Files and containers touched
| Path | Owner | Role |
|---|---|---|
config/hermes/var/www/html/admin/2/view_file_expressions.cfm |
hermes_commandbox |
The page (add + delete + bulk delete + Expression Helper + Amavis reload) |
config/hermes/var/www/html/admin/2/inc/get_file_expressions.cfm |
hermes_commandbox |
Loads type = 'CUSTOM-EXPRESSION' rows into the DataTable |
config/hermes/var/www/html/admin/2/inc/update_amavis_config_files.cfm |
hermes_commandbox |
Renders 50-user from template + File Rules (called on every change here too — expression edits affect rendered @banned_filename_re blocks) |
config/hermes/opt/hermes/conf_files/50-user.HERMES |
hermes_commandbox (read) -> hermes_mail_filter (live /etc/amavis/conf.d/50-user) |
Canonical Amavis template; receives the rendered @banned_filename_re blocks |
/etc/amavis/conf.d/50-user |
hermes_mail_filter |
Live Amavis config; reloaded with force-reload on every save |
files table, type = 'CUSTOM-EXPRESSION' |
hermes_db_server (hermes DB) |
Source of truth for the expression catalogue |
file_rule_components table |
hermes_db_server (hermes DB) |
Cross-reference checked by the delete guard |
hermes_mail_filter container |
— | Hosts Amavis; receives force-reload (not restart) on every change |
Operational consequences
- No regex validation at save. A malformed regex inserts cleanly
and only surfaces at Amavis reload time. The reload itself does
not roll back the DB. If reload starts failing immediately after
an Add, the most recent expression is the suspect — open it,
paste it into the Test a Pattern helper, and look for unescaped
metacharacters or unbalanced groups. The pattern with
\.exe$works; a typo of\.exe$.(trailing dot) parses but matches nothing. - Case is always insensitive. Every expression renders with the
imodifier. There is no per-expression case toggle. Operators who need strict case have to encode it in the pattern itself. - Order does not matter on this page. Expressions are stored
flat. The evaluation order that Amavis sees is decided by the
File Rule that bundles them — each component's
prioritycolumn onfile_rule_components. Changing the description here will not reorder anything. - Custom-Expression rows are visible to File Rules under "Custom Expressions". When the operator opens the Add/Edit modal on File Rules, every row this page creates shows up in the Custom Expressions card alongside the system catalogue. That is the only place the bundling happens.
Related
- File Extensions — sibling page for plain
extension entries (
.exe,.docm); the simpler half of the samefilestable, distinguished bytype IN ('EXT', 'EXT-HIGH') - File Rules — bundles extensions and expressions into named, prioritised rulesets; the consumer of every row this page creates
- Message Rules — content-level SpamAssassin rules (header / body / regex) — the body / header equivalent of what File Expressions does for attachment names
- Anti-Spam Settings — defines
final_banned_destiny(what Amavis does with a banned-expression match) and binds File Rules to recipients via SVF Policies - Antivirus Settings — ClamAV runs in the same Amavis pass; a virus verdict on the same attachment overrides the banned-expression result
- Score Overrides — sibling Amavis tuning page; both write into Amavis configuration but expression matches are categorical (matched -> banned) where SA rules are weighted
- ARC Settings — note that banned-expression rejections are a body-side filter result, not an authentication result — they fire after ARC chain evaluation
- Message History — a banned-expression
rejection appears with Type
Bannedand the matched expression surfaced in the detail view - System Logs — Amavis logs the
matched regex as
Blocked BANNED (\.exe$,…)on theamavis[...]:line