DadsFam AntiSpam — changelog for older versions
(Newer versions are listed in readme.txt and on the plugin's Changelog screen.)

= 2.12.2 =
* Ready for the WordPress.org plugin directory. Nothing changes in how it protects your site.
* The plugin's listed name is now "DadsFam AntiSpam", so its WordPress.org address matches its folder.
* The readme now describes every outside service the optional CAPTCHA can use: Cloudflare Turnstile and hCaptcha as well as Google reCAPTCHA.
* Fixed: the plugin's homepage link, and the link on the Add-ons screen, pointed at a page that no longer exists.
* Code-review tidying: clearer notes on the plugin's own database queries and a direct-access guard at the very top of two files.

= 2.12.1 =
* **Security: the reCAPTCHA secret key is no longer sent back to your browser.** The Settings page put the saved secret into the page — hidden as dots, but readable in the page source and kept by browsers' form memory. It now shows "✓ Saved" with an empty box: leave it blank to keep the key, or tick "Remove the saved secret key". The same way the Claude key works.
* The plugin description now lists the self-repair.

= 2.12.0 =
* **New: self-repair.** The plugin now checks itself every day, straight after every update, and whenever you (or your assistant) ask — and fixes what is safe to fix by itself: settings that were damaged, a missing Spam Log or Brain table, two CAPTCHAs on one form where DadsFam Login Security already has one, an address on both your whitelist and blocklist (the whitelist wins), duplicate list entries, the site's own server locked out by the rate limiter, a Brain that was never shown what the site's real writing looks like, public spam lists that stopped refreshing, and Autopilot blocks repeating a range you already blocked for good. Your dashboard says what it fixed, in plain words. It never switches a protection off to make a problem go away.
* **New: the Brain checks its own work.** It no longer vouches for a message that carries two or more classic spam signs — a Telegram contact, a shortened link, crypto, link code, three or more links, mixed alphabets, a name that is just the email address — however much its words sound like your site. (Found on the plugins site: its own pages are about SEO, so an SEO pitch "sounded like" a real message.) And while its track record shows it has doubted real messages lately, it holds itself to two points until the record recovers. Real customers come first.
* **New: an honest Brain.** When it thinks a message reads like your real mail but it noticed spam signs, it now says so instead of listing the signs as if they were the reason.
* **New: the Brain's numbers on your dashboard** — spam and real messages learned, words it knows, lessons from your other sites, and how much of the spam it judged it recognised.
* **New: remote control can run the self-repair** (run-self-repair), and the status report includes what it last fixed.
* Includes everything in 2.11.2.

= 2.11.2 =
* **Fixed: a range you blocked for good kept coming back as an Autopilot block.** Found on Ray Tac an hour after 2.11.1 went on: after "Block for good", the Autopilot looked at the log again, saw the same attack, and blocked the range a second time on top of your own block — longer each time. It now never blocks a range your own blocklist already covers, and tidies away any duplicate it made.
* **Fixed: "Set it up for me" could switch on "This site is behind a proxy" when it should not.** It decided from whichever request asked — so run remotely, or from an office network that adds a forwarded header, it could recommend trusting a header anyone can send. It now only recommends it when the connection really comes from a proxy in front of the site, and Cloudflare is recognised on its own as before.
* **Fixed: "Set it up for me" could put a second CAPTCHA on a form DadsFam Login Security already protects.** It now keeps the agreed split: where Login Security Pro puts its CAPTCHA on the login, registration or lost-password form, Anti-Spam stands down on that form — and it switches its own off there if it was on.

= 2.11.1 =
* **Fixed: remote control could not dry-run a spam message on sites behind a strict hosting firewall.** Found on plugins.dadsfam.co.za the day 2.11.0 went on: asking "would this spam get through?" came back as a firewall refusal (HTTP 406) before WordPress ever saw it — the spam words themselves tripped the host's firewall. The message, subject and search text can now also be sent base64-encoded, which a firewall cannot mistake for an attack; proven on that same site.
* **Fixed: the Self-Test said "This site is behind Cloudflare" whenever a request came from Cloudflare's network** — including an assistant connecting through Cloudflare's own servers — even on a site that is not behind Cloudflare. It now says so only when Cloudflare actually passes the visitor's address along.
* **Fixed: the Brain's status said "Awake and watching" while it was already helping decide.**

= 2.11.0 =
* **New: Claude second opinion (optional).** With your own Anthropic API key, a message the rules are unsure about — within two points of the spam line — is read by Claude, which says spam or real, how sure it is, and why. A confident "spam" blocks it and teaches the Brain, so the Brain gets sharper and needs Claude less over time. A very confident "real" lets through a genuine customer the rules were about to block. Anything the rules are sure about is never sent and never overruled, so a spammer cannot talk their way past it. Only the message, its subject, the kind of form and the email's domain are sent — never the visitor's IP address or name. A slow, failed or garbled answer changes nothing; a daily cap, and asking about the same message only once a day, keep the cost small. Off by default.
* **New: remote control.** Everything the plugin can do — 30 abilities — is published through the WordPress Abilities API (WordPress 6.9 and later), so an assistant such as Claude can answer "who is attacking the shop?", "why was this blocked?" and "would this message get through?", block or unblock anything, let an Autopilot block go, teach the Brain, change settings and run the Self-Test for you. Administrators only. A master switch and four group switches — look, manage, change settings, delete — can only be changed in wp-admin, so an assistant can never give itself more than you allowed. Every action is logged in Settings → Remote control, the two that delete are marked so an assistant asks first, it can never block the address it is asking from, and API keys can be set but never read back.
* **New: the Brain notices six more things today's spam carries** — Telegram and WhatsApp contacts, crypto and wallet addresses, shortened links, link code (HTML or forum tags), and messages written mostly in another alphabet. Each is learned from your own mail rather than applied as a rule: if your real customers say "WhatsApp me", the Brain learns that is normal on your site.
* **New: "What it has learned about your spam"** on the dashboard — the Brain's clearest lessons in plain words, such as "Three or more links: a strong sign of spam here (seen 412 times)".
* **Improved: the Self-Test** now reports on the Claude second opinion and on remote control.
* Includes everything in 2.10.0 (the Autopilot).

= 2.10.0 =
* **New: Autopilot — the Brain now blocks attackers by itself.** It reads the Spam Log and blocks the address, the network (256 addresses) or the whole range (65,536 addresses) a bot is working through. The attack that prompted it came from dozens of addresses in one range, each starting with a clean slate: every attempt was blocked, but stopping the attack itself meant a person reading the log and typing a range into the blocklist. Nobody has to now.
* **Blocks run out on their own, and come back longer.** One address: 1 day, then 3, 7 and 30. A network or range: 7 days, then 30 and 90. Waiting a block out never works — one that runs out while the attack is still going comes straight back at the next step.
* **It knows your real visitors and never blocks them.** Staff logins, paid WooCommerce orders, comments a moderator approved and "This was not spam" all mark a network as real, and a real network is never blocked. If a real customer turns up from inside a block, it is lifted on the spot. On its first run it reads who has already logged in, paid or had a comment approved, so an existing site is protected from its first decision. Only one-way hashes of those networks are stored.
* **Things it never does:** block a login, touch WooCommerce checkout, or block a private, reserved or Cloudflare address or the server itself. Your whitelist always wins.
* **On your dashboard:** what it is blocking right now and why, in plain words, with "Let back in" and "Block for good" on every block and a feed of what it did. The same list is on the Blocklist screen, and the Self-Test reports on it.
* **On by default, including on update.** It is the one new protection that does not wait to be switched on — a protection you have to remember to switch on is not install-and-forget. Settings → Autopilot turns it, or just the network and range blocks, off.
* **New: brand-new installs set themselves up.** The recommended settings that keep everything on your own server are applied the moment the plugin is activated. Anything that would contact another service still waits for you.
* **Fixed: sites behind Cloudflare with "behind a proxy" off saw every visitor as a Cloudflare address.** One bot could get a Cloudflare address locked out, and every real visitor coming through it with it. Cloudflare's published addresses are now recognised and the real visitor address is read automatically.
* **Improved: "Set it up for me" now switches on the whole Brain** — learning from real messages and helping decide, not only learning — and the Autopilot.

= 2.9.4 =
* **Fixed: spam caught on a WPForms form was still delivered.** Every check — the honeypot, the timer, the blocklist, the content filter, the email check, the rate limit and reCAPTCHA — recorded the block in your Spam Log and then told WPForms in a way WPForms never looks at, so WPForms carried on, saved the entry and emailed it to you. The block is now filed under the form itself, the way WPForms requires, and the submission stops with "Submission blocked." Proven against WPForms Lite 2.0.2.1. Contact Form 7, comments and registrations were never affected.
* **Fixed: on WPForms, the email check and the content filter never saw what was typed.** They read the WPForms submission as if it were shaped differently, so they never found the email address or the message — and left a PHP warning in the error log on every WPForms submission. They now read it the way WPForms sends it.

= 2.9.3 =
* **Fixed: the Spam Brain had never actually switched on — on any site.** Since the Brain first shipped, a one-word mistake in the code meant it never learned what a site's own genuine messages look like. Without that it could never decide it was ready, so it went on learning every spam it was shown while never once being allowed to use any of it. On one site that meant nearly six thousand spam messages learned and not a single decision made. The dashboard called this "Learning", which sounded healthy, and is why it went unnoticed. It now learns from the site's own pages, products and approved comments — including from the daily background job, so a site nobody logs into is covered too — and wakes up.
* **New: it wakes up watching, not acting.** A Brain that has been asleep does not get to start changing what gets blocked the moment the update lands. It gives every message a verdict and keeps a track record, but adds nothing to a decision until the owner switches "Let the Brain help decide" on. Every site updating to 2.9.3 blocks exactly what it blocked before.
* **New: a track record anyone can read.** The dashboard says in one sentence how the Brain is doing: how much of the site's spam it recognised, and whether it has ever been suspicious of a genuine message. When the record is good enough to trust, it says so and offers the switch. Verdicts are taken before the Brain learns from a message, never after, so it is never marking its own homework.
* **New: every block records what the Brain made of it** — including the no-JavaScript blocks, which until now recorded nothing about why.
* **New: who is attacking you.** The Spam Log spots when a pile of blocks is really one spammer sending the same message reworded over and over, and shows it as one campaign with a count, dates and an example, plus a button to see every copy. It groups on words that always travel together — the name of the prize, say — rather than on overall similarity, which was tried first and either shattered small campaigns or merged different ones. A genuine enquiry caught by mistake is never folded into a spam campaign. Read-only, and it works on the existing log immediately.
* Fixed: search only looked at the IP, email and name. With personal details not stored those are blank, so search could only ever find an IP. It now searches the message too.
* Fixed: the reason filter had fallen behind, so blocks caught by another DadsFam site through Shield could not be picked out. It now lists every reason actually in the log.
* Fixed: "Forget everything it learned" left the Brain unable to relearn the site's own writing, so it could fall straight back asleep.
* Fixed: uninstalling left four of the public spam lists added in 2.2.0 behind in the database — including the throwaway-email list, the largest thing the plugin stores — along with two of the Brain's own records, and the whole table of what the rest of the fleet had taught the site.
* Translation template included, and a handful of input checks and the uninstall clean-up tightened to current WordPress guidance.

= 2.8.0 =
* **The Brain can now learn from your other sites, not only its own traffic.** Until now every site worked out what spam looks like alone, from scratch, which meant a quiet site could sit for weeks knowing almost nothing while a busy one next door had already seen it all. With the Pro add-on's Shield switched on, what one site learns about individual words is pooled and handed to the rest. A site that has never been attacked can recognise a payload it has never seen. Measured: a fresh site scored an unseen pharmacy pitch at 0.5 — no opinion at all — and 1.0 once the fleet had shared what it knew.
* **What a site works out for itself always wins.** The fleet is only consulted for words this site has little or no experience of; once it has seen a word eight times, its own judgement is the only one that counts, and the fleet's is scaled down by half even below that. Tested directly: a regular customer's wording stays safe even when the fleet insists those exact words are spam.
* **Only hashes travel, as before.** A word is shared as an irreversible hash with two numbers beside it. There is nothing in it that can be turned back into a word, let alone a message, a name or an address.
* **Waiting for both sides is no longer a reason to sit idle.** The Brain held off scoring until it had seen enough spam *and* enough real messages of its own. A quiet site with borrowed spam knowledge and its own real messages now has both sides covered, so it acts instead of waiting.

= 2.7.3 =
* Version aligned with the Pro add-on. No change to the free plugin itself.

= 2.7.2 =
* Version aligned with the Pro add-on. No change to the free plugin itself.

= 2.7.1 =
* Version aligned with the Pro add-on. No change to the free plugin itself.

= 2.7.0 =
* **New: words written to dodge a filter are now folded back.** `v1agra`, `c-i-a-l-i-s`, `v.i.a.g.r.a`, `c1@l1s`, `v!agra` and full-width or accented lookalikes all straighten onto the real word, so the trick stops working the first time it is seen rather than every time it is varied. Learned as one disguise, caught as another.
* **Fixed: shouting got past everything.** "VIAGRA" and "viagra" were being learned as two unrelated words, so capitals alone were enough to walk past everything the Brain had already learned. Case is now folded, as it always should have been.

= 2.6.0 =
* **The Brain now uses Fisher's method instead of plain naive Bayes.** Multiplying probabilities together saturates — a handful of confident words drags the answer hard to one end and nothing else gets a say, which is why simple filters are loudest exactly when they are wrong. Fisher combines the evidence for and against as two independent chi-square tests, so a message with genuine evidence both ways lands in the middle and is left alone. Measured on held-out messages after identical training: the same spam caught, and one fewer real customer wrongly flagged. The message it rescued was "Thanks for the fast delivery, the rig fits perfectly and I will order again", which the old maths scored as spam.
* **A real message now counts double when weighing a word.** Turning away a customer is a far worse mistake than letting a spam through, and the maths now says so.
* **New: the Brain is no longer useless on a quiet site.** It has always refused to score until it has seen both spam and real messages — sensible, because a filter shown only spam believes everything is spam. But on a site with little contact-form traffic that wait can run to weeks. It now learns once, in the background, from your own published pages and posts: your writing, your products, your voice, which is far closer to a real enquiry than any generic word list. The Brain has a sense of normal from day one, and anything a real visitor sends afterwards still counts for more.
* **New: old evidence fades.** Spam changes. A word that meant spam with total confidence two years ago may be ordinary now, and a filter that never forgets becomes confidently out of date. Counts are halved on anything not seen for ninety days — what was learned is kept, just held less firmly, so fresh evidence leads.

= 2.5.0 =
* **New: Cloudflare Turnstile and hCaptcha, alongside Google reCAPTCHA.** Pick one under Settings → CAPTCHA. Turnstile is the one to reach for: free, unlimited, and invisible to real visitors — no puzzles, no pictures of traffic lights — and it catches the thing a hidden field never can, which is an automated browser that runs JavaScript and walks straight past the honeypot. That is exactly how spam gets through on a site that is being targeted rather than just swept. Nothing changes for anyone already using reCAPTCHA; it stays the default and your existing keys keep working.
* **New: the same message twice is now caught.** Bots fire one payload several times a minute. A person does not send the identical enquiry twice inside ten minutes, so the repeat is treated as spam. Only a hash is kept, never the message, and it expires on its own. Keyed on the address block rather than the exact address, because the same bot rotates through neighbours. Very short messages are never fingerprinted, so "Quote please" twice is still fine.
* **New: email addresses on a near-copy of a well-known provider are blocked.** `yahoo.co` for yahoo.com, `gmial.com` for gmail.com — one character out, or two swapped. A real person's address is either on the real thing or on something that looks nothing like it. Deliberately narrow: `yahoo.co.in`, `yahoo.co.jp`, `gmx.net` and the other genuine regional addresses are two ordinary edits from each other, so a looser rule would have rejected real customers. Tested against all of them.

= 2.4.1 =
* **Fixed: the self-learning Brain was never learning anything.** On a site whose contact form is built into the theme, the commonest block by far is a script posting straight at the handler without ever loading the page. Those blocks carry the full spam message — the address, the name, the payload — and the Brain was not being taught from a single one of them. A real site with three and a half thousand blocks on record had learned exactly zero. It now learns from them, which is precisely the material it needs.
* **Fixed: content fingerprints were being taken after the first check could already have blocked**, so a block at that check recorded no fingerprint and the pattern learner — the part that catches the same payload from a new address tomorrow — got nothing from it either. Fingerprints are now derived before anything can block.
* Together these mean the Brain and the pattern learner finally train on the traffic these sites actually get. Tested end to end: after a day of that traffic, the same payload from a brand-new address is recognised on its own, while a genuine enquiry is either left neutral or pulled towards safe.

= 2.4.0 =
* **Fixed, and this is the important one: a theme's own contact form had nothing behind the generic guard.** The `wp_mail` safety net scored every outgoing form email and then let it send anyway — it only ever wrote a log line. On a site whose contact form is built into the theme, rather than by Contact Form 7 or WPForms, that was the last line of defence and it was not defending anything. It now stops the message. The test for whether an email may be blocked is deliberately strict: a run of words from the email body has to appear verbatim in what the visitor just posted, so WooCommerce order confirmations, password resets, admin notices and every other generated email are never touched. Add-on developers can restore the old behaviour with the `dfsas_block_spam_mail` filter.
* **Fixed: the generic guard was still only checking the 53 built-in throwaway email domains.** 2.2.0 added a downloaded list of nearly nine thousand, and wired it into the email validator — but the guard called the bundled list directly and never saw the feed. It now checks both, when the feed is switched on.
* **Fixed: the spam log recorded almost nothing.** The page a bot posted to was being blanked along with names and addresses whenever "store personal details" was off — even though a URL is not personal data. Without it there was no way to tell which form was being hit. The page is now always recorded, and the generic guard passes the name and the start of the message too, so the log is readable.

= 2.3.0 =
* **The Brain now looks at the shape of a message, not only its words.** Nine language-independent signals — three or more links, mostly capitals, lots of exclamation marks, unusually many numbers, a phone number, an email address inside the message, very short, very long, and Cyrillic or Greek letters hidden inside Latin words (the classic "vіagra" trick). These catch spam using words the Brain has never seen, and they are safe to show a person because they describe the message, not its content. A normal question from a real person fires none of them — tested.
* **The Brain no longer learns from its own opinion.** When the content filter blocked a message, it taught the Brain "this was spam" — even when it was the Brain's own nudge that pushed the score over the line. That is a feedback loop, and it drifts. The filter now records whether the fixed rules reached the threshold by themselves, and only those verdicts become lessons.
* **"This was not spam" now teaches both sides.** It used to take back the spam lesson only. It now also teaches the message as a real one, so the Brain moves twice as far from the mistake — using the token hashes the log already keeps, never the text.
* **The Brain explains itself in the Spam Log.** Instead of `brain:+3 (p=0.92)`, a blocked message now says "The Brain is 92% sure this looks like your spam (14 features it recognised) — three or more links, mixed alphabets", and a rescued one says "…looks like your real messages".
* Includes everything from 2.2.0: nearly nine thousand throwaway email domains, three more public IP lists, and word-pair learning.

= 2.2.0 =
* **New: throwaway email domains, thousands of them.** A new list under Public spam lists downloads the most widely used open register of disposable and ten-minute email services — close to nine thousand domains, refreshed daily — and the disposable-email check uses it. The built-in list had 53. Off by default; "Set it up for me" turns it on. Stored gzipped in one option and unpacked only when an email is actually being checked, never on a page view. Exact matching, no hashing, no false positives from collisions.
* **New: three more public IP lists**, all from the same free FireHOL mirror, all off by default. BotScout's 30-day list — addresses caught filling in forms and sign-ups, the closest thing there is to a list built for this exact job (blocks). FireHOL's web-attacker list — a short, curated list of addresses attacking web servers right now (blocks). FireHOL's 24-hour abusers aggregate — everything reported for abuse in the last day, fresh but wide, so it only adds to the score. "Set it up for me" recommends the first two.
* **Improved: the Brain now learns word pairs.** "Buy" and "now" are each harmless; "buy now" is not. Adjacent-word pairs are added as their own features, which learn far faster than single words. Pairs start neutral, so a Brain trained before this update is not disturbed — it simply gets sharper from here on. The 200-feature cap per message still applies.
* Every new list appears as its own card under Public spam lists, with what it is, when it is safe, its entry count and when it last refreshed.

= 2.1.0 =
* **New: public spam lists (threat feeds).** The Spamhaus DROP and EDROP lists, StopForumSpam's toxic networks and last-7-days addresses, and the Tor exit list are downloaded to your server once a day — from the free FireHOL mirror, no keys, no account — and every submission, registration and comment is checked against them locally. A hit on a blocking list is turned away before anything else runs; a Tor exit only adds to the score unless you choose to block it. Nothing about the visitor is sent anywhere. Lists are stored as packed, sorted binary and searched by binary search, so checking 15 000 addresses costs the same as checking one. A download that fails keeps the last good copy, private addresses are never looked up, and your whitelist always wins. Off by default; "Set it up for me" recommends it.
* **New: StopForumSpam live lookup.** Optionally asks the StopForumSpam database — fed by thousands of sites — whether this exact IP or email account has been caught spamming, and blocks above a confidence you choose (80 by default, their own guidance). Answers are cached; a slow or unreachable lookup never blocks anyone. Off by default because it shares the visitor's IP and email with a third party; say so in your privacy policy if you turn it on.
* **New: Spam Brain.** A self-learning filter of the kind email has used for twenty years. It learns from every message the other checks confirm as spam and from every real message that gets through — including Contact Form 7 and WPForms submissions that were accepted and delivered, and every comment you mark spam or not-spam in Comments — then nudges the content score: up to +4 for wording that looks like your spam, down to −2 for wording that looks like your real customers. It never blocks on its own and only starts scoring after it has seen 20 of each. One-way hashes of words are stored, never the message. Off by default; "Set it up for me" recommends it.
* **New: "This was not spam" in the Spam Log.** One click whitelists the sender, lifts any lockout on the address, and makes the Brain unlearn that message. The dashboard help has pointed at this button for two versions; now it exists.
* **Fixed: the "Send Test Email" button did nothing.** It has been on the dashboard since 1.7 with no code behind it. It now sends a test to the site's admin address and reports the mailer's own error text when sending fails, so you can tell a broken mailer from a broken form.
* **Redesigned Settings.** One long page of cards has become sections you can jump to, with a search box that finds any setting by a word in its description, a plain-English sentence under every switch, a "Recommended" chip on every number that fills it in for you, preset pickers for anything measured in seconds or minutes (no more working out that 3600 is an hour), and a save bar that lights up the moment something changes and stays in view. Two settings that existed but were never on the screen — log retention days and "store submission details" — are now there. Every option keeps exactly the name it had, so nothing is lost on update.
* **Spam Log reasons in plain words.** "content_filter" is now "Message looked like spam"; "sfs_listed" is "Known spammer (StopForumSpam)"; and so on, on the log, the filter dropdown and the dashboard. A legend explains the action icons.
* Improved: the content scorer now also flags cold-outreach pitches ("rank your website", "dear website owner", "no upfront cost", "contact me on Telegram") as a light nudge, and treats thirteen more spam-heavy email TLDs (.top, .buzz, .icu, .cam, .rest, .monster and friends) as suspicious.
* Improved: the dashboard's Protection Health score now counts public spam lists and the Brain, a Threat Intelligence tile shows how many known-bad addresses are loaded, and the self-test reports both.
* Improved: the "Clear All Logs" placeholder row spanned eight columns in a seven-column table.

= 2.0.14 =
* **Fixed: with reCAPTCHA v3 on the login form, nobody could log in.** The login form was given a hidden token field, but the script that fills token fields was told to skip login forms — so the field stayed empty, every login arrived with no token, and every one was refused with "reCAPTCHA verification failed". This was deterministic, not intermittent, and it has been the case since the token script was rewritten. Login forms are now always tokened, and the token is minted with the correct action name (login, register, lostpassword) so Google scores it properly.
* **Changed: reCAPTCHA can no longer lock a real person out of login on its own.** A login already has a hard gate — the password — so reCAPTCHA there only needs to slow brute force down. A login with no token (script blocked by a privacy extension, or slow to load) is now allowed and logged. A single low score is allowed and logged; five low-score attempts from one address inside fifteen minutes earns a fifteen-minute lockout. A forged token or a bad secret key is still refused. This follows Google's own guidance for v3: act on the score, do not block outright on it. WooCommerce My Account login gets the same treatment.
* Fixed: a login refusal was logged with no details at all — no Google error code, no score, no whether a token was even present — which made this impossible to diagnose from the Spam Log. Every login decision now records all three.
* Fixed: WordPress can call the login check twice for one attempt (once by username, once by email). Tokens are single-use, so the second check always failed. The verdict is now reached once per request and reused.

= 2.0.13 =
* **Fixed: obvious form spam was scoring 2 out of 5 and walking straight through.** A real bot sent "Instant access grab your $25,000 promo code $>$>$>$> https://shorto.link/…" with a hundred-character random suffix, from a name like "MichaelFus", and the content filter saw nothing wrong but a single link. Seven new signals now catch what it missed: links through URL shorteners, prize and promo-code bait and large currency amounts, repeated symbol patterns, long random letter-and-digit tokens, bot-generated names, a name that contradicts the email address, and blank-line padding. That message now scores 19. Fifteen real customer messages — orders, quotes, tracking numbers, an ID number, a budget in rand — were tested alongside it and every one still passes.
* **Fixed: a bot that skips JavaScript skipped three checks at once.** A script posting straight at a form handler never runs the page, so it never picks up the honeypot, the timestamp or the reCAPTCHA token — and the plugin let all three slide to spare visitors with JavaScript off. A contact-form submission arriving without those fields now adds to its spam score, and a new "Require JavaScript on custom / theme forms" setting blocks such submissions outright. Off by default; the one-click setup recommends it.
* **New: lockouts that get longer each time an address comes back.** A persistent bot treats a fixed lockout as a timer — one was watched waiting out its hour and returning every sixty minutes through a whole night. With "Lock repeat offenders out for longer each time" on, every repeat doubles the lockout: 1 hour, 2, 4, 8, up to a week. A real person who trips it once still gets the short first one, and the unblock button resets it. Off by default; the one-click setup recommends it.
* Fixed: the pattern learner could be beaten by appending a random token to each message, because the token's letters survived into the fingerprint and made every copy hash differently. Words containing digits are now dropped before hashing, so the same spam with a different suffix is recognised as the same spam.
* Fixed: the sender's name was never passed to the content scorer, so name-based signals could not fire on custom forms.
* The dashboard's "Spam is still getting through" help now tells you exactly which two settings to turn on and why.

= 2.0.12 =
* **The whole admin has been redesigned.** A proper masthead with the navigation built into it, everything the plugin does laid out as six large tiles you press rather than words in a menu bar, cards that sit off the page instead of being outlined onto it, and larger, clearer type throughout. Built for people who do not want to learn a security plugin.
* **New: "When something goes wrong" — plain-English help built into the dashboard.** The four things people actually write in about — a real customer message got blocked, nobody can submit the form, I have locked myself out, spam is still getting through — each with the exact steps to fix it, and a button straight to the right screen.
* **New: one button that sets the plugin up properly for your site.** It looks at what you actually run — which form plugins are installed, whether comments are open, whether anyone can register, whether you sell anything, whether you sit behind Cloudflare — and turns on the settings that suit it. It shows you exactly what it will change and why, in plain English, before you press anything. Your reCAPTCHA keys, licence and blocklists are never touched.
* **The dashboard now tells you the truth.** It used to say "Your site is protected" no matter what was switched off. It now says plainly whether you are covered, could be tighter, or need attention — and it goes green only when there is genuinely nothing left to fix.
* Fixed: the default rate limit locked a visitor out for a full 24 hours after 5 submissions in an hour. On an office or a family sharing one connection that locks out real people for a day. The recommended setup uses 10 an hour with a one-hour hold instead.
* Fixed: three places read request data without unslashing it first, a discouraged text-domain call, a heredoc, and two nonce notices — the plugin now passes the WordPress.org Plugin Check with zero violations.

= 2.0.10 =
* **Fixed: reCAPTCHA v3 was silently throwing away real form submissions on any form that takes more than two minutes to fill in.** A v3 token is only valid for 120 seconds, and the plugin fetched one when the page loaded and then never refreshed it. Anybody completing a registration, an application or a long enquiry arrived with an expired token, Google replied "timeout-or-duplicate", and the submission was blocked — with no message, and on AJAX-based forms the submit button simply spun forever. The token is now kept fresh: refreshed on a timer while the form sits open, when you come back to a backgrounded tab, and again the moment you press submit.
* Fixed: an expired token no longer blocks the submission at all. Expiry means somebody took their time, not that they are a bot, so it is now allowed through and recorded in the Spam Log as "allowed" instead of being thrown away. Every other check — honeypot, time check, blocklist, disposable email, content filter and rate limiter — still runs on that submission exactly as before. A forged token, a bad secret or a low bot score are all still blocked.
* Fixed: on forms that submit by AJAX, the plugin no longer takes over the submit and posts the page away underneath the form's own handler.

= 2.0.9 =
* New: protection self-test. A single button on the dashboard runs a battery of health checks — log storage, visitor-IP detection, how many core modules are on, the custom-form guard, pattern learning and auto-lockout, a live spam-scoring sanity check (it scores a known spam sample and a known clean message), your reCAPTCHA keys, and the cleanup schedule — and reports a clear pass, warning or failure for each with plain-language guidance. Perfect for confirming each site is correctly protected after an update.
* New: maintenance tools on the dashboard. "Whitelist My IP" adds your current address to the whitelist so you can never lock yourself out while tuning the rules; "Clear Lockouts" releases every temporarily-blocked address and resets the rate counters; "Clear Learned Patterns" makes the guard forget the spam fingerprints it has learned, which is handy if a genuine message was ever caught.
* Improved: the dashboard's module list now also shows the Custom Form Guard, Spam Pattern Learning and Repeat-Offender Lockout, so their status is visible at a glance alongside the core modules.

= 2.0.8 =
* New: homoglyph / mixed-script detection. A word that mixes Latin with Cyrillic or Greek lookalike letters — the classic way to sneak a banned word past a filter, like a fake "PayPal" written with Cyrillic characters — is now flagged as suspicious. Genuinely non-Latin messages (a real Russian, Greek or other-language enquiry) are deliberately NOT penalised, because a whole word in a single non-Latin script is normal language, not an attack.
* New: more content signals — keyboard-walk and gibberish detection (qwerty/asdf runs and random consonant strings), obfuscated or injected links (BBCode and Markdown link markup, "hxxp" masking, raw IP-address URLs), the same URL repeated several times, shouting (mostly-capitalised) text, and expanded spam categories covering finance/crypto, pharma, loans, gambling, replica goods, adult, and SEO/backlink spam. Each is a weighted nudge against the score threshold rather than an instant block, so a single incidental mention will not trip on its own.
* Improved: because the content score also drives the IP-agnostic pattern learner, sharper detection means a rotating-IP bot's payload is confirmed and blocked sooner.
* Improved: custom and theme form blocks can now show your own message, or redirect the visitor to a page you choose instead of the default block screen — gentler if a genuine visitor is ever caught. Redirects are constrained to your own site and can never become an open redirect.
* Developer: new dfsas_content_score filter so add-ons can contribute to a submission's spam score.

= 2.0.7 =
* New: IP-agnostic spam-pattern learning. When a submission is confirmed as spam, the guard records a one-way fingerprint of its message wording and of the set of domains it links to. A later submission matching a known pattern is blocked immediately — even from a completely different IP — which defeats a bot that rotates its address (for example every 24 hours) while sending the same payload. The check runs early and is cheap, so a known-spam payload never reaches the reCAPTCHA round-trip to Google.
* Privacy and accuracy: only one-way hashes are stored, never the message content. Learning is taken only from trustworthy content signals (a filled honeypot, a spam keyword, a disposable address, a high content score, a failed reCAPTCHA); an IP, email or domain block teaches the learner only when the message itself independently scores as spam, so a genuine visitor on a blocked range is never fingerprinted. Every path fails open.
* New: controls on the "Custom Forms & Repeat Offenders" settings card for how many confirmations are needed before a pattern is auto-blocked and how long a pattern is remembered, plus a "Known Spam Pattern" reason in the Spam Log filter.

= 2.0.6 =
* New: repeat-offender auto-lockout. When the same address is blocked several times in a row, it is placed into a temporary lockout and its next submission is refused immediately — before the blocklist scan and before any reCAPTCHA round-trip to Google. A persistent bot no longer costs a full pipeline run on every hit. The lockout reuses the same mechanism as the rate limiter, so the unblock button in the Spam Log clears it instantly.
* New: log flood control. Identical entries for the same address and reason are throttled — by default at most one every five minutes — so a single relentless bot can no longer bury the Spam Log in hundreds of duplicate rows. The moment an address is locked is always recorded. Fully configurable, or set to 0 to log every hit.
* New: a "Custom Forms & Repeat Offenders" card on the Settings screen collects the generic-guard and escalation controls, and a "Locked Out" reason can be filtered in the Spam Log.

= 2.0.5 =
* Fixed: saving Settings no longer resets options shown on the Blocklist screen, and vice versa. Each admin form now saves only the fields it actually contains; every other stored value is carried forward untouched. Every checkbox ships a companion hidden field so an unchecked box is recorded as off rather than going missing, which is what let one screen quietly wipe another before.
* Fixed: the "Test reCAPTCHA Keys" button now works. It asks Google to validate the saved secret key and reports plainly whether the keys are valid, rather than doing nothing.
* Fixed: several admin actions had silently stopped working — deleting or clearing the spam log, quick-blocking an address from the log, exporting and importing settings, and clearing a rate-limit lockout. The admin script was reading its configuration from the wrong place, so those requests went nowhere. Script and server now agree.
* New: custom and theme contact forms are protected on the server. A generic guard inspects real submissions that no dedicated integration handles — a bespoke "Contact Us" page, a page-builder widget, a plain HTML form — and applies the honeypot, time check, blocklist, disposable-email, content-scoring and rate-limit rules to them. It steps aside for forms an integration already covers, exempts logged-in staff and whitelisted addresses, and fails open so a real visitor is never blocked on a guess.
* New: reCAPTCHA now works on custom and theme contact forms for every version, not only v3. With "Generic HTML Forms (JS)" switched on, v3 runs invisibly and is verified on the server; v2 Checkbox and v2 Invisible are now rendered directly into the form — the tick box or the invisible badge — and verified on the server as well. This closes the gap where reCAPTCHA appeared to have no effect on a bespoke contact page because nothing was ever rendered or verified there. Forms are detected automatically by their message box, and an optional CSS selector lets you target an exact form.
* New: a Support tab with the support forum, email, contact form and plugin links, plus a short checklist for hardening a custom contact page.

= 2.0.4 =
* Version aligned with the PRO add-on 2.0.4, which fixes the disposable-list update schedule. No functional change to the free plugin.

= 2.0.3 =
* Version aligned with the PRO add-on 2.0.3, which fixes a licence-activation problem. No functional change to the free plugin.

= 2.0.2 =
* Fixed: only two of the nine protection modules actually honoured the "skip checks for logged-in staff" rule, so a site owner testing their own contact form could still be blocked by the time check, the content filter, the email validator or reCAPTCHA. All nine now honour it, as does the optional PRO add-on. The documented `dfsas_bypass_checks` filter now genuinely applies everywhere.
* Fixed: the MX-record check treated "this domain has no mail server" and "DNS could not answer" as the same thing. On a host with a slow or failing resolver that meant every visitor was told their email address was invalid. A negative result is now only trusted when name resolution is confirmed to be working; otherwise the address is accepted, because refusing a real customer is the worse mistake. The confirmation is cached, so it costs at most one extra lookup an hour and never runs unless a check has already failed.
* New: a `dfsas_dns_probe_domain` filter, for sites whose firewall blocks the default probe.

= 2.0.1 =
* Removed dead code from the admin JavaScript. It still carried licence-activation and remote-list handlers left over from the previous combined plugin, bound to controls that no longer exist. Nothing user-facing changed, but a free plugin has no business shipping licence machinery.
* Fixed: the hook reference in the plugin header listed two hooks that were never actually fired, and omitted four that were. The documented list and the code now match.
* New: a `dfsas_dashboard_page_end` action so an add-on can append its own dashboard panels.
* Output escaping tightened across the admin screens, and translator comments added wherever a placeholder is used.

= 2.0.0 =
* Security: the visitor IP is no longer read from forwarded headers by default. Trusting `X-Forwarded-For`, `CF-Connecting-IP` or `X-Real-IP` unconditionally meant anyone could send a made-up header to bypass the IP blocklist entirely, and could reset their rate-limit allowance on every single request. A "This site is behind a proxy" setting enables header reading for sites that genuinely need it.
* Fixed: a form posting a field as an array (`email[]`, `message[]`) caused a fatal error and a blank page on submit. Every request read is now array-safe.
* Fixed: the whitelist is available on every site. It was previously unavailable without a licence, which meant an admin could block a range containing their own address and lock themselves out of wp-login with no way to recover.
* Fixed: the rate limiter now consults the whitelist and exempts logged-in staff. Neither was checked before, so a whitelisted address could still be locked out.
* Fixed: the rate-limit window is fixed rather than sliding, so a steady stream of legitimate submissions no longer extends a lockout indefinitely.
* Fixed: settings saved programmatically are now sanitised. One code path previously stored the submitted array without sanitising it at all.
* Fixed: scheduled events are removed when the feature that owns them is switched off, and their callbacks are always registered, so an event can no longer be left firing with nothing listening.
* Performance: the free-tier log cap ran a table-wide DELETE with a subquery on every single log write, which is exactly the wrong thing to do while a site is being flooded. History is now unlimited, with a daily retention clean-up instead.
* Performance: dashboard statistics use one aggregate query rather than six separate counts, and CSV export streams in batches so a large log cannot exhaust memory.
* Changed: every list is unlimited — IPs, emails, domains, usernames, keywords and log history. The full disposable-domain list is bundled.
* Changed: keyword matching respects word boundaries for short keywords, so "seo" no longer flags "video".
* Changed: default rate limits relaxed from 5 submissions an hour with a 24-hour lockout to 10 an hour with a 1-hour lockout.
* New: a "Store submission details" setting to keep personal data out of the log entirely.
* New: IPv6 support in the blocklist, including IPv6 CIDR ranges.
* New: a documented hook API so add-ons can extend the plugin cleanly.
* Upgrading from 1.9.x keeps every setting and log entry.

= 1.9.6 =
* Routing and detection refinements across the form integrations.

= 1.5.0 =
* Comment spam protection with scoring.

= 1.2.0 =
* Google reCAPTCHA v2 and v3 support.

= 1.0.0 =
* First release.
