CLI
Copy MarkdownCLI
The engine and CLI are open source (MIT), in Go: sendscore/sendscore. The CLI runs every check on a saved message. It needs no account, so it works on your own machine and in CI.
Install
go install github.com/sendscore/sendscore@latestLanguage packages
Packages for TypeScript, Python, PHP, Rust, Ruby, .NET, Java, and Elixir are in the repository, but none is published yet. Each one wraps this CLI and adds a typed API, so every language runs the one engine and returns the same verdict. From Go, import the engine directly.
Save an issue as .eml
The CLI reads a message as your mail client received it, headers and all.
- Gmail: open the issue, choose Show original from the message menu, then Download original.
- Apple Mail: select the issue, then File, Save As, with the format set to Raw Message Source.
- Fastmail: open the issue, then Download message from the message menu.
Save it from a real inbox rather than exporting it from your newsletter platform, so the headers your provider added are in it.
Check one issue
sendscore check issue.eml # a report to read
sendscore check --json issue.eml # the verdict JSON
cat issue.eml | sendscore check - # read the message from standard inputThe JSON is the same verdict the app returns. A message can be at most 25 MB.
| Flag | What it does |
|---|---|
--json |
Print the verdict as JSON. |
--offline |
Make no DNS lookups. Checks that need DNS are skipped, not failed. DKIM and DMARC can’t be evaluated then, so ready_to_send is false and the verdict is at least warn. |
--links |
Fetch every link and image. Off by default, because it is slow and it touches your tracking links. A link whose text or address suggests unsubscribe, subscribe, opt out, preferences, confirm, verify, or activate is never fetched, nor is a click-tracked link with text that says nothing, a redirect into one is not followed, and private and other non-public addresses are refused. |
--spamhaus-key KEY |
Look link hosts and domains up on Spamhaus DBL through Data Query Service, which allows commercial use. Also read from SENDSCORE_SPAMHAUS_DQS_KEY, and --help never shows its value. |
--surbl-zone ZONE |
Look link domains up on a SURBL data feed zone. Also read from SENDSCORE_SURBL_ZONE, and --help never shows its value. |
--public-blocklists |
Look link hosts and domains up on the free public Spamhaus DBL and SURBL zones, which are for non-commercial use only. |
--ip <address> |
The IP address of the server that delivered the message. |
--mail-from <address> |
The envelope sender (MAIL FROM). Defaults to the Return-Path. |
--helo <name> |
The HELO name the delivering server gave. |
--tls yes|no |
Whether delivery used TLS. |
--previous <file.eml> |
The newsletter’s previous issue, to list what changed. |
Give it the envelope
A saved file does not record which server delivered it, so auth.spf, auth.ptr, and auth.tls are skipped unless you pass what the receiving server saw. The Received headers in the file usually show the IP:
sendscore check --ip 203.0.113.10 --mail-from bounce@mail.example.com --tls yes issue.emlA message sent to a test address on your own app carries all of it.
Without the envelope, SPF can’t be checked. When the Return-Path domain aligns with the From domain, SPF could have passed DMARC when the message was delivered, so auth.dmarc is skipped too, ready_to_send is false, and the verdict is at least warn. Pass --ip and --mail-from to evaluate it.
Compare with the previous issue
Give check the previous issue too, and the verdict’s changes lists what moved between them. The previous issue is read with no DNS lookups, so only what the messages themselves carry is compared, which is the signing domains and selectors, the Return-Path, the unsubscribe headers, the link hosts, the number of links and images, and the size. Authentication results are not compared, because checking last week’s message against today’s DNS would show today’s answer, not the one it got when it was sent. To see whether a DNS change broke authentication, check a new message.
sendscore check --json --previous last-week.eml issue.emlOr print only the changes:
sendscore diff last-week.eml issue.eml
sendscore diff --json last-week.eml issue.emldiff takes --json, and makes no DNS lookups for either message. It exits 1 when a change made a check worse, and 0 otherwise.
Exit codes
| Code | Meaning |
|---|---|
0 |
Nothing failed or warned. Skipped checks don’t count, so a run can exit 0 with checks skipped. |
1 |
Warnings only, or authentication could not be checked (for example with --offline). For diff, a change made a check worse. |
2 |
A check failed. |
64 |
Usage error, such as an unknown flag or a bad --ip. |
65 |
The input is not an email message, or is larger than 25 MB. |
66 |
The input file cannot be read. |
70 |
Internal error. |
74 |
The report cannot be written. |
The codes from 64 up are errors with no verdict, so a script can tell them from a verdict of warn or fail.
Checks that need the network
The DNS checks look records up as they run, and the link checks fetch URLs when --links is on. Without a network, or when a lookup times out, those checks are skipped with the reason, never passed. A temporary DNS failure, such as a timeout looking up a DKIM key or a PTR name’s address, makes a check skipped, never failed. When DKIM or DMARC can’t be evaluated, the verdict is at least warn, ready_to_send is false, and the summary says "Authentication could not be checked because DNS lookups failed, so this is not ready to send." Link checks share 30 seconds, and a link still unfinished then is listed under not_checked, never as broken. The verdict for a saved file can differ from the one the app gave when it received the message if the DNS has changed since.