Stalwart 0.16.22 accepts SMTP DATA without a Rspamd Milter scan

Your question

I run Stalwart 0.16.22 in Docker with Rspamd and ClamAV on the same Docker network. The inbound listener is named smtp on port 25. The saved Milter targets rspamd:11332, runs at DATA, has TLS off and TempFail on Error on, and uses listener == ‘smtp’.

The Stalwart container can connect to rspamd:11332. Rspamd’s proxy worker is listening there with milter = true. A direct ClamAV scan detects the EICAR test signature, and a direct Rspamd MIME scan returns CLAM_VIRUS and Action: reject. But submitting that MIME test to Stalwart on 127.0.0.1:25 returned 250 Message queued; Rspamd’s scan count did not increase beyond the direct scan. I also temporarily tested an always-true enable condition, but the SMTP scan count still did not increase.

What should I inspect to determine why Stalwart 0.16.22 isn’t invoking or honoring the Rspamd Milter for SMTP DATA?

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

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.

Thanks. Before changing the production configuration, could you please link the Stalwart 0.16.22 documentation or source for the claimed listenerId variable and stage-mask behavior? Also, is there a read-only way in Community Edition to inspect the active MtaMilter object and see whether it was invoked for an SMTP DATA transaction?

Our saved Milter is rspamd:11332, with DATA selected, TLS off, TempFail on Error on, and listener == 'smtp'. Rspamd and ClamAV work in direct tests, but an SMTP test to Stalwart received 250 Message queued without an increase in Rspamd’s scan count.

The variable is listener, and it holds the listener’s name, so listener == 'smtp' is correct for your setup. listenerId and a stage mask don’t exist; stages is a plain set. Both are covered at Variables | Stalwart and Milter filters | Stalwart.

The 250 Message queued tells me Stalwart never tried to reach Rspamd. With TempFail on Error enabled, a failed connection would have produced a 4xx, so a 250 and an unchanged scan count mean the milter isn’t in the running configuration. Saving an MtaMilter doesn’t apply it: run Management › Actions › Reload server settings (or restart) and read the result. If it reports Unable to resolve milter hostname rspamd, the name didn’t resolve when the configuration was built, which in Docker happens when Stalwart comes up before the rspamd container. A depends_on with a healthcheck, or an IP address in the milter, takes care of that.

stalwart-cli query MtaMilter shows the saved object without changing anything, but that’s what is stored, not what’s loaded. The proof that it runs is a milter.action-accept event, logged at info level for every message that passes through the milter.