https://github.com/schmug/dmarcheck
DNS email security analyzer — DMARC, SPF, DKIM, BIMI & MTA-STS. Cloudflare Worker with JSON API and interactive HTML report.
https://github.com/schmug/dmarcheck
bimi cloudflare-workers dkim dmarc dns email-security mta-sts spf
Last synced: 4 days ago
JSON representation
DNS email security analyzer — DMARC, SPF, DKIM, BIMI & MTA-STS. Cloudflare Worker with JSON API and interactive HTML report.
- Host: GitHub
- URL: https://github.com/schmug/dmarcheck
- Owner: schmug
- License: mit
- Created: 2026-03-30T22:14:52.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-23T22:50:57.000Z (11 days ago)
- Last Synced: 2026-05-24T00:24:12.346Z (11 days ago)
- Topics: bimi, cloudflare-workers, dkim, dmarc, dns, email-security, mta-sts, spf
- Language: TypeScript
- Homepage: https://dmarc.mx
- Size: 1.28 MB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 17
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
dmarcheck
DNS email security analyzer — checks DMARC, SPF, DKIM, BIMI, and MTA-STS records for any domain.
Meet **DMarcus**, the @ creature who guards your inbox.
**Live at [dmarc.mx](https://dmarc.mx)**
## Three ways to use dmarcheck
| | Where | What you get |
|---|---|---|
| **Free scanner** | [dmarc.mx](https://dmarc.mx) | Unlimited on-demand scans, JSON API, 10 req/min per IP |
| **Pro — $19/mo** | [dmarc.mx/pricing](https://dmarc.mx/pricing) | Nightly monitoring (25 domains), email alerts on grade drops, saved history, bulk scan, 60 req/hour API key |
| **Self-host** | this repo | Clone, `wrangler deploy`, run your own. MIT-licensed. Pro features activate when you configure D1 / WorkOS / Stripe bindings — optional, see [below](#optional-paid-tier-env-vars). |
The hosted tier at `dmarc.mx` exists for people who'd rather pay than run it. Everything is MIT and there is no crippled-OSS / paid-premium split.
## Features
- **DMARC** — Policy parsing, validation, reporting URI checks
- **SPF** — Full recursive include chain resolution, 10-lookup limit tracking, misconfiguration detection
- **DKIM** — Auto-probes ~30 common selectors + custom selectors, key strength analysis
- **BIMI** — Logo and VMC validation, DMARC policy cross-check
- **MTA-STS** — DNS record + HTTPS policy file fetch and validation
### Dual Output
- **JSON API** for developers — `curl https://dmarc.mx/api/check?domain=dmarc.mx`
- **Interactive HTML report** for browsers — expandable protocol cards, SPF include tree, educational tooltips
### Grading
Letter grades from A+ to F. DMARC is the gatekeeper — no DMARC record or `p=none` is an automatic F.
| Grade | Criteria |
|-------|----------|
| **A+** | All five protocols fully configured and validated |
| **A** | DMARC reject, SPF valid with hardfail, DKIM found, + BIMI or MTA-STS |
| **B** | DMARC reject, SPF + DKIM present |
| **C** | DMARC quarantine, SPF + DKIM present |
| **D** | DMARC quarantine but missing SPF or DKIM |
| **F** | No DMARC record or `p=none` |
## What is email security?
New to DMARC, SPF, and DKIM? Cloudflare has a great primer: [What are DMARC, DKIM, and SPF?](https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/)
### Message header analyzers
Once your DNS records are configured, you can verify authentication results in actual email headers:
- [Google Admin Toolbox — Message Header Analyzer](https://toolbox.googleapps.com/apps/messageheader/)
- [Microsoft Remote Connectivity Analyzer — Message Header](https://mha.azurewebsites.net/)
## API
```bash
# JSON response
curl https://dmarc.mx/api/check?domain=dmarc.mx
# With custom DKIM selectors
curl "https://dmarc.mx/api/check?domain=dmarc.mx&selectors=myselector,other"
# Content negotiation on /check
curl -H "Accept: application/json" "https://dmarc.mx/check?domain=dmarc.mx"
```
Response includes a `summary` with elevated fields and full `protocols` detail:
```json
{
"domain": "example.com",
"grade": "A",
"summary": {
"dmarc_policy": "reject",
"spf_result": "pass",
"spf_lookups": "3/10",
"dkim_selectors_found": 2,
"bimi_enabled": true,
"mta_sts_mode": "enforce"
},
"protocols": { ... }
}
```
## Self-Hosting
You can deploy your own instance of dmarcheck to Cloudflare Workers.
### Prerequisites
- [Node.js](https://nodejs.org/) (v18+)
- A [Cloudflare account](https://dash.cloudflare.com/sign-up) (free plan works)
### Setup
```bash
git clone https://github.com/schmug/dmarcheck.git
cd dmarcheck
npm install
```
### Local Development
```bash
npm run dev # starts local dev server on http://localhost:8790
npm test # runs unit tests
```
To point the DNS resolver at custom servers during local development (useful
for testing against `1.1.1.1`, `8.8.8.8`, or an internal resolver), set
`DNS_SERVERS` to a comma-separated list:
```bash
DNS_SERVERS=8.8.8.8,1.1.1.1 npm run dev
```
When unset, the system default resolver (or the Cloudflare Workers DNS
polyfill in production) is used.
### Deploy to Your Own Cloudflare Account
1. Authenticate with Cloudflare:
```bash
npx wrangler login
```
2. Edit `wrangler.toml` to use your own custom domain (or remove the `routes` block to use the default `*.workers.dev` subdomain):
```toml
routes = [
{ pattern = "yourdomain.com", custom_domain = true },
]
```
3. Deploy:
```bash
npm run deploy
```
### Configuration
| Setting | Location | Default |
|---------|----------|---------|
| Dev server port | `wrangler.toml` → `[dev].port` | 8790 |
| Rate limit | `src/rate-limit.ts` → `LIMIT` | 10 req/IP/min |
| Rate limit window | `src/rate-limit.ts` → `WINDOW_SECONDS` | 60s |
| DKIM selectors | `src/analyzers/dkim.ts` → `COMMON_SELECTORS` | ~30 common selectors |
| Scoring rubric | `wrangler.toml` → `[vars].SCORING_CONFIG` (JSON) | shipped rubric — see [`src/shared/scoring-config.ts`](src/shared/scoring-config.ts) for the knobs |
### Database migrations
The dashboard, history, and alerts features use Cloudflare D1. Migrations live
in `src/db/migrations/` and apply automatically via
`.github/workflows/migrate.yml` after CI passes on `main`. The workflow
expects two GitHub repo secrets (Settings → Secrets and variables → Actions):
| Secret | Value |
|--------|-------|
| `CLOUDFLARE_D1_TOKEN` | API token scoped to **D1:Edit** only (separate from the Workers deploy token so a leak in one doesn't widen into the other) |
| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |
If you skip the secrets, the workflow will fail and you can either delete
`.github/workflows/migrate.yml` from your fork or apply migrations manually
with `npx wrangler d1 migrations apply dmarcheck-db --remote`.
### Optional: paid-tier env vars
These unlock the same Pro features the hosted tier at `dmarc.mx` offers — see the [three ways to use dmarcheck](#three-ways-to-use-dmarcheck) table up top. They're all optional: the free scanner, dashboard, and API work without any of them. Set any as wrangler secrets (`wrangler secret put NAME`):
| Secret | Purpose |
|--------|---------|
| `STRIPE_SECRET_KEY` | Stripe API key for Checkout + Portal calls |
| `STRIPE_WEBHOOK_SECRET` | Signing secret for the `/webhooks/stripe` endpoint |
| `STRIPE_PRICE_ID_PRO` | Price ID for the Pro plan offered via Checkout |
| `CF_ANALYTICS_TOKEN` | Cloudflare Web Analytics beacon token (32-char lowercase hex). Injects a cookieless beacon on public HTML pages; skipped on `/dashboard/*`, `/auth/*`, `/webhooks/*`. Leave unset to disable. |
If any of the three Stripe secrets are missing, `isBillingEnabled` returns false and all
paid-tier routes return 404. This keeps a fresh `wrangler deploy` working
end-to-end for self-hosters who only want the free scanner. `CF_ANALYTICS_TOKEN`
is independent — set it only if you want to send analytics to your own
Cloudflare Web Analytics dashboard.
## Stack
- [Hono](https://hono.dev) — lightweight web framework for Cloudflare Workers
- TypeScript + `node:dns` via `nodejs_compat`
- Rate limited via Cloudflare Cache API (no extra bindings needed)
- Pro features (auth, billing, history, cron) use Cloudflare D1 + WorkOS + Stripe. The hosted tier at `dmarc.mx` runs on the same code and same stack — no private fork.
## Contributing & reporting
- **Found a bug or want a feature?** Open an issue in the
[issue tracker](https://github.com/schmug/dmarcheck/issues) — search first to
avoid duplicates.
- **Found a security vulnerability?** Please **don't** open a public issue.
Report it privately via the process in [SECURITY.md](SECURITY.md).
- **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md) for setup,
the test/lint/typecheck gate, and commit conventions. Maintainers and the
review policy are listed in [MAINTAINERS.md](MAINTAINERS.md).
Security posture is documented in [SECURITY.md](SECURITY.md) and the
[threat model](THREAT_MODEL.md).
## Verifying a release
Every release ships a `SHA256SUMS` manifest (covering the SBOM and the
GitHub-generated source archives) and a Sigstore keyless signature bundle
(`SHA256SUMS.bundle`). The signing identity is the GitHub Actions workflow
itself — no long-lived private key is involved.
### Prerequisites
Install [cosign](https://docs.sigstore.dev/cosign/system_config/installation/):
```bash
# macOS / Linux with Homebrew
brew install cosign
# or via Go
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
```
### Verify a release
Replace `vYYYY.M.N` with the tag you want to verify (e.g. `v2026.5.1`).
```bash
# 1. Download the manifest and signature bundle
gh release download vYYYY.M.N -R schmug/dmarcheck --pattern 'SHA256SUMS*'
# 2. Verify the Sigstore signature — confirms the bundle was signed by
# the release workflow running on the schmug/dmarcheck repository
cosign verify-blob \
--bundle SHA256SUMS.bundle \
--certificate-identity-regexp "https://github.com/schmug/dmarcheck/.github/workflows/release.yml" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
SHA256SUMS
# 3. Check file integrity
sha256sum -c SHA256SUMS
```
A successful run of step 2 prints `Verified OK`. Step 3 confirms that the
downloaded archives match the checksums signed in step 2.
## License
MIT