A spam filter that reads every language and explains every decision.

Spam Scanner catches spam, phishing, scams and malware for Node.js, the command line and mail servers such as Postfix, Exim and Haraka. Every result lists the tests that fired and why.

npm install -g spamscanner
  • Node.js 18+
  • Milter, spamd, HTTP
  • Ollama, Claude, ChatGPT
  • No telemetry
$ spamscanner scan message.eml
SPAM  score 16.3 (spam at 5.0, reject at 15.0)  action: reject  language: en
   +6.3  BAYES_999                    Classifier spam probability 99.9%
   +5.0  PHISHING_LOOKALIKE_DOMAIN    "paypa1-secure.top" imitates paypal by swapping characters
   +3.0  DECEPTIVE_LINK               A link shows "https://www.paypal.com/signin" but goes to paypa1-secure.top
   +2.0  FROM_NAME_BRAND              Display name says "paypal" but the message is from paypa1-secure.top
$ echo $?
1

How a message is judged

Every check adds or removes points, and the total decides. Thresholds and points can be changed, and nothing is a black box: the result names each test.

  1. Read

    The message is parsed and its text normalized: styled letters, invisible characters and lookalike alphabets are undone.

  2. Check

    The classifier, link, attachment, authentication, blocklist and rule checks run at the same time, each with a timeout.

  3. Score

    Each test adds or removes points. At 5 a message is spam; at 15 a mail server should refuse it.

  4. Ask

    Close calls can go to a language model, which adds up to 6 points or removes up to 3.

  • below 5accept
  • 5 to 15tag as spam
  • 15 and upreject

See it in 1:58

How a message is scored, how every language is read, when a language model is asked, and how it plugs into Postfix, Exim, Haraka and Node.js.

The video's title frame: the Spam Scanner logo.

What it checks

Words are one signal. Links, senders, attachments and authentication say as much, and none of them depend on the language of the message.

  • A classifier for every script

    Words, word pairs, links, senders, HTML and attachments, counted and combined the way SpamBayes does, with an honest "unsure" when the clues disagree.

    How it decides
  • Every language, carefully

    Unicode word segmentation for Chinese, Japanese and Thai, disguises undone, and no penalty for a language the model saw little ham in.

    Languages
  • Phishing

    Lookalike domains with Cyrillic or swapped letters, links whose text shows another address, brands in display names, and Cloudflare's malware resolver.

    Phishing checks
  • Attachments

    Executables found by their bytes, also renamed to .pdf; double extensions; programs in ZIP files; Office macros; active PDFs; ClamAV.

    Attachment checks
  • Authentication

    SPF, DKIM, DMARC and ARC, DNS blocklists, allow and deny lists, and a rule for mail that spoofs your own domain.

    Authentication
  • Language models

    Close calls go to a model: Ollama on your own machine, or Claude, ChatGPT, Gemini and any OpenAI-compatible server.

    Language models

Every test and its pointsHow it works

Every language, without guessing

Words are found with the Unicode rules, so languages written without spaces work, and disguises are undone before counting. These are the words the classifier counts, computed when this page was built.

A language the bundled model saw little ham in gets "unsure" instead of "spam", so ordinary mail in Arabic or Korean is never flagged for its script. Languages

Disguised English

Claim your fr␣ee prіze: V1agra 𝐅𝐑𝐄𝐄

  • claim
  • your
  • free
  • prize
  • viagra
  • free

A zero-width space, a Cyrillic і, a digit for a letter and mathematical bold letters

Chinese, no spaces

恭喜您获得一百万元大奖

  • 恭喜
  • 您
  • 获得
  • 一
  • 百万
  • 元
  • 大奖

Segmented with a dictionary

Thai, no spaces

ยินดีด้วยคุณได้รับรางวัลหนึ่งล้านบาท

  • ยินดี
  • ด้วย
  • คุณ
  • ได้
  • รับ
  • รางวัล
  • หนึ่ง
  • ล้าน
  • บาท

Segmented with a dictionary

Russian

Поздравляем! Вы выиграли миллион

  • поздравляем
  • вы
  • выиграли
  • миллион

Lowercased by Unicode rules

Plugs into your mail server

A milter for Postfix and Sendmail, a SpamAssassin-compatible spamd server for spamc, Exim and Haraka, a content filter, an HTTP API, a TCP server, and the library.

All of them add X-Spam-Flag, X-Spam-Score, X-Spam-Status headers, and remove any a sender forged.

Shell
spamscanner milter --port 7831 --auth --subject-tag "[SPAM]"

# /etc/postfix/main.cf
smtpd_milters = inet:127.0.0.1:7831
milter_default_action = accept

A milter checks mail during the SMTP session, so spam at the reject threshold is refused before it is accepted. Postfix guide

A language model for the close calls

Clear spam and clear ham never reach it. When the score is close or the classifier is unsure, a model reads the message and answers spam, phishing, scam, malware or ham. Run one on your own machine, or use a hosted one, on any URL, port and authentication.

Instructions hidden in a message for AI filters are scored as spam, and the model is told the message is data. The end-to-end tests check it against a real model through Ollama.

  • Ollama
  • LM Studio
  • llama.cpp server
  • vLLM
  • LocalAI
  • Jan
  • OpenAI
  • Anthropic (Claude)
  • Google Gemini
  • Mistral AI
  • Groq
  • OpenRouter
  • DeepSeek
  • xAI
  • Together AI
  • Fireworks AI
  • Cerebras
  • Hugging Face Inference Providers
  • Azure OpenAI
  • Cloudflare Clef
  • Cloudflare Clef Flash
  • TypeSafe Jev
  • TypeSafe Jev on OpenRouter
  • Any decision model server
Shell
ollama pull qwen3.5:4b
spamscanner llm-test --llm ollama --llm-model qwen3.5:4b
spamscanner milter --llm ollama --llm-model qwen3.5:4b

Outlined: runs on your own machine. Personal data is removed before a message goes to a hosted provider.

OllamaHugging FaceLicenseSize
qwen3.5:0.8bQwen/Qwen3.5-0.8BApache-2.01.3 GB
gemma4:e2bgoogle/gemma-4-E2B-itApache-2.04.6 GB
qwen3.5:4bQwen/Qwen3.5-4BApache-2.03.3 GB
qwen3.5:9bQwen/Qwen3.5-9BApache-2.06.6 GB
gpt-oss-safeguard:20bopenai/gpt-oss-safeguard-20bApache-2.014 GB

Recommended open models. All recommendations

Measured, and trainable

The bundled model is trained on public datasets and tested on messages it did not see. A model trained on your own mail does better: train it from mbox files, Maildirs or datasets, measure it, and teach it from "report spam" buttons.

Training

Held-out messagesMessagesPrecisionRecallFalse positives
English6,564100.0%97.1%0.0%
Russian1,682100.0%97.4%0.0%
Italian1,38998.1%85.3%1.8%
German1,30997.7%76.1%2.2%
Spanish1,28197.5%82.6%2.6%
All13,84099.2%85.1%0.6%

The classifier alone, on every tenth message of its public training data, held out. Unsure messages count as missed. German, Italian and Spanish come from synthetic datasets with noisy labels. About the bundled model

Built for Forward Email

Forward Email keeps no logs of message content, so it could not send mail to an outside filtering service. Spam Scanner runs on your own servers, sends nothing anywhere unless configured to, and explains every decision without a person reading the mail. Forward Email and upgrading

A project by Forward Email, the privacy-focused email service.