Skip to content

Detecting machine-generated email addresses

· 7 min read · Disposable email

You detect a machine-generated email address by scoring its local part (the bit before the @) on a few features that real names rarely have: letter pairs that almost never occur in human language, long runs of consonants, digits wedged between letters, and number suffixes that aren't a year or a date. xk7qmz2p@gmail.com trips all of them, priya.sharma91@gmail.com trips none. Combine the features into a score, block only at the high end, and expect some real people to land in the middle.

This matters because domain blocklists can't see these addresses. The domain is Gmail or Outlook, perfectly legitimate. A script just created thousands of accounts there with gibberish usernames, then used them to sign up for your product.

Why the domain check isn't enough

A disposable email check answers "is this inbox a throwaway service?". It says nothing about ccjxx90sze@gmail.com, which is a real Gmail account that a bot registered and may never read again. Bulk signups at mainstream providers are common precisely because they sail past domain lists. If you're seeing them, fake signups: where they come from helps you figure out what's driving them.

So the only evidence left is the string itself.

The features that actually separate bots from people

Letter-pair (bigram) frequency

Human names and words are built from a fairly small set of common letter pairs: an, er, th, ri, ma. Random strings draw pairs uniformly, so most of their pairs are rare ones like qm, xk, jx. Measure what fraction of a local part's pairs are common, or better, compute the average log-probability of each pair from a corpus of real names and words. This is the strongest single feature.

Consonant runs

dpdcnf has six consonants in a row. Very few names do. Measure the longest run within each segment separately; if you strip dots first, john.smith turns into johnsmith and you've invented a four-consonant run (hnsm) that isn't there.

Digit placement

Where digits sit matters more than how many there are.

  • Embedded digits (et72wxknay, xk7qmz2p) are a strong signal. People put numbers at the end of a handle, not in the middle.
  • Trailing digits are normal for people, as long as they look like a year, a birthday or a short number: john1990, mike051290, priya99.
  • Trailing digits that aren't dates (6768, 477, 27281) are a moderate signal. A random generator doesn't produce valid dates more often than chance.

Rare letters and missing vowels

Several of j k q v w x z in one short string, or a long letter sequence with no vowels at all, both push the score up. They're weaker than the first three features and best used as tie-breakers.

What doesn't work well: entropy

Shannon entropy sounds like the right tool and isn't. Local parts are 6 to 15 characters, so the character entropy of xkqmzbtw and michaels is identical (3.0 bits each), because both are eight distinct letters. Entropy measures how many different symbols appear, not whether they form something pronounceable. Bigram statistics capture that; entropy doesn't.

A small scorer you can run

This is a self-contained JavaScript version of those features. It uses a hand-picked list of common letter pairs instead of a trained bigram model, so treat it as a starting point and tune it on your own signups.

// random-local-part.mjs  (run with: node random-local-part.mjs)
const VOWELS = /[aeiouy]/;
const RARE = new Set([...'jkqvwxz']);
const COMMON_BIGRAMS = new Set(
  ('th he in er an re on at en nd ti es or te of ed is it al ar st to nt ng se ha as ' +
   'ou io le ve co me de hi ri ro ic ne ea ra ce li ch ll be ma si om ur ca el ta la ' +
   'ns ge ly ei na ni no mi mo sa so ja jo ka ko ki ya yo ru ia ie ah ay ey').split(' ')
);

function looksLikeDate(d) {
  if (d.length === 2) return true;
  if (d.length === 4) return +d >= 1900 && +d <= 2099;
  if (d.length === 6) { // MMDDYY or DDMMYY
    const [a, b] = [+d.slice(0, 2), +d.slice(2, 4)];
    return (a >= 1 && a <= 12 && b >= 1 && b <= 31) || (a >= 1 && a <= 31 && b >= 1 && b <= 12);
  }
  return false;
}

export function randomness(local) {
  const base = local.toLowerCase().split('+')[0];
  const core = base.replace(/[._-]/g, '');
  const letters = core.replace(/[^a-z]/g, '');
  const reasons = [];
  let score = 0;

  const pairs = [];
  for (let i = 0; i < letters.length - 1; i++) pairs.push(letters.slice(i, i + 2));
  if (pairs.length >= 4) {
    const common = pairs.filter((p) => COMMON_BIGRAMS.has(p)).length / pairs.length;
    if (common < 0.25) { score += 35; reasons.push('few common letter pairs'); }
    else if (common < 0.4) { score += 15; reasons.push('some unusual letter pairs'); }
  }

  // Measure runs per segment so 'john.smith' doesn't become 'hnsm'.
  const segments = base.split(/[._\-\d]+/);
  const runs = segments.flatMap((s) => s.match(/[^aeiouy]+/g) ?? []).map((m) => m.length);
  const run = Math.max(0, ...runs);
  if (run >= 5) { score += 25; reasons.push(`consonant run of ${run}`); }
  else if (run === 4) { score += 10; reasons.push('consonant run of 4'); }

  if (/[a-z]\d+[a-z]/.test(core)) { score += 25; reasons.push('digits inside letters'); }
  const tail = core.match(/(\d+)$/)?.[1];
  if (tail && tail.length >= 3 && !looksLikeDate(tail)) {
    score += 20; reasons.push('non-date digit suffix');
  }

  const rare = [...letters].filter((c) => RARE.has(c)).length;
  if (rare >= 3) { score += 15; reasons.push('many rare letters'); }
  if (letters.length >= 6 && !VOWELS.test(letters)) { score += 20; reasons.push('no vowels'); }

  return { score: Math.min(100, score), reasons };
}

for (const local of ['xk7qmz2p', 'ccjxx90sze', 'john.smith', 'mike051290',
                     'lomigi6768', 'coolcat123', 'szczepan.grzegorz']) {
  console.log(local, randomness(local));
}

Running it gives:

Local part Score Why
xk7qmz2p 95 few common pairs, digits inside letters, rare letters, no vowels
ccjxx90sze 100 few common pairs, consonant run of 5, digits inside letters, rare letters
john.smith 0 nothing
mike051290 0 the suffix parses as a date
lomigi6768 20 non-date digit suffix only
coolcat123 20 non-date digit suffix only
szczepan.grzegorz 60 few common pairs, consonant run of 4, rare letters

A threshold around 50 catches the obvious bot output. It also exposes the two hard problems, which are the last three rows.

The hard cases

Pronounceable gibberish

lomigi6768 is the modern bot address. Generators learned to alternate consonants and vowels so the stem looks like a word, then append random digits. Letter-pair checks score it as human. The fix is a dictionary: if the alphabetic stem isn't a known first name or word, and the digits aren't a date, call it random. That's how pingutil handles it. Our email_signals combine a character-bigram model with an international first-name dataset, so lomigi6768 comes back is_randomized: true while priya99 and john1990 don't.

The trade-off is unavoidable: coolcat123 also gets flagged, because "coolcat" isn't a name and 123 isn't a date. Plenty of real people have handles like that. If your users skew toward gamers or teenagers, weigh this feature lower.

Real names that look random

szczepan.grzegorz is a Polish first name and surname. Polish uses consonant clusters like szcz and grz that English almost never does, so an English-trained bigram model sees noise. Pingutil scores it as randomized too (66 out of 100 in our test), which is a false positive. Names from other languages whose consonant clusters are rare in English can hit the same problem.

If you have a large user base in a specific country, train your bigram table on names from that country, or lower the weight of the bigram feature for addresses at that country's national providers (wp.pl, seznam.cz).

Random by design: privacy relays

Apple's Hide My Email, Firefox Relay and older DuckDuckGo private addresses (the DuckDuckGo help page gives avtzqdr@duck.com as an example of the previous randomized format) generate unguessable local parts on purpose. They're random and they belong to real, reachable people. Check for those hosts before applying any randomness rule. Email relay services are not disposable email has the host list and the code.

Corporate and university IDs

Some organizations assign addresses like a12345@ or jk4721@. A randomness check on a company or university domain deserves less weight than the same check on Gmail, since a bot can't easily mint addresses on someone else's corporate domain.

How to act on the score

A randomness score is one signal. Stack it with the others rather than letting it decide alone:

  • High score at a free webmail provider, plus a burst of similar signups: block. This is the classic pattern.
  • High score at a disposable domain: you'd block on the domain anyway.
  • Mid score, or high score on a company domain: allow with limits (smaller trial, no outbound sending) and review.
  • Relay host: ignore the score.

If you want these signals without maintaining a bigram model and a names list, pingutil returns them in fast mode:

curl -X POST https://pingutil.com/v1/lookup \
  -H "Authorization: Bearer $PINGUTIL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": "ccjxx90sze@gmail.com", "skip_enrichment": true }'

The response includes email_signals.is_randomized, email_signals.randomness_score (0 to 100) and email_signals.domain_looks_random, plus a verdict with reason: "randomized_local_part" when it fires. Field details are in the API reference. To fold the score into a broader decision alongside domain age, DNS and category, see building a simple email risk score from free signals.

Before you ship any version of this, pull a few hundred real signups from last month, score them, and read every address above your threshold by eye. You'll find your own szczepan within ten minutes, and you'll set the threshold far better than any blog post can.

Questions people ask

How can I tell if an email address was randomly generated?
Look at the part before the @. Machine-generated local parts tend to have letter pairs that rarely occur in names, long consonant runs, digits embedded between letters and digit suffixes that are not years or dates. Score those features together rather than relying on any single rule.
Can random email detection flag real users?
Yes. Names from languages with dense consonant clusters, such as Polish, can score as random, and so can privacy relay addresses that are random by design. Use a randomness score to limit or review an account rather than as the only reason to block it.
Is Shannon entropy a good way to detect random email addresses?
Not on its own. Local parts are short, so entropy barely separates a real name from gibberish of the same length. Letter-pair statistics, consonant runs and digit placement work better.