Let me look at the milter.md docs in full detail, especially about how it’s attached to a stage/enable expression.
I now have enough to give a well-grounded, actionable answer.
Start by confirming Stalwart is actually invoking the milter code path at all, since the milter integration is data-driven from live server state, not restart-required config files.
1. Check the live milter events, not just Rspamd’s counters. Stalwart logs every milter action at info level under the milter.* event group (milter.action-accept, milter.action-reject, milter.action-temp-fail, milter.io-error, milter.timeout, etc.). Watch the live trace stream (/api/telemetry/traces/live in the WebUI’s telemetry view, or stalwart-cli logs) while re-sending the EICAR test. If you see zero milter.* events for that session, Stalwart never called run_milters() for the DATA stage, meaning the enable expression evaluated to false or the object wasn’t loaded — this is a config problem, not a network problem.
2. Verify the enable expression is matching the actual listener id, not a label you assumed. listener in the expression resolves to the id of the NetworkListener object handling the connection, not a display name — it must exactly (case-sensitively) match the id you gave that listener object when you created it. A common cause of “milter never even runs” is that the listener is actually named something like smtp-in or the default smtp id was changed, so listener == 'smtp' never evaluates true. Temporarily setting enable to a plain "true" (which you tried) should bypass this — if scans still don’t increase even then, the milter object itself likely isn’t part of the running session config at all (see next point).
3. Confirm the MtaMilter object was actually saved as an active object and is present in session.milters. Milters are stored as a list of MtaMilter objects and read into the running SMTP session config; check in the WebUI under Settings › MTA › Filters › Milters that the milter you edited is listed there (not left in a draft/unsaved state), and that stages is encoded as an object, e.g. {"data": true}, not an array — a malformed stages field can cause the entry to fail to parse and be silently dropped from the milter list during config compilation.
4. Check port/hostname and useTls match your rspamd proxy exactly, and that the JSON was actually submitted (via WebUI or stalwart-cli import/apply) — editing the fields in a form that wasn’t saved, or applying a plan against the wrong instance, would also produce this symptom (Stalwart accepting mail with 250 as if no milter is configured, since if milters.is_empty() { return Ok(...) } short-circuits with no scan and no error).
Reference: Milter filters and MtaMilter object reference.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.