SpamAssassin (SA) is the oldest open-source spam engine still in widespread production use. Cloud filters like Google's and Microsoft's have moved on to machine-learning ensembles, but Postfix, cPanel, Plesk, and most self-hosted MTAs still run SA as the first line of defence. When your customer's mail admin drops your sender in Spam, odds are the cause is a SpamAssassin rule firing on something you didn't know was a signal.
SA sums a pile of small positive and negative rule scores. Above 5.0 you go to Spam. The most common reasons honest senders score high: no plain-text part, missing PTR, broken MIME boundaries, URLs on URIBL-listed domains. All five are fixed in configuration, not copywriting.
How SpamAssassin actually works
Every message is parsed, tokenised, and run through a battery of rules. Each rule is a regex, DNS lookup, Bayesian classifier hit, or header check. A rule that fires contributes a numeric score — positive for spammy, negative for legitimate. The final verdict is a simple sum.
The default threshold is 5.0. Under 5.0 you pass; 5.0 to 7.0 is borderline Spam; above 7.0 you are firmly in Spam; above 10.0 many setups reject outright at SMTP time.
Each rule looks something like this in the SA configuration:
score MISSING_SUBJECT 1.799
score HTML_IMAGE_RATIO_02 1.756
score URIBL_BLACK 3.000
score RDNS_NONE 1.274
score ALL_TRUSTED -1.800Key thresholds you need to know
- Below 1.0 — you're clean and look positively legitimate. Target this.
- 1.0–4.9 — passing, but filter tuning or a bad link can push you over.
- 5.0 — default Spam cut-off. Borderline and unpredictable.
- 7.0+ — strong spam signal. Most configurations move you to Junk with certainty.
- 10.0+ — many setups outright reject at the SMTP level with a 5xx response.
Running SA on your own messages
The fastest feedback loop is running SpamAssassin locally against a.eml file you exported from your test send.
# Install on Debian/Ubuntu
apt install spamassassin spamc
# Score a saved message
spamassassin -t < message.eml | grep -E "X-Spam-(Status|Score|Report)"
# Or use the Docker image
docker run --rm -i instantlinux/spamassassin < message.emlThe output gives you a line-by-line rule breakdown. That breakdown is the only thing you need — it tells you exactly which rules fired and what each contributed.
X-Spam-Score: 8.1
X-Spam-Report:
* 1.8 MISSING_SUBJECT Missing Subject: header
* 1.8 HTML_IMAGE_RATIO_04 HTML has a low ratio of text to image area
* 1.3 RDNS_NONE Delivered to internal network by a host with no rDNS
* 1.7 URIBL_BLOCKED ADMINISTRATOR NOTICE: URIBL was blocked
* 1.5 HTML_MIME_NO_HTML_TAG HTML-only without <html> tagThe rules that trip honest senders
HTML-to-text ratio and image-heavy bodies
HTML_IMAGE_RATIO_02 through HTML_IMAGE_RATIO_08 measure how much of your body is images versus actual text. A newsletter that's one hero image plus a footer scores 1.5 to 2.0 here. Combine with HTML_MIME_NO_HTML_TAG (1.5) and MIME_HTML_ONLY (1.4) and you're halfway to Spam before you've written a word.
Fix: send multipart/alternative with both a plaintext and an HTML part, and keep the HTML body to at least 60% real text. Every reputable ESP does this by default; custom code and some transactional APIs do not.
Content-Type: multipart/alternative; boundary="BOUND"
--BOUND
Content-Type: text/plain; charset=UTF-8
Hi Anna, thanks for signing up. Confirm your email:
https://example.com/confirm?token=abc
--BOUND
Content-Type: text/html; charset=UTF-8
<html><body><p>Hi Anna, thanks for signing up...</p></body></html>
--BOUND--RDNS_NONE and PTR records
RDNS_NONE (+1.3) fires when the sending IP has no PTR record, or the PTR doesn't resolve back via forward-confirmed reverse DNS. It's a cheap, reliable spammer signal — real mail servers set PTR, botnets don't.
Fix: set the PTR at your hosting provider's control panel (Hetzner, OVH, AWS Route 53, DigitalOcean networking). Confirm with dig -x 203.0.113.10 and make sure the returned hostname resolves back to the same IP. We cover the full setup in our PTR record guide.
MIME structure fixes
Malformed MIME is one of the most reliably-weighted signals in SA. Rules like MIME_BAD_BOUNDARY, MISSING_MIME_HB_SEP and MIME_HEADER_CTYPE_ONLY each add 1–2 points.
- Use unique, long boundary strings (your library does this by default; hand-rolled SMTP code often doesn't).
- Always include
charset=UTF-8in every part's Content-Type header. - Separate headers from body with a true blank line (CRLF CRLF), not a single LF or extra whitespace.
- Close every boundary with the trailing
--.
Subject line and header rules
SA penalises subjects that look like marketing shouting:
SUBJ_ALL_CAPS— +1.5 if most of the subject is uppercase.SUBJECT_EXCESS_EXCLAIM— +0.7 per extra "!".MISSING_SUBJECT— +1.8. Transactional mail sent without a Subject is flagged hard.FROM_LOCAL_NOVOWEL— +0.5 for from-addresses that look random.
Fix: sentence case, one exclamation maximum, every message has a subject, and sender local-parts should be pronounceable.
URIs and link reputation
URIBL_BLACK (+3.0), URIBL_DBL_SPAM (+2.5) and URIBL_BLOCKED (+1.7) test every URL in your body against SURBL, Spamhaus DBL, and URIBL. One bad link destroys an otherwise clean message.
Fix: before you send, run every URL's host through host example.com.dbl.spamhaus.org. If the response is anything other than NXDOMAIN, the domain is listed. Also avoid URL shorteners (bit.ly is disproportionately listed) and tracking domains shared across thousands of unrelated senders.
The 8.1 to 0.9 walk-through
Here's a real example from a test send that came in at 8.1. Six rule fixes took it to 0.9.
MISSING_SUBJECT(+1.8) → added Subject header. New score: 6.3.HTML_MIME_NO_HTML_TAG(+1.5) → wrapped body in proper <html><body> tags. New score: 4.8.MIME_HTML_ONLY(+1.4) → added plaintext alternative part. New score: 3.4.RDNS_NONE(+1.3) → set PTR at hosting provider, waited 15 minutes for propagation. New score: 2.1.URIBL_BLOCKED(+1.7) → replaced a bit.ly shortener with the final destination URL. New score: 0.4 (content gained aALL_TRUSTEDbonus).- Final polish — removed two extra exclamation marks in the subject. Final score: 0.9.
The body copy did not change. The template is identical. Only the MIME structure, DNS, and one link URL moved.
Gmail and Outlook do not run SpamAssassin. But a message that scores under 1.0 in SA is almost guaranteed to pass the simple technical checks Gmail and Outlook apply before they get to their ML classifiers. Treat SA as a cheap, offline smoke test — fast feedback, no seed mailbox needed.