SpamTag not set by Stalwart

Your question

Im trying to set up spamfilters and am not able to do it.

I set my unbound instance as dns resolver because of spamhaus. i set some of the scores to reject (deleted and recreated because just changing the type would result in an error.)

I set rejectlimit to 6 for testing and used https://blt.spamhaus.com for testing. Nothing gets stopped. If i view the source code of the mail i notice the missing tags.

I read through the Stalwart and Spamhaus docs, tryied to debug with chat and claude. My configuration seams to be fine, because its basicly the one stalwart shiped with.

Im out of answers or even questions to ask and am in dire need of help to not go crazy.

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

Since the X-Spam-Status and X-Spam-Result headers are added unconditionally to every message that goes through the filter, a message with no such headers at all means the spam filter did not run on it, not that no tags fired. That points to a few likely culprits:

1. The spam filter may be skipped for this session. Filtering is gated per-SMTP-session by the enableSpamFilter expression on MtaStageData, evaluated at the DATA stage. Its default is:

{"else": "is_empty(authenticated_as)"}

That means the filter runs only for unauthenticated sessions. If you’re sending your Spamhaus BLT test message through an authenticated submission (e.g. logged in as a local user via port 587/465), the filter is deliberately skipped and no headers will ever appear, regardless of your score thresholds. Send the test from an external/unauthenticated relay to trigger it, or adjust the expression if you specifically want internal mail scanned too.

2. Check the master switch. Confirm enable is true on the SpamSettings singleton (Settings › Spam Filter › General). This is separate from enableSpamFilter.

3. Confirm the rule set actually downloaded. On first start Stalwart pulls rules from spamFilterRulesUrl (default the GitHub release of spam-filter-rules.json.gz). If that download failed (e.g. no outbound internet, or your custom DNS resolver interfering with the HTTPS fetch), there would be no rules to score against. You can force a refresh under Maintenance > Update SPAM rules in the WebUI and watch for errors.

4. Check the DNSBL entries themselves. If you deleted/recreated the Spamhaus SpamDnsblServer entries, make sure enable is true on them and that the zone/tag expressions weren’t altered incorrectly — core entries (STWT_*) are only safely modifiable via their enable field; any other edits get overwritten on the next rule update, or may silently misbehave if malformed.

5. Enable event logging for spam.dnsbl, spam.dnsbl-error, and related spam.* events to see whether DNSBL lookups are even being attempted for your test messages — this will confirm whether your unbound resolver is actually being queried.

Reference: Spam filter overview and DATA stage / enableSpamFilter.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Didn`t gave a lot of info on the subject.

Stalwart Spamhaus DNSBL tests never trigger

I am trying to troubleshoot an issue with the Spamhaus DNSBL integration in my Stalwart setup. So far, none of the Spamhaus test messages have ever triggered a DNSBL hit or been blocked, and I would like to get some input from the developers/community.

Setup

  • Proxmox host
  • Stalwart running in a Debian LXC
  • LXC was installed using the Proxmox Community Scripts
  • Inbound mail is delivered directly to Stalwart
  • Outbound mail is sent through an external mail relay
  • Stalwart’s spam-filter configuration is essentially untouched/default
  • Stalwart uses my own Unbound instance as its DNS resolver
  • Current v0.16.21

For testing I am using the official Spamhaus Blocklist Tester:

Relevant DNSBL configuration

The standard Stalwart DNSBL definitions are still enabled. In particular, the built-in Spamhaus IP DNSBL is present:

STWT_RBL_SPAMHAUS_IP

I also created a custom E-mail DNSBL rule to test Spamhaus DQS Zero Reputation Domain (ZRD):

DLNTN_DQS_SPAMHAUS_ZRD_FROM

with E-mail scope.

The intended zone was:

env_from.domain + '.mykey.zrd.dq.spamhaus.net'

The DNS query itself works correctly. For example:

dig A 'zrd-dqs.blt.spamhaus.net.mykey.zrd.dq.spamhaus.net' +noall +answer

returns:

zrd-dqs.blt.spamhaus.net.mykey.zrd.dq.spamhaus.net. 1 IN A 127.0.2.2

Fixed-zone test

To rule out the env_from.domain expression, I temporarily replaced the dynamic Zone expression with the exact, known-working hostname:

'zrd-dqs.blt.spamhaus.net.ttdiptb6suzzxcnfql3n2kata.zrd.dq.spamhaus.net'

This did not change the result.

The corresponding Spamhaus BLT test message was still accepted and no custom DNSBL tag appeared.

The message contains:

X-Spam-Result:  SPF_ALLOW (-0.20),
	ARC_NA (0.00),
	DKIM2_NA (0.00),
	DKIM_NA (0.00),
	FROM_EQ_ENV_FROM (0.00),
	FROM_HAS_DN (0.00),
	MID_RHS_MATCH_ENV_FROMTLD (0.00),
	MID_RHS_MATCH_FROMTLD (0.00),
	RCPT_COUNT_ONE (0.00),
	RCPT_DOMAIN_IN_BODY (0.00),
	RCPT_IN_BODY (0.00),
	SOURCE_ASN_54054 (0.00),
	TO_DN_NONE (0.00),
	TO_MATCH_ENVRCPT_ALL (0.00),
	RCVD_COUNT_ZERO (0.10),
	RCVD_NO_TLS_LAST (0.10),
	DMARC_NA (1.00),
	HELO_IPREV_MISMATCH (1.00)

There is no RBL_SPAMHAUS_* or RBL_SPAMHAUS_ZRD tag.

Additional observation

Stalwart’s journal does show multiple:

DNSBL query

events, so DNSBL processing is apparently taking place.

However, I have not been able to determine from the normal logs which DNSBL was queried or what DNS response Stalwart received.

What I am trying to understand

If I understand the default configuration correctly, I would expect at least one or two of the appropriate Spamhaus BLT tests to trigger with the standard, otherwise untouched Stalwart spam-filter configuration.

So far, every Spamhaus test mail has been accepted.

I have also tried troubleshooting this with ChatGPT and Claude, but neither was able to identify a configuration error.

I want to determining whether:

  1. my understanding of the default Stalwart Spamhaus/DNSBL behavior is correct;
  2. there is some additional configuration required for the built-in STWT_RBL_SPAMHAUS_IP rule to actually produce a tag/reject;
  3. using a custom Unbound resolver introduces a relevant issue;
  4. there is a way to see the actual DNSBL query and response generated by Stalwart;

Any help from someone familiar with the current Stalwart spam-filter/DNSBL implementation would be greatly appreciated.

Your expectation of the defaults is not correct. The shipped Spamhaus rules query the free public zones, zen.spamhaus.org for addresses and dbl.spamhaus.org for domains. Nothing in the bundle queries DQS, and no ZRD or HBL rule ships at all. Your own dig output shows the BLT payload answering under .zrd.dq.spamhaus.net, which is DQS-only data, so no built-in rule was ever going to fire on it.

Using DQS means recreating each zone you want as a custom rule with your key in the zone expression, along the lines of ip_reverse + '.<key>.zen.dq.spamhaus.net' for the IP scope and value + '.<key>.dbl.dq.spamhaus.net' for domains.

Your custom rule producing nothing even with the hostname hard-coded is a different problem: DNSBL servers and tag scores are compiled into the running configuration when the server starts. Creating the rule in the UI stores the object and changes nothing until you run Actions, Reload, Server settings. Unbound is not involved.

Watch out when you retest. Negative answers are cached in process for 24 hours against the zone string, and the Invalidate Caches action does not touch that cache, so a fix can look like it did nothing until you restart. Also switch env_from.domain to domain on that rule: the former is the envelope sender’s domain, which on a BLT test is not usually the thing you are testing.

For seeing the query and the answer directly, the spam.dnsbl event at debug level records the zone queried, the answer that came back, and which element triggered it, with spam.dnsbl-error for lookups that fail. You want the event’s fields, not just the “DNSBL query” line.

Thank you for clearing this misconception! Im going to sit down with this new insight next weekend and report back. =)