resources background

Blog

SpamAssassin NRD Scoring for Legacy Mail Servers

Written By Sameer Asad, WhoisFreaks Team Published: July 29, 2026, Last Updated: July 29, 2026

There is a category of threat that reputation-based spam filtering was never designed to catch. Not because the tools are bad. SpamAssassin has been one of the most reliable open-source spam filters for over two decades. The problem is that the attack arrives before any reputation exists.

A domain registered yesterday has no blocklist entries. No Bayes hits. No DNSBL records. No phishing reports. To a score-based filter working only with existing data, it looks identical to a domain registered five years ago that has simply never sent email. The filter has no signal to act on.

Newly registered domains (NRDs) are the preferred infrastructure for phishing campaigns, business email compromise (BEC) setups, and malware distribution precisely because of this window. Palo Alto Networks' Unit 42 found that more than 70% of newly registered domains are malicious, suspicious, or not safe for work, using a 32-day window as the period when an NRD is most likely to be flagged as malicious. The playbook is simple: register a convincing domain, send a campaign within the first few days, move on before the blocklists catch up. The cost is minimal, a few dollars per domain, and the operational security is built right into the registration timeline.

This guide adds a new scoring signal to SpamAssassin that closes that window: a custom Perl plugin that checks every sender domain against the WhoisFreaks NRD feed and raises the spam score when there is a match. It is not a hard block. It is a calibrated signal. The WF_NRD_SENDER rule adds 3.5 points to the message score, which stacks with Bayes, DNSBL, and SpamAssassin's other checks to produce a final verdict that accounts for domain age.


Why SpamAssassin Still Runs on Millions of Mail Servers

Before getting into the integration, it is worth addressing the question that comes up whenever SpamAssassin is mentioned alongside modern alternatives like Rspamd: why not just replace it?

The answer is almost always the same: existing infrastructure. SpamAssassin is embedded in a huge number of mail setups (Plesk, cPanel, ISPConfig, Postfix, Sendmail, Exim, Amavis) where it has been running reliably for years. The configurations, custom rule sets, and Bayes databases represent real accumulated value. Replacing SpamAssassin is a migration project. Adding a plugin to it is an afternoon.

The other honest answer is that SpamAssassin's rule-and-score architecture is well-suited to this kind of extension. Adding a signal like NRD domain age does not require touching the rest of the configuration. The plugin loads independently, fires on the eval rule, contributes its score, and gets out of the way. Everything else, thresholds, actions, Bayes, DNSBL, continues exactly as configured.


How the Integration Works

The architecture has four components working together:

Architecture: WhoisFreaks NRD API feeds a fetch script that rebuilds the domain list, loaded by SpamAssassin's WhoisFreaksNRD Perl plugin to score incoming messages

The fetch script (wf-nrd-fetch.sh, installed to /usr/local/bin/wf-nrd-sa-fetch.sh) runs daily at 05:00 UTC via cron. It pulls both gTLD and ccTLD files from the WhoisFreaks API. The ccTLD file becomes available at roughly 03:00 UTC for the previous day, so the 05:00 schedule gives a clean two-hour buffer. Each day's files are cached separately so the script only downloads what is new. The final domain list is rebuilt by merging all cached files, deduplicating, and writing atomically via mv so SpamAssassin always reads a complete file. After the write, it sends SIGHUP to spamd. No restart, no downtime, the process just reloads the list in place.

The Perl plugin (WhoisFreaksNRD.pm) is a standard SpamAssassin plugin loaded via a .pre file (wf-nrd.pre). At startup, and again on every SIGHUP, it reads the domain list into a Perl hash for O(1) lookups. On each incoming message, check_wf_nrd_sender() extracts the sender domain from the envelope (EnvelopeFrom) first, falling back to the From: header. If the domain is in the hash, the eval rule fires.

The rules file (wf-nrd.cf) defines WF_NRD_SENDER, sets the score, and points to the list file path. The whole thing is wrapped in an ifplugin guard so the rule only activates when the plugin is actually loaded.

The cron job (wf-nrd-spamassassin, installed to /etc/cron.d/) is what triggers the fetch script on the 05:00 UTC schedule. It carries the WINDOW_DAYS setting that controls how many days of NRD history the list keeps.

On the delivery side, Postfix passes incoming mail through SpamAssassin as a content filter, and the installer can also wire up spamass-milter so scoring happens at the SMTP layer. With the milter in place, a message scoring at or above the reject threshold (default 15, set via REJECT_SCORE) is refused during the SMTP conversation rather than accepted and filed. Messages below that but above required_score get the X-Spam-Flag: YES header, which a Dovecot Sieve rule can use to move them to Junk.


What the Score Does and Does Not Do

This is the piece that is most worth thinking through before deploying.

WF_NRD_SENDER at +3.5 does not block email from newly registered domains. SpamAssassin's default required_score is 5.0, meaning WF_NRD_SENDER alone does not cross the threshold. A legitimate startup that registered its domain last week and sends well-formed mail with valid SPF/DKIM and a clean message body will score 3.5, not enough to be flagged.

That is intentional. The NRD signal is strongest when it stacks with other evidence.

Scenario Likely combined score Outcome
NRD sender + valid SPF/DKIM + clean body 3.5 Delivered normally
NRD sender + missing DKIM + generic subject 5.5 to 6.5 Flagged as spam
NRD sender + Bayes hit + DNSBL match 9.0 to 12.0 Flagged confidently
NRD sender + phishing body patterns 12.0+ Flagged, possibly rejected

The score is a dial, not a switch. If your environment has very low false positive tolerance, leave it at 3.5 or even 2.0. If you are protecting a high-risk environment (financial services, healthcare, any organization that receives targeted BEC attempts), raise it to 5.0 and let NRD domain age alone be sufficient to flag a message.


Installing the Integration

The full source is at github.com/WhoisFreaks/wf-spamassassin-nrd-feed. Clone it and run the installer:

git clone https://github.com/WhoisFreaks/wf-spamassassin-nrd-feed.git
cd wf-spamassassin-nrd-feed
sudo ./install.sh

The installer detects your Linux distribution automatically and handles the differences between package managers, service names, and spamd user accounts across distros:

Distro support table: Ubuntu/Debian (apt/debian-spamd), RHEL/Rocky/Alma (dnf/spamd), Fedora (dnf/spamd), openSUSE (zypper/spamd), Arch Linux (pacman/spamd)

When the installer runs, it will ask for your WhoisFreaks API key, then handle the rest: creating directories, copying the plugin, the .pre loader, and the rules file, seeding the initial domain list, configuring Postfix, enabling services, and dropping the cron job. The final output confirms the distro it detected and all the paths it used.

For those who prefer to see each step rather than run the script blind, the README has a full manual install section with per-distro package commands.


Verifying the Integration

Once installed, three checks confirm everything is working:

1. Plugin loaded with domains:

spamassassin --lint -D 2>&1 | grep -i whoisfreaks

Look for a line reporting the domain count. If it shows zero, the list file did not populate. Check the fetch log at /var/log/wf-nrd-spamassassin.log.

2. Rule fires on a test message:

Pull any domain from the list file and pipe a test message through SpamAssassin directly:

TESTDOMAIN=$(head -1 /var/lib/spamassassin/nrd/nrd_domains.list)

cat <<EOF | spamassassin -D 2>&1 | grep -i 'WF_NRD\|nrd'
From: test@${TESTDOMAIN}
To: [email protected]
Subject: Test

Test body.
EOF

A successful test shows whoisfreaks-nrd: HIT in the debug output and WF_NRD_SENDER in the score breakdown.

3. Headers on delivered mail:

grep -A5 "X-Spam-Status" /var/mail/username

When WF_NRD_SENDER fires on real mail, it appears in the tests= field of X-Spam-Status:

X-Spam-Status: No, score=3.5 required=5.0 tests=WF_NRD_SENDER
Terminal output showing SpamAssassin's WhoisFreaksNRD plugin loading domains, detecting an NRD sender hit, and scoring WF_NRD_SENDER at 3.50 points

Practical Use Cases

Legacy mail infrastructure on cPanel / Plesk / ISPConfig

These control panels ship SpamAssassin as the built-in spam filter and rarely offer easy paths to replace it with Rspamd. The WhoisFreaksNRD plugin drops in without touching the panel's existing SpamAssassin configuration. Copy the four files (plugin, .pre loader, rules file, cron job) and install the cron job. The panel continues managing thresholds and delivery actions the same way it always has.

Organizations targeted by BEC campaigns

Business email compromise almost always uses freshly registered lookalike domains: company-billing.com, invoices-companyname.net, and similar. These domains are built for a single campaign and intentionally avoid any prior malicious activity that would trip reputation filters. Raising WF_NRD_SENDER to 5.0 in these environments means a freshly registered lookalike domain alone is sufficient to flag a message, regardless of how well-constructed the email body is.

Running alongside Rspamd

Some setups, particularly those using Amavis as a wrapper, run both Rspamd and SpamAssassin. If you have already deployed the WhoisFreaks Rspamd integration, do not also load the SpamAssassin plugin. The NRD score would be applied twice to the same message. Use one or the other depending on which filter is making the final delivery decision.

Shared hosting with multiple domains

On a shared mail server handling hundreds of domains, SpamAssassin's per-user configuration features let you vary the NRD score by recipient. High-risk accounts (finance team, executives) can have WF_NRD_SENDER at 5.0 in their user prefs while the rest of the server runs at 3.5.


Tuning Over Time

The default score of 3.5 is a starting point. After a week of running the integration in score-only mode, pull the X-Spam-Status headers from your mail logs and look at what is combining with WF_NRD_SENDER:

grep -h "tests=.*WF_NRD_SENDER" /var/log/mail.log | \
  grep -oP "tests=\K[^,\n]+(,[^,\n]+)*" | \
  tr ',' '\n' | sort | uniq -c | sort -rn | head -20

This gives a count of which other SpamAssassin rules are co-firing with WF_NRD_SENDER. If it consistently appears alongside BAYES_99 and RCVD_IN_DNSWL_NONE, you already have good compound coverage and 3.5 is appropriate. If it mostly appears alone on legitimate messages, drop it to 2.0.

One caveat: the feed covers all newly registered domains, not just confirmed malicious ones. Researchers, journalists, startups, and hobbyists register new domains regularly and may send legitimate email from them. The scoring model is specifically designed to handle this. A well-configured legitimate sender will contribute enough positive signals (valid SPF, DKIM pass, clean Bayes) to keep the total score below threshold even with WF_NRD_SENDER present.


The Rolling Window and Feed Freshness

The default window is 10 days. This means the domain list at any given moment contains all domains registered across the previous 10 days, both gTLD (.com, .net, .org) and ccTLD (.uk, .de, .io). The cron job at 05:00 UTC fetches the previous day's files, merges them into the list, and evicts anything older than the window.

Window sizing trades coverage against list size and false positive rate. The approximate domain counts below come from the WhoisFreaks NRD feed and reflect typical daily volumes as of mid-2026; actual counts vary day to day with registration activity.

Window Approx. domain count Best for
5 days ~300K Environments with legitimate new-domain contacts
10 days ~500K to 800K Default, good balance for most deployments
30 days ~1.5M to 2M High-security environments, maximum NRD coverage

The list is loaded into a Perl hash in memory. Even at 2 million domains, the load takes under two seconds and the memory footprint is around 150 MB for the spamd process, acceptable for a dedicated mail server. On a shared or resource-constrained system, stick to the 10-day window or lower.


A Note on SpamAssassin vs Rspamd for New Deployments

If you are setting up a mail server from scratch and have no existing SpamAssassin dependency, Rspamd is the stronger starting point: faster, lower memory footprint, built-in Lua scripting, a web UI, and native milter support that makes Postfix integration cleaner.

The WhoisFreaks NRD integration exists for both. For a brand-new Postfix server, start with the Rspamd integration. For an existing deployment already running SpamAssassin, whether on a VPS, a shared hosting panel, or a server you inherited, this plugin adds NRD awareness without touching anything else.


Conclusion

SpamAssassin has been detecting spam through rule-based scoring for long enough that its limitations are well-mapped. Reputation-based rules work until reputation doesn't exist. Bayes catches patterns that have appeared before. DNSBL blocks addresses that have already been reported.

None of those mechanisms see a domain that was registered 48 hours ago.

The WF_NRD_SENDER rule fills that gap. It does not replace SpamAssassin's existing signal stack. It adds to it, giving the filter one more dimension of evidence before making a delivery decision. For any server receiving targeted phishing, BEC attempts, or bulk spam from freshly registered infrastructure, that extra dimension makes a measurable difference.

The full source, installer, and documentation are at github.com/WhoisFreaks/wf-spamassassin-nrd-feed.


Part of the WhoisFreaks NRD Tools series. Related integrations: Pi-hole NRD Feed | AdGuard Home NRD Feed | Suricata NRD Feed