The MIME structure parses cleanlystruct.mime · Structure
{
"has_html": true,
"has_text": true
}
Run SendScore on a saved email, send a test to your own server, or let it watch every issue. It returns a verdict that lists every problem it found and who needs to fix each one.
go install github.com/sendscore/sendscore@latestOpen source, with no account and no server. For you, or for your agent.
The gap
Agents already write newsletters and schedule them through the platform’s API. Then they stop, because nothing tells them what reached a mailbox:
{{first_name}} go out unrendered?SendScore is the step between writing and sending. Run the CLI on an issue saved from a real inbox, or run the app and it receives the preview directly, as a subscriber’s mail server would, with no forwarding service to rewrite headers.
Then it runs fixed checks. The same message always gets the same verdict.
Verdict
The report below is drawn from the verdict JSON underneath it. The summary says where the issue stands, and when something is wrong, the top fixes say what to do first.
Each check shows the headers or records it read. A check that could not run says why, instead of passing quietly.
PASS Ready to send.
Every check that ran passed.
{
"has_html": true,
"has_text": true
}
{
"text_bytes": 1373
}
{
"clip_bytes": 104448,
"html_bytes": 4729
}
{
"date": "Mon, 5 Oct 2026 20:45:36 +0000",
"from": "hello@dailyphototips.com",
"message_id": "\u003cREDACTED@convertkit-mail4.com\u003e"
}
{
"from_domain": "dailyphototips.com"
}
{
"signatures": [
{
"a": "rsa-sha256",
"aligned": true,
"d": "dailyphototips.com",
"key_bits": 2048,
"key_type": "rsa",
"result": "pass",
"s": "cka"
},
{
"a": "rsa-sha256",
"aligned": false,
"d": "mail.kit.com",
"key_bits": 2048,
"key_type": "rsa",
"result": "pass",
"s": "esp1"
}
]
}
{
"domain": "dailyphototips.com",
"weakest_rsa_bits": 2048
}
No envelope: SPF needs the IP address that delivered the message. Pass --ip and --mail-from, or send the message to a SendScore address.
{
"domain": "ckespa.dailyphototips.com",
"limit": 10,
"lookups": 3,
"void_lookups": 0
}
{
"domain": "dailyphototips.com",
"policy": "quarantine",
"policy_tag": "p",
"record": "v=DMARC1; p=quarantine; pct=100;"
}
{
"dkim_aligned": true,
"from_domain": "dailyphototips.com",
"policy": "quarantine",
"spf_aligned": false
}
{
"dkim_aligned": true,
"dkim_mode": "r",
"from_domain": "dailyphototips.com",
"spf_aligned": false,
"spf_domain": "ckespa.dailyphototips.com",
"spf_mode": "r"
}
No envelope: the delivering IP address is unknown.
No envelope: how the message was delivered is unknown.
{
"uris": [
"mailto:unsub+REDACTED@convertkit-mail.com",
"https://example.unsubscribe.convertkit-mail4.com/REDACTED"
]
}
{
"list_unsubscribe_post": "List-Unsubscribe=One-Click"
}
{
"signed_by": "dailyphototips.com"
}
{
"link": "https://example.unsubscribe.convertkit-mail4.com/REDACTED"
}
{
"found": []
}
{
"images": 2,
"links": 4
}
{
"images": 2
}
{
"characters": 47,
"subject": "[RE] Are you joining the photo editing webinar?"
}
{
"preview": "Join us as we go over some killer post-processing secrets in Lightroom and Photoshop. Just a quick h"
}
{
"images": 2,
"text_characters": 740
}
{
"links": 4
}
{
"links": 4
}
{
"found": []
}
Blocklist lookups are off. Set a Spamhaus DQS key, or turn on the free public lists for non-commercial use.
Link checking is off.
Link checking is off.
Receipt at SendScore does not prove inbox placement at Gmail or any other provider.
{
"schema": "sendscore.verdict/v1",
"message": {
"received_at": "2026-10-05T20:45:36Z",
"from": "DailyPhotoTips <hello@dailyphototips.com>",
"subject": "[RE] Are you joining the photo editing webinar?",
"message_id": "<REDACTED@convertkit-mail4.com>",
"size_bytes": 17785
},
"verdict": "pass",
"ready_to_send": true,
"summary": "Every check that ran passed.",
"top_fixes": [],
"checks": [
{
"id": "struct.mime",
"category": "struct",
"status": "pass",
"title": "The MIME structure parses cleanly",
"evidence": {
"has_html": true,
"has_text": true
}
},
{
"id": "struct.plain_text",
"category": "struct",
"status": "pass",
"title": "Has a plain text version",
"evidence": {
"text_bytes": 1373
}
},
{
"id": "struct.html_size",
"category": "struct",
"status": "pass",
"title": "HTML is 5 KB",
"evidence": {
"clip_bytes": 104448,
"html_bytes": 4729
}
},
{
"id": "struct.headers",
"category": "struct",
"status": "pass",
"title": "Message-ID, Date, and From are present",
"evidence": {
"date": "Mon, 5 Oct 2026 20:45:36 +0000",
"from": "hello@dailyphototips.com",
"message_id": "<REDACTED@convertkit-mail4.com>"
}
},
{
"id": "struct.from_domain",
"category": "struct",
"status": "pass",
"title": "Sent from your own domain (dailyphototips.com)",
"evidence": {
"from_domain": "dailyphototips.com"
}
},
{
"id": "auth.dkim",
"category": "auth",
"status": "pass",
"title": "DKIM signature verified",
"evidence": {
"signatures": [
{
"a": "rsa-sha256",
"aligned": true,
"d": "dailyphototips.com",
"key_bits": 2048,
"key_type": "rsa",
"result": "pass",
"s": "cka"
},
{
"a": "rsa-sha256",
"aligned": false,
"d": "mail.kit.com",
"key_bits": 2048,
"key_type": "rsa",
"result": "pass",
"s": "esp1"
}
]
},
"docs": "https://support.google.com/mail/answer/81126"
},
{
"id": "auth.dkim_key",
"category": "auth",
"status": "pass",
"title": "DKIM key is 2048 bits",
"evidence": {
"domain": "dailyphototips.com",
"weakest_rsa_bits": 2048
}
},
{
"id": "auth.spf",
"category": "auth",
"status": "skipped",
"title": "SPF not checked",
"reason": "No envelope: SPF needs the IP address that delivered the message. Pass --ip and --mail-from, or send the message to a SendScore address."
},
{
"id": "auth.spf_lookups",
"category": "auth",
"status": "pass",
"title": "SPF needs 3 of 10 DNS lookups",
"evidence": {
"domain": "ckespa.dailyphototips.com",
"limit": 10,
"lookups": 3,
"void_lookups": 0
},
"docs": "https://www.rfc-editor.org/rfc/rfc7208#section-4.6.4"
},
{
"id": "auth.dmarc_record",
"category": "auth",
"status": "pass",
"title": "DMARC policy is quarantine",
"evidence": {
"domain": "dailyphototips.com",
"policy": "quarantine",
"policy_tag": "p",
"record": "v=DMARC1; p=quarantine; pct=100;"
},
"docs": "https://support.google.com/mail/answer/81126"
},
{
"id": "auth.dmarc",
"category": "auth",
"status": "pass",
"title": "DMARC passes",
"evidence": {
"dkim_aligned": true,
"from_domain": "dailyphototips.com",
"policy": "quarantine",
"spf_aligned": false
},
"docs": "https://www.rfc-editor.org/rfc/rfc7489"
},
{
"id": "auth.alignment",
"category": "auth",
"status": "pass",
"title": "DKIM aligns with the From domain",
"evidence": {
"dkim_aligned": true,
"dkim_mode": "r",
"from_domain": "dailyphototips.com",
"spf_aligned": false,
"spf_domain": "ckespa.dailyphototips.com",
"spf_mode": "r"
}
},
{
"id": "auth.ptr",
"category": "auth",
"status": "skipped",
"title": "Reverse DNS not checked",
"reason": "No envelope: the delivering IP address is unknown."
},
{
"id": "auth.tls",
"category": "auth",
"status": "skipped",
"title": "TLS not checked",
"reason": "No envelope: how the message was delivered is unknown."
},
{
"id": "unsub.header",
"category": "unsub",
"status": "pass",
"title": "List-Unsubscribe has an https link",
"evidence": {
"uris": [
"mailto:unsub+REDACTED@convertkit-mail.com",
"https://example.unsubscribe.convertkit-mail4.com/REDACTED"
]
},
"docs": "https://support.google.com/mail/answer/81126"
},
{
"id": "unsub.one_click",
"category": "unsub",
"status": "pass",
"title": "One-click unsubscribe is set up",
"evidence": {
"list_unsubscribe_post": "List-Unsubscribe=One-Click"
},
"docs": "https://www.rfc-editor.org/rfc/rfc8058"
},
{
"id": "unsub.signed",
"category": "unsub",
"status": "pass",
"title": "Unsubscribe headers are DKIM signed",
"evidence": {
"signed_by": "dailyphototips.com"
},
"docs": "https://www.rfc-editor.org/rfc/rfc8058"
},
{
"id": "unsub.visible",
"category": "unsub",
"status": "pass",
"title": "Visible unsubscribe link",
"evidence": {
"link": "https://example.unsubscribe.convertkit-mail4.com/REDACTED"
}
},
{
"id": "content.placeholders",
"category": "content",
"status": "pass",
"title": "No unrendered merge tags",
"evidence": {
"found": []
}
},
{
"id": "content.insecure_links",
"category": "content",
"status": "pass",
"title": "Every link and image uses https",
"evidence": {
"images": 2,
"links": 4
}
},
{
"id": "content.image_alt",
"category": "content",
"status": "pass",
"title": "Every image has alt text",
"evidence": {
"images": 2
}
},
{
"id": "content.subject",
"category": "content",
"status": "pass",
"title": "The subject line reads cleanly",
"evidence": {
"characters": 47,
"subject": "[RE] Are you joining the photo editing webinar?"
}
},
{
"id": "content.preheader",
"category": "content",
"status": "pass",
"title": "The inbox preview starts with real text",
"evidence": {
"preview": "Join us as we go over some killer post-processing secrets in Lightroom and Photoshop. Just a quick h"
}
},
{
"id": "content.text_ratio",
"category": "content",
"status": "pass",
"title": "The message carries real text",
"evidence": {
"images": 2,
"text_characters": 740
}
},
{
"id": "content.shorteners",
"category": "content",
"status": "pass",
"title": "No link shorteners",
"evidence": {
"links": 4
}
},
{
"id": "content.link_text",
"category": "content",
"status": "pass",
"title": "Links go where their text says",
"evidence": {
"links": 4
}
},
{
"id": "content.active",
"category": "content",
"status": "pass",
"title": "No scripts, forms, embeds, or attachments",
"evidence": {
"found": []
}
},
{
"id": "content.blocklists",
"category": "content",
"status": "skipped",
"title": "Link domains not looked up",
"reason": "Blocklist lookups are off. Set a Spamhaus DQS key, or turn on the free public lists for non-commercial use."
},
{
"id": "content.links",
"category": "content",
"status": "skipped",
"title": "Links not checked",
"reason": "Link checking is off."
},
{
"id": "content.images",
"category": "content",
"status": "skipped",
"title": "Images not checked",
"reason": "Link checking is off."
}
],
"changes": [],
"fingerprint": {
"dkim_domains": [
"dailyphototips.com",
"mail.kit.com"
],
"dkim_selectors": [
"cka._domainkey.dailyphototips.com",
"esp1._domainkey.mail.kit.com"
],
"return_path_domain": "ckespa.dailyphototips.com",
"list_unsubscribe": true,
"one_click": true,
"link_hosts": [
"example.click.convertkit-mail4.com",
"example.unsubscribe.convertkit-mail4.com",
"preferences.convertkit-mail4.com"
],
"html_bytes": 4729,
"links": 4,
"images": 2,
"auth": {
"dkim": "pass",
"dmarc": "pass"
}
},
"explanation": null,
"note": "Receipt at SendScore does not prove inbox placement at Gmail or any other provider."
}
A real issue of Daily Photo Tips, checked by the real engine (sendscore version e9a003b), with the subscriber’s personal tokens removed. It was checked as a saved file with no envelope, so SPF, reverse DNS, and TLS are skipped. Link checks and blocklists are off by default, so those are skipped too.
For agents
The tools come from the app, which you run yourself. The agent gets an address from it, sends the preview from Kit, Beehiiv, or whatever platform it already uses, and reads the verdict. Then it fixes what failed, or schedules the send.
get_test_addressReturns a temporary address, its token, and when it expires. The address accepts mail for 24 hours.wait_for_messageWaits up to 50 seconds for the preview. Returns the verdict, ready_to_send, the summary, the top fixes, and the report’s id, or not_received so the agent can call again.get_reportReturns the full verdict JSON: every check, with its evidence and fix.compare_to_previousTakes two reports from the last 7 days, such as a preview before and after a fix. Returns what changed, and marks any change that made a check worse.claude mcp add --transport http sendscore https://sendscore.example.com/api/mcpUse your own app’s address. Then run /mcp to sign in by email and press Allow. In claude.ai or another client, add the same address as a custom connector. Or run the local server with sendscore mcp, SENDSCORE_URL, and an API token. All of them serve the same tools, as MCP setup explains. Without MCP, the same steps are three API requests with an API token.
For operators
Monitoring is part of the app. Subscribe a SendScore address on your own server to each newsletter, like any reader.
Each issue is set against the last one:
A change in your provider’s signing setup is easy to miss, because the issue still arrives. The comparison catches it on the first issue it affects.
Checks
Thirty-one checks in five groups. A failure in the first two sets ready_to_send to false.
Who sent the issue, and how it was delivered.
auth.spfauth.spf_lookupsauth.dkimauth.dkim_keyauth.dmarc_recordauth.dmarcauth.alignmentauth.ptrauth.tlsThe unsubscribe Gmail and Yahoo require of bulk senders.
unsub.headerunsub.one_clickunsub.signedunsub.visibleBuilt the way mail clients expect.
struct.mimestruct.plain_textstruct.html_sizestruct.headersstruct.from_domainWhat a reader would see go wrong.
content.placeholderscontent.linkscontent.insecure_linkscontent.imagescontent.image_altcontent.blocklistscontent.link_textcontent.shortenerscontent.subjectcontent.preheadercontent.text_ratiocontent.activeGiven the previous issue to compare against.
changes.regressionsLimits
What it sees
What it cannot see
So it never reports a placement percentage, and every verdict carries a note that says so.
The checks are rules, not a model. The same message with the same DNS gets the same verdict, and there is no AI in the engine.
Open source
.eml file, or two to compare, and prints the report as text or JSON. Run it on your machine or in CI, with no account. sendscore/sendscoreStart with sendscore check --json issue.eml. More in the CLI docs and self-hosting.
Check the next issue before it goes out.