Clarification on Spam Filter, Blocked Domains & DKIM Verification Operations

Your question

I have a couple of questions regarding message verification and rejection and their defaults. I’ve checked the docs but I am left with a few questions.

  1. I received a piece of spam email purporting to be from my own address; the to and the from are the same. Since I have DMARC policy set to reject on my domain, shouldn’t Stalwart have terminated this message in the MTA DATA phase when the policy was verified? The header does confirm the policy was recognized as reject and both SPF and DKIM failed.

  2. If I wanted to enforce a rejection based on DKIM validation, e.g. reject any message that does not have a valid DKIM header, regardless of the DMARC policy (or lack thereof) would I change the MTA → Sender Authentication from ‘relaxed’ to ‘strict’, or do I require a sieve script?

  3. To flat out reject all email from a domain, is that what Spam Filter → Lists → Blocked Domains is for, and do wildcards or regex work here, or only exact matches?

  4. When the spam classifier hits the required default of 100 ham and spam, what begins to change?

  5. What do you need to alter in order to have high scoring messages rejected instead of landing in Junk, or is that tied to the minimum samples in question 4?

Thanks, and keep up the great work.

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

Now let’s check domain_lists.md for blocked domains and classifier training defaults for the 100 samples question.

1. Why wasn’t the spoofed message rejected despite DMARC p=reject?

By default, DMARC verification in Stalwart is set to relaxed, which only records the Authentication-Results/DMARC outcome in headers, it does not reject anything, even when the domain’s own policy is p=reject. Rejection only happens if dmarcVerify is set to strict on the SenderAuth singleton (MTA › Inbound › Sender Authentication). The default expression is:

{
  "dmarcVerify": {
    "match": [{"if": "listener == 'smtp'", "then": "relaxed"}],
    "else": "disable"
  }
}

So Stalwart deliberately doesn’t auto-enforce the sending domain’s DMARC policy at reject-level unless you change this to strict.

2. Enforcing rejection based on DKIM alone (regardless of DMARC)

Yes, this is a SenderAuth setting, not a sieve script. The dkimVerify field on the same SenderAuth singleton accepts relaxed (report only, the default), strict (reject on DKIM failure), or disable. Set it to strict to reject any message whose DKIM fails, independent of DMARC alignment.

3. Blocked Domains list: exact match, wildcard, or regex?

Spam Filter › Lists › Blocked Domains stores entries as MemoryLookupKey objects where the key field carries the domain literal. The docs describe these lookup lists purely in terms of domain-name entries (e.g. trusted-domains, freemail-providers, etc.) with no mention of wildcard or regex matching support, entries are exact domain names.

4. What changes once the classifier reaches 100/100 samples?

Below the minimum thresholds (minSpamSamples / minHamSamples, default 100 each), the classifier does not make classification decisions at all. Once both thresholds are met, the trained logistic-regression model becomes active and starts producing a spam probability for each message, which is then mapped to classifier tags contributing to the overall spam score. Before that point, there simply is no classifier-based scoring.

5. Rejecting high-scoring messages instead of Junk delivery

This is independent of the classifier’s sample minimums. It’s governed by the scoreReject (and optionally scoreDiscard) fields on SpamSettings. By default scoreReject is 0, meaning no message is rejected purely on score; raising it to a value at or above your desired threshold makes Stalwart reject at SMTP time once the cumulative score reaches it. You can also attach Discard/Reject action variants directly to a specific SpamTag if you want a single signal to force rejection regardless of overall score.

Refs: DMARC, DKIM verify, Domain lists, Classifier training, Spam filter scores and general settings.

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

  1. Because DMARC is verify-only by default. dmarcVerify ships as relaxed on port 25, which records the result in Authentication-Results and reports on it but never rejects. Set it to strict under MTA > Inbound > Sender Authentication, and a p=reject domain with both SPF and DKIM failing gets rejected at DATA, which is your case exactly.

  2. Set dkimVerify to strict on the same page. That rejects anything without a passing signature, independently of DMARC, and no Sieve is needed. Be clear about what it costs though: every unsigned sender and every forwarded message whose signature broke in transit gets rejected too, which in practice is a lot of legitimate mail. Don’t confuse it with dkimStrict, which is a different boolean about ignoring insecure signatures carrying an l= parameter.

  3. Yes, and it’s a hard reject rather than a score bump: the BLOCKED_DOMAIN tag maps straight to reject. But it’s an exact key lookup with no wildcards and no regex, and it tests the From header domain rather than the envelope sender, so subdomains need their own entries. For wildcard behaviour you’d write a custom SpamRule with an ends_with condition emitting the same BLOCKED_DOMAIN tag.

  4. Below either threshold the classifier abstains completely: it contributes no tags at all, and messages are judged on rules and DNSBLs alone. Once both are met it starts emitting its probability tags into the score. It won’t switch on the instant you hit 100 either, it waits for the next training cycle, which defaults to 12 hours.

  5. Unrelated to 4, as it happens. Three thresholds under Spam Filter > General: scoreSpam defaults to 5 and files to Junk, while scoreReject and scoreDiscard both default to 0, which means disabled. That’s why Junk is currently the worst outcome you can get. Set scoreReject above your spam score, somewhere around 10 to 15, to reject at SMTP time. scoreDiscard accepts and then silently drops with no bounce, which is harsher and worth being deliberate about. Individual tags can also force reject or discard regardless of score, which is the mechanism behind the blocked domains in question 3.

Thank you for the further clarification.

Edited

I glossed over this the first read. I will give this a try. Thanks again.