Spam filter DNSBL-queries `o.box` from the text `P.O.Box`, junking legitimate mail

Issue Description

The spam filter performs a domain-blocklist lookup on a token taken from a postal address, not from any URL the message contains, and the resulting hit is on its own enough to classify a fully authenticated message as spam.

A footer containing a US postal address written without a space after P.O. yields the token O.Box. Because .box is a delegated gTLD, o.box is a syntactically valid hostname, so the tokenizer treats it as a scheme-less URL and crates/spam-filter/src/analysis/init.rs:132 promotes it to https://o.box. The domain is then submitted to the configured domain blocklists, and Spamhaus lists that TLD wholesale.

The stock STWT_DBL_SPAMHAUS_DOMAIN rule maps a final octet of 2 to DBL_SPAM, weighted 6.5 — above the default scoreSpam of 5.0 on its own. One postal address is therefore sufficient to junk a message regardless of everything else about it.

This generalises beyond .box: any prose containing word.tld where tld is a delegated gTLD is looked up as a hostname. .zip and .mov are the obvious cases — a message mentioning report.zip or clip.mov in running text has that queried against a domain blocklist as though it were a host.

Expected Behavior

A bare word pair in running prose should not be treated as a hostname for the purpose of domain blocklist lookups. A message whose only “listed domain” comes from its own postal address should not be classified as spam, and no single DNSBL hit derived from prose should exceed scoreSpam unaided.

Actual Behavior

The message is classified as spam on the strength of DBL_SPAM alone.

In the case observed, the message passed SPF, DKIM and DMARC and carried the best SenderScore reputation band. It scored 6.30 against the default 5.0 threshold. With DBL_SPAM excluded the same message scores -0.20.

Every domain the message actually references resolves NXDOMAIN on dbl.spamhaus.org. The only positive answer is for o.box, which appears nowhere in the message except as part of the postal address P.O.Box 25050.

Stalwart Version

v0.16.x

Installation Method

Built from source

Database Backend

RocksDB

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

OIDC

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

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

Confirmed against the current source: the plain-text tokenizer used in crates/spam-filter/src/analysis/init.rs runs with tokenize_urls_without_scheme(true), so any bare word.tld pair in the message body (subject, plain text, and HTML text) is emitted as TokenType::UrlNoScheme and treated identically to a real TokenType::Url throughout analysis/url.rs. That code path extracts the host’s SLD and calls check_dnsbl(..., Element::Domain, ...) on it with no check that the “URL” actually appeared as a link or came with a scheme — so P.O.Boxo.box gets queried against every Domain-scope DNSBL server exactly as you described, including the core STWT_DBL_SPAMHAUS_DOMAIN rule. This is a real gap in the tokenizer/analysis logic, not something you can disable via existing settings, so I can’t offer a docs-based fix for the root cause itself — a human will need to weigh in on whether the tokenizer should be more conservative about scheme-less hostnames drawn from running prose.

As an immediate mitigation until that’s addressed, you have two documented levers:

  1. Lower or zero the score for the tag this rule emits using a SpamTag override (found in WebUI under Spam Filter › Scores). For example:
{ "@type": "Score", "tag": "DBL_SPAM", "score": 1.0 }

This keeps the DNSBL check active for genuinely spammy domains while preventing a single scheme-less hit from junking an otherwise clean, fully-authenticated message on its own.

  1. Disable the specific core entry (STWT_DBL_SPAMHAUS_DOMAIN) if you’d rather not rely on domain-scope Spamhaus DBL at all, per the core DNSBL servers docs:
{ "@type": "Domain", "name": "STWT_DBL_SPAMHAUS_DOMAIN", "enable": false }

Neither of these fixes the underlying “prose is parsed as a hostname” behavior, only its scoring impact.

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

Fixed in v0.16.21 (yet to be released).