Sender reputation without a blacklist
Sharing what one site learns with every other site is the strongest signal in spam filtering, and the easiest to abuse. How spamex does it with decay, hashing and a two-site rule.
The address [email protected] sends the same backlink pitch to a bakery on Monday, a law firm on Tuesday and a plumber on Wednesday. Each site sees one message. Together they see a pattern. Sharing that pattern is the biggest advantage a hosted filter has over one you run alone.
It is also the easiest thing to get wrong. A naive shared blacklist has three failure modes: it never forgets, it can be poisoned by one bad actor, and it stores personal data it does not need. Here is how spamex handles each.
It forgets
Every sender entity (IP address, email address, email domain) has two counters: spam and ham. Each confident verdict adds one to the relevant counter. The counters decay exponentially with a 14-day half-life: a sender with ten spam hits today has five in two weeks and effectively none in two months. A compromised address that is cleaned up, or a shared IP that changes hands, is forgiven automatically. There is no list to get off.
It needs corroboration
A single customer’s counters affect only that customer. If your site marks an address as spam five times, the address is bad for you, immediately. Another site only sees that judgement once a second, independent site agrees. The hard “network” verdict, the one that rejects a message before any model runs, requires five confident spam hits, near-zero ham, and two or more sites.
This is the rule that stops poisoning. Without it, one malicious customer could mark a competitor’s email address as spam via the feedback endpoint and get it blocked everywhere. With it, they can only block it on their own site, which they could do anyway.
For the same reason, verdicts that were themselves produced by reputation never feed the counters. A message rejected because of a bad IP does not make the IP worse. Only independent evidence (the model’s raw probability, the language model’s confidence, a rule that fired without reputation input) counts. One mistake cannot snowball.
It stores hashes
IPs and email addresses are stored as SHA-256 hashes with a masked display form (203.0.113.x, m***@example.com) for the admin panel. Email domains are stored in clear because a domain is not a person, and free-mail domains such as gmail.com are excluded from domain-level scoring because they would just aggregate everyone.
The result is a reputation system that can say “this sender was bad, recently, on several sites” without ever holding a list of people.
What it looks like from the API
{"label": "spam", "score": 0.97, "stage": "rules",
"reasons": ["IP_REPUTATION_BAD", "EMAIL_REPUTATION_BAD"]}
And the pleasant opposite, for a customer who writes to you often:
{"label": "ham", "score": 0.04, "stage": "ml",
"reasons": ["EMAIL_REPUTATION_GOOD", "ML_HAM"]}
A good reputation scales the spam score down before thresholding, so a regular correspondent who one day sends a message with two links does not get flagged for it.
The honest limitation
Reputation is only as good as the traffic that feeds it, and the network is young. The two-site rule that protects against poisoning also means a brand-new spammer address is judged on content alone the first few times. That is the trade we chose: a slower start over a system that can be weaponised.