Every email validator can tell you an address is bad. That capability is a commodity — it is a syntax check and a DNS lookup, and it has been implemented thousands of times. The part that is actually worth building is naming the address the user meant to type, because that is the part that changes what happens next.
A rejection ends the interaction. "Did you mean diego@gmail.com?" continues it, with one click.
Transpositions are one edit, not two
The distance metric matters more than it sounds like it should. Plain Levenshtein distance counts insertions, deletions and substitutions. It does not know about transpositions, so swapping two adjacent characters costs two edits under Levenshtein: gmial to gmail reads as substitute i→a, substitute a→i.
Damerau-Levenshtein adds transposition as a single primitive operation. Under it, gmial to gmail is one edit.
This is not a rounding difference. Adjacent-character transposition is one of the most common typing errors — Damerau's original 1964 analysis of spelling errors identified it as one of four dominant error classes, and keyboard entry has not become less prone to it since. A validator using plain Levenshtein with a threshold of one edit misses every transposition. Raising the threshold to two to compensate is worse: at distance two you start matching domains that are genuinely different from each other, and confident wrong suggestions are more damaging than no suggestion.
So: transposition-aware metric, tight threshold.
Candidates come from a curated list, not the whole DNS
You cannot compute edit distance against every registered domain, and you would not want to. The comparison set is roughly 180 curated high-volume domains — the large consumer providers, the major regional providers, and the business hosts people actually type at signup forms.
Curation is what keeps false positives down. A small company domain that happens to sit one edit from gmail.com should never be "corrected" into it, and it will not be, because it is not a candidate. The list is the safety mechanism, not a limitation.
The Brazilian set is in there deliberately, which brings up the most useful repair in the whole engine.
The .com and .com.br confusion runs both ways
A Brazilian user typing quickly produces usuario@gmail.com.br — the muscle memory of a country that suffixes almost everything with .com.br. A user with a genuine .com.br address in a hurry produces contato@empresa.com, dropping the country code.
Both are typos. Both resolve to something. gmail.com.br is a registered domain; empresa.com may well belong to someone else entirely. Neither is caught by edit distance against the local part, and neither is caught by a DNS check, because in both cases DNS answers happily about a domain that exists.
So the repair is bidirectional and explicit: try adding .br to a .com address, try removing it from a .com.br address, and see whether the result is a known high-volume domain. This class does not exist at all in validators built for a single-market audience, which is a reasonable illustration of why locale matters in something as apparently universal as an email check.
TLD-only repairs
The third repair class is the top-level domain alone, with the second-level label already correct. outlook.con is outlook.com with one substitution in the TLD. gmail.co is gmail.com with a deletion.
These are worth handling separately from whole-domain distance because the confidence is different. When the second-level label matches a known provider exactly and only the TLD is off, the intent is unambiguous — nobody means to send mail to outlook.con. Whole-domain edit distance against a long candidate list carries more uncertainty, and the confidence score should reflect that rather than being flat across all repair classes.
Note that .co is a real TLD, delegated to Colombia. gmail.co exists, resolves, and publishes a null MX. The repair is not "that TLD is invalid" — it is "that specific second-level-plus-TLD combination is a known misspelling of a domain in our candidate list".
Confidence is part of the answer
{
"verdict": "undeliverable",
"reason": "typosquat_mx",
"suggestion": "user@gmail.com",
"confidence": 0.94,
"checked": { "syntax": true, "mx": true, "typo": true, "disposable": true },
"cached": true
}
0.94 is not decoration. It lets the caller decide how assertive to be in the UI. High confidence justifies pre-filling the corrected address with a visible undo. Lower confidence justifies asking rather than telling.
The reason tells you which stage produced the suggestion, which is diagnostically useful: likely_typo means edit distance matched a candidate, while typosquat_mx means the domain resolves fine but its mail exchanger is a known typo-collection host. The second is the more interesting finding, and it is invisible to any purely lexical approach.
Ordering: correction runs before DNS
This is the structural consequence and the reason the pipeline is arranged the way it is. Typo domains frequently resolve perfectly well — gmai.com has working MX records pointed at mail.h-email.net. If DNS runs first, it returns a healthy answer, the pipeline short-circuits with deliverable, and the typo stage never executes.
Correction before resolution. The suggestion is the product; the verdict is the commodity.
What the caller should do with it
if (result.verdict === "undeliverable" && result.suggestion) {
return offerCorrection(result.suggestion); // not an error state
}
Treat it as a question rather than a failure. Give the user a one-click accept and a real "keep what I typed" escape hatch, since confidence is a probability and an insistent user should win. The mechanics of wiring this into a registration handler, including timeouts and the fail-open path, are in Fail Open: Email Validation Must Never Block a Signup.
To see the suggestion behaviour without credentials, POST /demo/check resolves a frozen fixture corpus covering every verdict and reason. It is an evaluation endpoint and cannot validate arbitrary real addresses; live DNS checks run on a paid key, from $1/mo.