Deliverability & Infra

How Modern Spam Filtering Actually Scores Mail

The Mythic Intel Team · Mar 1, 2025 · 7 min read

Modern spam filters do not return a yes-or-no verdict. They compute a weighted score from dozens of independent signals, then compare that score against a ladder of thresholds to pick a single action: let it through, defer it, tag it, or reject it. Rspamd and SpamAssassin both work this way, and understanding the score is the difference between guessing why mail got filtered and reading the exact reason off the headers.

Each signal a filter checks is called a symbol, and every symbol carries a weight. A failed SPF check, a hit on a DNS blocklist, a Bayesian classifier that thinks the body looks like spam, an unusual ratio of links to text: each one fires a symbol that adds (or subtracts) points. The message score is the sum of all symbol weights. Negative weights matter as much as positive ones, because a strong DKIM-aligned signature or a known-good sender can pull a borderline message back below the reject line.

The score is a sum of symbols

In Rspamd, a processed message produces a list of symbols and a total score. A real example reads like a ledger:

R_SPF_ALLOW       -0.20
DKIM_VALID        -0.40
DMARC_POLICY_ALLOW -0.50
BAYES_SPAM         4.50
RBL_SPAMHAUS_XBL   3.00
MIME_BASE64_TEXT   0.10
----------------------------
score: 6.50

Each line is a symbol that matched, with its contribution. The classifier (BAYES_SPAM) is usually the heaviest single mover because it is trained on the corpus that actually flows through your server, so it learns what your spam looks like rather than a generic baseline. Authentication results (R_SPF_ALLOW, DKIM_VALID, DMARC_POLICY_ALLOW) almost always carry negative weights, since passing authentication is evidence of legitimacy, not of spam.

This additive model is why no single check decides the outcome. A message can fail SPF and still be delivered if everything else about it looks clean, and a message can pass SPF and still be rejected if the body, the links, and the reputation signals stack up against it.

Bayesian classification

The Bayesian classifier is the statistical core. It tokenizes the message and asks, for the words and features present, how often they have appeared in known-spam versus known-ham during training. The output is a probability that the message is spam, which Rspamd maps onto symbols like BAYES_SPAM (high probability) or BAYES_HAM (low probability), each with a corresponding weight. Because it is trained on your own mail, the classifier improves as you feed it confirmed spam and confirmed ham. An untrained or poorly trained Bayes is the most common reason a filter scores erratically.

Reputation and rate signals

Alongside content, the filter pulls in network-level evidence:

  • DNS blocklist (RBL/DNSBL) checks against the connecting IP and against URLs in the body. A hit on a list like Spamhaus contributes a positive weight.
  • SPF, DKIM, and DMARC evaluation results, each producing a symbol with a weight tied to pass, fail, or alignment.
  • Per-sender and per-IP rate and reputation tracking, so a source that suddenly spikes in volume or has a history of complaints scores higher.
  • Greylisting state, which defers unknown triplets of (sender IP, envelope from, envelope to) on first contact.

Thresholds map score to action

Once the score is final, Rspamd compares it against an ordered set of action thresholds and returns exactly one action to the MTA as a recommendation. The configured order must be ascending in severity. A typical local.d/actions.conf looks like this:

greylist = 4;
add_header = 6;
rewrite_subject = 8;
reject = 15;

The full set of actions Rspamd can return is:

  • no action: deliver normally (score below the first threshold)
  • greylist: emit a temporary defer so a legitimate sender retries; spam software often does not
  • soft reject: a temporary 4xx defer, used by greylisting and rate limiting
  • add header: deliver but stamp a spam header the user's rules can sort on
  • rewrite subject: deliver with a tagged subject
  • reject: refuse the message with a permanent 5xx
  • discard: accept then silently drop, returning success to the sender
  • quarantine: divert to a quarantine store (Rspamd 1.9 and later)

Only one action applies per message: the first threshold the score crosses, evaluated from least to most severe. Some modules bypass the score entirely and force an action through a pre-result. Greylisting and rate limiting do this by setting soft reject directly, which is why a message can be deferred even though its content score is low. SpamAssassin follows the same shape with a single configurable boundary (required_score, default 5.0); above it the message is tagged as spam, and the MTA or a milter decides whether to file or reject.

Reading the verdict

The practical payoff is that every filtering decision is auditable. Rspamd writes the symbols and score into the message headers (X-Spamd-Result) and exposes them in its web UI and history. When a sender complains that mail vanished, you do not guess. You pull the score line, see that BAYES_SPAM and an RBL hit pushed it past reject = 15, and you fix the actual cause, whether that is retraining Bayes, delisting the IP, or repairing authentication.

In a mail-infrastructure interview, the answer to "how does a spam filter decide" is exactly this: it sums weighted symbols into a score, then applies the first matching action threshold, with greylisting and rate limits able to force a defer outside the score. Being able to say that out loud, and then read a real X-Spamd-Result header to prove it, is what separates someone who has run a filter from someone who has only configured one.

your turn

Stop reading about interviews. Start training for yours.