Better Than CAPTCHA
Publish a real email address again, and delete the contact form.
What it does
Watches the mailbox behind your published address and decides, per message, whether a human should ever see it — using four checks, cheapest first.
- Header forensics.
email-authenticity-checkreads the SPF, DKIM and DMARC results the receiving server already recorded, compares the visible From domain against Return-Path, Reply-To and the DKIM signing domain, and flags link domains that don't match the sender. Deterministic Python: no model, no tokens, the same answer every time for the same headers. Forged mail dies here having cost nothing. - Known sender. Anyone who has written before goes straight through. A person is challenged at most once, ever.
- Confirm reply. An unknown but authentic sender gets one automatic reply asking them to reply once — anything, even a single word. Their reply arrives as ordinary mail and adds them to the known list permanently.
- Route. Real mail reaches you. Everything held back is listed in a digest, never silently dropped.
Why this instead of a CAPTCHA
Vendors hide behind CAPTCHA forms for one reason: publish an address and the inbox drowns. So the customer pays — clicking traffic lights to ask a simple question — to solve the vendor's spam problem.
A puzzle asks a human to prove they're not a robot. This asks them to send one reply, which a human does in two seconds and a bulk mailer almost never does at all, because completing a round trip per address destroys the economics of blasting. Nothing is outsourced to a third party, no visitor is fingerprinted, and there is no account to create.
Why it isn't a spam filter
Spam filters score spamminess. Most of what makes a published address unusable is perfectly authenticated, perfectly legitimate bulk pitching — it is simply not addressed to a problem you have. Rungs 2 and 3 sort that out by whether a human on the other end will spend two seconds, which no content score can measure.
Use case
You want [email protected] on your website instead of a form. A prospective client emails
you and gets through on their second message, which they send within a minute of the first. A
vendor blasting 40,000 addresses never replies to anything, so you never see them.
Setup
Credential — one email-read credential named contact-inbox: IMAP host, port, the
address, and an app password. Read-only by construction: the folder is opened READ_ONLY, so
this workflow can never mark, move or delete your mail.
Recipient — the last task sends the digest to [email protected]. Change it to a real
address, a contact name, or user:<name> for an in-app inbox.
Schedule — every 5 to 15 minutes. A first-time sender waits that long for the confirmation reply, so shorter is friendlier.
State — two plain text files under $INTOUCH_HOME/state/better-than-captcha/, one entry
per line, meant to be edited by hand:
| file | what it holds |
|---|---|
known-senders.txt |
addresses that skip the challenge. Add colleagues and existing clients here before you go live. |
seen-ids.txt |
message ids already handled, so nobody is challenged twice. |
What it does NOT do
- It does not read replies. Promoting a sender to known when they reply is not automated
yet: today you add them to
known-senders.txt. Until that lands, the same person is challenged once per new thread rather than once ever. - It does not judge intent. The optional fifth rung — an AI step reading the body to separate a real enquiry from an SEO pitch — is not in this version. Everything authentic and unknown gets the confirmation reply.
- It does not touch your mailbox. No flags, no moves, no deletes. "Held back" means listed in the digest and not replied to; the message is still sitting in your inbox.
- It has not been run against a live mailbox. The steps and their contracts are verified and the workflow validates; the behaviour against real mail at volume is not yet measured.
Why the batch is small
maxMessages is 10. A published property is truncated at 10,000 characters, and the decision
step reads the message JSON from an environment variable — a larger batch would be cut
mid-object and silently drop the tail. Ten messages every five minutes is 120 an hour, far more
than a contact address receives.