Command line

Text
spamscanner <command> [options]
CommandWhat it does
scan [file|-]Scan a message from a file or standard input
filter -f <sender> -- <recipients...>Postfix content filter: scan standard input, add headers, pass it on
milterMilter for Postfix and Sendmail, port 7831
httpHTTP API, port 7832
serverPlain TCP server, port 7830
spamdSpamAssassin-compatible spamd server, port 783
trainTrain a model from mbox files, Maildirs, folders or datasets
evalMeasure a model on labelled mail
learn spam|ham [file|-] --model <file>Teach a model one message
llm-testCheck language model settings with three sample messages
modelsList recommended open models
version, help

scan#

Shell
spamscanner scan message.eml
spamscanner scan - < message.eml
spamscanner scan message.eml --json
spamscanner scan message.eml --headers > tagged.eml
spamscanner scan message.eml --subject-tag "[SPAM]" > tagged.eml
OptionMeaning
--jsonPrint the full result as JSON
--headersPrint the message with X-Spam-* headers added
--subject-tag <tag>Also prefix the subject of spam
--verboseShow every test, and the classifier's strongest clues
--threshold <n>Score at which mail is spam (default 5)
--reject-threshold <n>Score at which mail is rejected (default 15)
--model <file>A model file instead of the bundled one
--no-classifierDo not use the classifier
--config <file>A JSON file with library options
--allow-language <codes>Accepted languages, for example en,de,fr

Exit codes: 0 hamWanted mail: the opposite of spam., 1 spam, 2 error.

SMTP session#

OptionMeaning
--ip <address>IP address of the client that sent the message
--hostname <name>The client's verified reverse DNS name
--helo <name>The name it gave in HELO or EHLO
--from <address>Envelope sender (MAIL FROM)
--to <address>Envelope recipient; repeat for several

Checks#

OptionMeaning
--authCheck SPF, DKIM, DMARC and ARC (needs --ip)
--dnsbl <zone>IP blocklist, for example zen.spamhaus.org; repeatable
--uribl <zone>Domain blocklist for links, for example dbl.spamhaus.org; repeatable
--dns-server <ip>Name server for DNS checks; repeatable
--no-cloudflareDo not ask Cloudflare's filtering resolvers about links
--clamav [socket]Scan attachments with clamd, at its default socket or the one given
--allowlist <value>Always accept this IP address, domain or address; repeatable
--denylist <value>Always reject this IP address, domain or address; repeatable

Language model#

OptionMeaning
--llm <provider>ollama, clef-flash, jev, openai, anthropic and others (list)
--llm-model <name>Model, for example qwen3.5:4b or claude-haiku-4-5
--llm-method <method>decision (a probability for each verdict, in one step; the default where available) or generate (methods)
--llm-account <id>Cloudflare account ID, for clef and clef-flash
--llm-url <url>Base URL, for example http://10.0.0.5:11434
--llm-host, --llm-port, --llm-path, --llm-protocolChange one part of the provider's URL
--llm-api-key <key>API key; see also the environment variables below
--llm-auth <type>bearer, x-api-key, api-key, basic, header or none
--llm-auth-header <name>Header for the key, with --llm-auth header
--llm-username, --llm-passwordFor --llm-auth basic
--llm-header "Name: value"Extra request header; repeatable
--llm-mode <mode>auto (close calls only, the default) or always
--llm-timeout <ms>Default 30000
--llm-policy <text>Extra rules for the model, for example "We never send invoices"
--llm-redact, --no-llm-redactRemove personal data first; on by default for remote providers

filter#

A Postfix content filter. It reads a message from standard input, adds X-Spam-* headers and passes it to sendmail with the same envelope.

Shell
spamscanner filter -f "$sender" -- "$recipient"
OptionMeaning
--sendmail <path>Default /usr/sbin/sendmail
--subject-tag <tag>Prefix the subject of spam
--rejectBounce mail at the reject threshold instead of passing it on
--discardDrop mail at the reject threshold instead of passing it on

Exit codes follow sendmail's conventions, which Postfix reads: 0 delivered (or discarded), 64 no recipients given, 69 rejected as spam (Postfix bounces it), 75 any failure, so Postfix keeps the message and tries again later.

milter, http, server and spamd#

Shell
spamscanner milter --port 7831 --reject --subject-tag "[SPAM]"
spamscanner http --port 7832 --token "$SPAMSCANNER_TOKEN"
spamscanner server --port 7830
spamscanner spamd --port 783

Port 783 is the port SpamAssassin clients use by default. Ports below 1024 need root or the CAP_NET_BIND_SERVICE capability; use another port, such as --port 7833, and tell the client.

OptionMeaning
--port <n>TCP port
--host <ip>Address to listen on (default 127.0.0.1)
--socket <path>Listen on a Unix socket instead
--rejectMilter: refuse mail at the reject threshold
--reject-code <n>Milter: 451, try again later (the default), or 550
--quarantineMilter: hold spam in the mail server's quarantine
--name <hostname>Milter: this server's name in Authentication-Results
--token <secret>HTTP: require Authorization: Bearer <secret>; needed for /learn
--allow-tellspamd: accept TELL requests (spamc -L spam) to learn
--out <file>HTTP and spamd: save what is learned to this model file
--subject-tag <tag>Milter and spamd: prefix the subject of spam
--verboseMilter: log every scan. TCP server: answer with one text line

The scan options above apply to the servers too. The milter, the HTTP API, the TCP server and spamd.

train, eval and learn#

Shell
spamscanner train --spam ~/Mail/Junk --ham ~/Mail/Archive --out my-model.json
spamscanner eval --model my-model.json --spam test/spam.mbox --ham test/ham.mbox
spamscanner learn spam message.eml --model my-model.json
OptionMeaning
--spam <path>Spam: an mbox file, a Maildir or a folder of .eml files; repeatable
--ham <path>Ham, likewise; repeatable
--dataset <file>A CSV or JSON Lines file with text and label columns; repeatable
--text-column <name>, --label-column <name>Column names, when they are not detected
--out <file>Where to write the model (default spamscanner-model.json)
--mergeStart from the bundled model (or --model) instead of an empty one

learn updates the model file in place, creating it from the bundled model the first time. Training

llm-test and models#

Shell
spamscanner models
spamscanner llm-test --llm ollama --llm-model qwen3.5:4b

llm-test sends one ordinary message and two scams, in English and Italian, to the model, prints its verdicts, the time each took, the method used and the hardware, and exits 0 only if all three are right.

Configuration file#

--config file.json (or the SPAMSCANNER_CONFIG environment variable) loads library options. Command-line options override the file.

JSON
{
  "threshold": 6,
  "authentication": true,
  "dnsbl": {"ip": ["zen.spamhaus.org"], "domain": ["dbl.spamhaus.org"]},
  "dns": {"servers": ["127.0.0.1"]},
  "clamav": {"socket": "/var/run/clamav/clamd.ctl"},
  "llm": {"provider": "ollama", "model": "qwen3.5:4b"},
  "scores": {"deceptiveLink": 4, "FROM_NAME_BRAND": 3}
}

Environment variables#

VariableMeaning
SPAMSCANNER_CONFIGConfiguration file
SPAMSCANNER_MODELModel file used instead of the bundled one
SPAMSCANNER_TOKENToken for the HTTP API
SPAMSCANNER_LLM_API_KEYAPI key for any language model provider
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, TYPESAFE_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY, XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, CEREBRAS_API_KEY, HF_TOKEN, AZURE_OPENAI_API_KEYEach provider's own key
NODE_DEBUG=spamscanner*Debug logging

Edit this page on GitHub · Markdown ·