Spam-protection minor. Adds three content-agnostic checks that target automated contact-form spam — the kind that fills valid-looking fields, varies its wording and language, and reuses throwaway sender addresses, so there's no content signature to match. All are opt-in, form-mode only, and backwards-compatible; existing configs are unaffected.
Added
- Reputation check (#44). Optional
[endpoints.reputation]— a StopForumSpam lookup on the submitter email and/or IP before send, blocking known form-spam identities regardless of message content or language. Bounded LRU result cache; fails open by default so a provider outage never blocks real mail (fail-open events incrementposthorn_check_failed_open_total{check="reputation"}). Sends email+IP to a third party — opt-in. - Proof-of-browser check (#45, ADR-18). Optional
[endpoints.proof_of_browser]— Posthorn serves a challenge token from aGETon the endpoint that only page JavaScript can fetch and inject as_pob_token, blocking bots that POST the form directly. An optionalmin_agetime-trap rejects impossibly-fast submissions. No third party; works on static sites. - Captcha check (#33). Optional
[endpoints.captcha]— Cloudflare Turnstile verification, the escalation tier for bots that render a real browser. Byte-identical silent 200 on a rejected token; fails closed by default on a provider outage. - New
posthorn_check_failed_open_total{endpoint,check}metric andspam_blockedkindsreputation/proof_of_browser/captcha.
Changed
- The three spam checks (origin, honeypot, CSRF) were refactored into a two-phase pipeline (#60): header checks pre-parse, form checks post-parse, each producing a
spam.Verdictthe handler renders through one mapping. No behavior change — the extraction is what let the three new checks land as small additions rather than more inline blocks.
Fixed
- The SMTP ingress was constructed with a nil metrics recorder, so
posthorn_submissions_sent_total{endpoint="smtp_listener"}and its failure counter never emitted. The registry and recorder are now shared with the HTTP mux, and the real send latency is recorded (was hardcoded 0). (#57) posthorn validatenow builds the SMTP ingress, so a listener config with a semantic error (e.g.require_tlswith notls_cert) is caught at validate time instead of failing at boot. (#58)
Recipes
- Added Authentik and Mastodon SMTP-listener recipes to the docs site.
Image: ghcr.io/craigmccaskill/posthorn:1.2.0 (multi-arch, cosign-signed, SBOM + provenance attested). Validated via v1.2.0-rc.1 before GA.