Access spam headers from system sieve script in DATA stage

Your question

I would like to access the spam score added by the included spam filter or a milter to handle rejections on emails based on their spam score and other parameters (like e.g. the destionation).
But i can’t seem to access the X-Spam-Score header from the DATA stage sieve script. Is there a way to do what i want?

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

This is the key issue here. The DATA stage processes filters in this order: Milter filters first, then Sieve scripts, and finally MTA Hooks. The built-in spam filter runs as a separate step at the DATA stage, but the X-Spam-Status and X-Spam-Result headers it adds are added to the message after the filter pipeline completes - meaning they are not yet present when your DATA-stage Sieve script runs.

Here is what the docs say about when the headers become available:

For messages that are neither discarded nor rejected, Stalwart adds the X-Spam-Status and X-Spam-Result headers so that end users can build their own filters on top of the filter’s output, for example using Sieve via ManageSieve or JMAP Sieve.

The docs specifically direct end-user spam score filtering to ManageSieve or JMAP Sieve (i.e., delivery-time user Sieve scripts), not the DATA-stage system script. The X-Spam-Status/X-Spam-Result headers are accessible at delivery time from those scripts.

For your use case (reject at SMTP time based on spam score + destination), the recommended approach is to use the built-in spam filter’s own threshold settings directly:

  • scoreReject on SpamSettings - rejects messages at SMTP time when the score meets or exceeds this value.
  • scoreDiscard on SpamSettings - silently drops messages above a threshold.
  • Per-tag Reject or Discard actions via SpamTag objects for finer-grained control.

These are configured in the WebUI under Settings > Spam Filter > General. If you need destination-conditional logic (e.g., different thresholds per recipient domain), that is not directly exposed through the spam filter thresholds, which are currently global. A milter (via MtaMilter) that runs its own spam scoring and can reject based on envelope data before the built-in filter runs may be an alternative path for that specific requirement.

Relevant docs:

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Have you tried using the spamtest Sieve function?

Yes, I tried the following sieve script and can’t get it to work. It never rejects the email. Not even with 1 as comparision value.

require ["spamtest", "relational", "comparator-i;ascii-numeric", "reject"];

if spamtest :value "ge" :comparator "i;ascii-numeric" "1" {
    reject "Your message was classified as spam and refused.";
}

This should be like the example from the RFC, but instead of fileinto I use reject.

I have configured the script to be always called (else) in the DATA stage.

spamtest was implemented for untrusted Sieve scripts only. Version 0.16.7 will support this command also in trusted scripts.

Thank you, looks good from my first tests.

I really appreciate the quick fixes. Kudos

In my opinion ``spamtest`` is not the solution we are all looking for. Using spamtest the user defines again at which score a mail is to be considered spam, even if this has already been done once on the mail server. As somebody already noted earlier (think it was on GitHub) the spamtest testing is not how other mail servers work, they all provide Spam Status and Score headers value to user sieve scripts or don’t even let spam mails bubble to the user sieves. It would be great we could “revert” to this kind of spam handling: what is considered spam is handled as such. Action completed.

Kind regard,
Sebastian