EMAIL GUARD
Once an agent reads and sends email, the inbox becomes an attack surface. An incoming message can hijack the agent with instructions hidden in the body, spoof a sender it trusts, or bait it into a phishing link. On the way out, an agent can leak a secret or torch its own sending domain. Email Guard is the deterministic check for both directions: no LLM, so it can't be talked out of its verdict by the email it's scanning.
FreeDeterministic, no LLMReads SPF / DKIM / DMARC resultsRDAPHTTP + MCP
verdict appears here…
| Endpoint | What it does |
|---|---|
POST /api/scan-inbound | The guard. Before the agent acts on a message: known injection and hijack patterns (including zero-width, bidi, and hidden-HTML payloads), sender header checks (SPF/DKIM/DMARC results as reported by the receiving server, reply-to mismatch, a 24-name brand list against the display name), a DNS and RDAP look at the sender domain (disposable, or registered in the last 30 days), link reputation → verdict allow / review / block plus fixed-shape metadata |
POST /api/scan-outbound | Before the agent sends: 22 secret patterns and 3 PII patterns over the subject, body and HTML (with a redacted copy), deliverability problems that burn the sender domain, dead recipients (disposable, or no MX = guaranteed bounce). Credential formats outside those 22 patterns are not detected, and the email-address rule is noisy: any address in the body puts the message at review |
GET /api/check-domain-auth | SPF, DMARC, MX, domain age (RDAP), and disposable status for any domain or address → weak / enforced |
Four string comparisons on headers. Reply-To domain against From domain and Return-Path domain against
From domain, by exact equality, so a legitimate mail.example.com sending for
example.com trips it too. An address written inside the display name that differs from the real
From address. And a 24-name brand list (paypal, apple, microsoft, google, amazon, netflix, coinbase, chase,
irs, docusign, usps, fedex and the like) matched as a substring of the display name, flagged only when that
same string is missing from the From domain.
That last one is the brand-impersonation flag, and it is narrow.
"PayPal Security" <svc@paypal-secure.tk> is not flagged, because "paypal" is in the domain.
"Barclays Bank" <svc@evil.tk> is not flagged, because Barclays is not on the list.
"PayPaI Security" with a capital i is not flagged, because the substring stops matching. There
is no general impersonation detection here.
I read SPF, DKIM and DMARC results out of the Authentication-Results header that the
receiving mail server wrote. Nothing here verifies a DKIM signature. No cryptography is involved at
any point.
DKIM DNS records are never looked up either. The record lives at
<selector>._domainkey.<domain> and the selector cannot be derived from a
domain name alone, so check-domain-auth reports SPF and DMARC only and returns an
explicit dkim: { checked: false }.
This matters for how much you trust the result. For mail your own server received, that header is
a good signal. For a raw .eml handed to you by an untrusted party it is worthless, because
an attacker can type dkim=pass into the file. Every response says which situation applies
and carries verified_here: false, so an agent cannot mistake a value we read for one we
checked.
{ "mcpServers": { "email-guard": { "command": "npx", "args": ["-y", "email-guard-mcp"] } } }
Or call the HTTP endpoints directly — JSON in, JSON out, CORS open. OpenAPI spec.
The dangerous move is letting an agent reason over the raw body of a hostile email, because that is where
the injection lives. scan-inbound gives you a fixed JSON shape instead: verdict, risk, score,
sender fields, auth results, domain age, injection findings, link analysis. The agent can decide what to do
from that without re-reading the attack.
The shape is fixed. Nothing in it is sanitised. Every field holding free text holds the attacker's text,
copied verbatim. subject is the first 200 characters of the subject line.
sender.display, sender.from, sender.replyTo and
sender.domain come straight off the headers. links[].url and
links[].host are the URL as written. injection.findings[].match is up to 120
characters of the matched body text. The note strings in sender.spoofFlags, and
the reasons list, quote those same addresses back at you. The verdict, the scores, the rule ids
and the advice string are mine; anything with free text in it came out of the email. Wrapping it in JSON does
not make it safe to obey. Treat it as data, the same as the body.
Data sources are all free and public: DNS, RDAP, disposable-domain lists, and the injection and secret
rulesets. Both scan endpoints return the ruleset version as rules_version.