Predax Blog

Product updates, best practices, and deep dives into IP intelligence.

Checking Customer Emails Without Ever Seeing Them: Disposable-Email Screening in Predax 1.15.0
Predax Team

Checking Customer Emails Without Ever Seeing Them: Disposable-Email Screening in Predax 1.15.0

disposable-emailemail-validationfake-signupswoocommerce-fraudwordpress-securityprivacy

Both of our WordPress plugins have carried the same sentence in their privacy disclosures since the day they were published: only the visitor's IP address is checked — no names, no emails, nothing else leaves your site. That sentence made adding an email check awkward, because the obvious way to build one is to send the whole address to an API and get a verdict back. Every registration, every checkout, a customer's email on the wire. We weren't willing to do that, so the feature that shipped this week in Security 1.15.0 and Fraud Guard 1.11.0 works differently: your site sends us the domain half of the address — mailinator.com, never [email protected] — and everything that needs to look at the part before the @ runs in PHP on your own server, with no network call at all. It turns out the domain is where nearly all the useful signal lives anyway. Here's how it works, what it catches, and what it deliberately refuses to block on.

TL;DR

  • The plugin sends the domain only. The mailbox name never leaves your server.
  • Domains are checked against a list of 8,310 known throwaway providers, refreshed weekly, on top of the just-under-fifty list bundled in the plugin itself.
  • Role accounts and random-looking names are judged locally, in PHP, with no API call.
  • Only a throwaway domain can refuse a registration or checkout. Every other signal flags and logs, never blocks.
  • An enabled check uses your plan allowance — about one call per new domain per six hours, cached and shared between both plugins.

Why throwaway addresses are worth catching

A disposable address is one a visitor gets in two clicks, uses once, and abandons. Mailinator, Guerrilla Mail, YOPmail and thousands of smaller clones all work the same way: no signup, no password, an inbox anyone can read that disappears on its own.

For a legitimate customer that's a strange choice. For someone abusing your site it's the obvious one, because the whole point is to not be reachable afterwards.

Two patterns show up over and over. The first is fake signups: an automated script working through your registration form, leaving behind a user table full of accounts nobody will ever log into and a mailing list whose deliverability slowly degrades as those addresses bounce. We wrote about the mechanics of that in the fake-signups playbook. The second is card testing, where someone runs stolen card numbers through your checkout in small amounts to find which ones still work, and needs a fresh unlinkable email address for each attempt. That one is expensive, because the gateway fees and eventual chargebacks land on you. There's a whole post on card testing if that's the problem you have.

In both cases the throwaway address isn't the attack. It's the tell.

The constraint that shaped the design

Here is the thing we could not do: take [email protected], POST it to our API, and read back a verdict. It would have been the simplest possible implementation, and it would have made the privacy line in both readmes false. Sites install our plugins partly because that line is there.

So we split the address. The domain goes to the API, because everything genuinely domain-level lives there — whether it's a known throwaway provider, whether it's a free consumer mailbox, whether it has any mail server configured at all. The mailbox name stays home, because the two checks that need it are cheap enough to run in PHP: is this a role account like info@ or sales@, and does the local part look like something a keyboard generated rather than a person.

Code open in an editor on a laptop screen, lit blue, representing the local PHP checks that run on the site's own server rather than being sent to an API
Code open in an editor on a laptop screen, lit blue, representing the local PHP checks that run on the site's own server rather than being sent to an API

That split has a second benefit we didn't plan for but happily took: it caches far better. Your customers share domains constantly — a hundred shoppers might produce ninety gmail.com addresses — while a full address repeats almost never. Sending domains means most checks are answered from a local cache and never reach the network at all.

What the shared list catches that the bundled one misses

Both plugins have always shipped with a small hardcoded list of throwaway domains, just under fifty of them. It covers the famous ones and it's better than nothing, but it's a static array in a PHP file, which means it only updates when the plugin does.

The server-side list has 8,310 domains and refreshes weekly from the disposable-email-domains project, which is released under CC0. That's the practical difference the feature buys you.

To make it concrete: getnada.com is a throwaway provider that isn't in the plugin's bundled list. Before this release, a registration from that domain sailed straight through. Now it doesn't. Multiply that by several thousand.

The matching also walks subdomains, which matters more than it sounds. Providers like Mailinator accept mail at arbitrary subdomains, so [email protected] reaches the same inbox as [email protected]. Exact-string matching would have caught the second and waved through the first, which is a one-character bypass of the entire feature. The check now walks up the domain to its registrable part, stopping short of public suffixes so foo.co.uk is never reduced to co.uk.

None of that turns a list into a solution. A throwaway domain registered this morning is on nobody's list, including ours, until someone notices it. Lists are a floor, not a ceiling, which is part of why the local checks exist alongside this one rather than instead of it.

What deliberately doesn't block

This is the part we spent the most time on, because a fraud check that occasionally refuses a real customer is worse than no check at all. We've written about that trade-off before and the same rule applied here.

Role accounts score nothing. info@, sales@ and accounts@ are detected and reported, and they add exactly zero to the risk score. That's how a great many legitimate businesses buy things. Scoring it would have meant quietly penalising B2B customers for using a shared mailbox, which is normal behaviour, not suspicious behaviour.

Random-looking names flag but can't block. The check scores 25 out of 100, deliberately below the line where anything happens. It exists to add context in your log — an address that looks machine-generated *alongside* a Tor exit node is a different story than either on its own — not to make decisions. The first version of it flagged bjorkstrand, which is a real Scandinavian surname, so we raised the threshold until all fifty of the real surnames in our test set passed, and accepted that we'd miss some genuine junk in exchange.

A DNS timeout is not evidence. The check for whether a domain can receive mail at all has three possible answers, not two: yes, no, and couldn't tell. Early on it had only two, so any DNS failure — a timeout, a slow resolver, a bad moment on our side — came back as "this domain cannot receive mail" and added 60 to the risk score. That's a real customer flagged because of our infrastructure. Now an unanswerable lookup adds nothing and is reported honestly as unknown.

And the whole thing fails open. If our API is unreachable the plugin falls back to its bundled list and the registration or checkout continues. The local list is consulted every time regardless, so enabling this can only ever *add* blocks to what your site already did.

The one signal that can actually refuse someone is a domain on the throwaway list, and even that is a setting rather than a default.

Turning it on, and what it costs

In Predax Security the control is on the settings page as Block disposable email addresses, with three positions: off, flag, and block. In Predax Fraud Guard the equivalent setting governs the checkout email. Both ship off, so nothing changes on an existing install until you decide it should.

If you're not sure, run it on flag for a week and read your threat log. You'll find out whether you actually have this problem before you start refusing anyone, which is the same advice we give for every other blocking feature.

The honest cost line: an enabled check consumes your API plan allowance, at roughly one call per new domain per six hours per account. Repeated domains are served from cache and cost nothing, and the cache is shared between the two plugins, so a site running both doesn't pay twice for the same shopper. A store whose customers mostly use the big free providers will see a handful of calls a day. A site being actively hit with thousands of different throwaway domains will see more, though at that point you have bigger problems than allowance.

Also new: choosing which crawlers may read your site

Worth mentioning since it landed in the previous release. Security 1.14.0 added a crawler policy with three switches: allow or block search engines, AI crawlers, and SEO crawlers, independently. All three default to allow, so nothing changes unless you change it.

The part that makes it work is that identity is checked against the IP ranges each operator publishes, not the user-agent header, which anyone can type. Something claiming to be Googlebot from an address Google doesn't own gets no special treatment from either switch.

The limitation is the one every range-based approach has: it only covers crawlers that publish ranges. CCBot, Amazonbot and Bytespider don't publish any, so they fall through to your normal rules rather than being covered by the AI switch. There's more detail on the WordPress plugin page.

Where this leaves things

Both updates are live on WordPress.org now, Security 1.15.0 and Fraud Guard 1.11.0, and will arrive through the normal WordPress update screen if you already have them installed.

If you'd rather work with the API directly, the same checks are available as a documented endpoint that accepts either a full address or a bare domain and returns the individual signals rather than a single verdict. That's in the API reference.

And if you turn this on and it flags something it shouldn't have, tell us. The bjorkstrand case came from testing the check against real names before shipping it, and the whole design of this feature came out of taking that failure mode seriously.