How to Catch Email Typos at Signup
by EmailGuard Engineering
Share this article
Enjoyed this article?
More notes on building products, infrastructure, and teams.
by EmailGuard Engineering
More notes on building products, infrastructure, and teams.
When the user types jane@gnail.com or jane@company.ccom, reject the raw string and show a suggested address they can accept in one click. EmailGuard can return suggested_email and suggested_domain while syntax_validation stays false. The suggestion is a corrected candidate from domain and TLD typo rules. It is not a mailbox ping and it is not permission to silently rewrite the row.
This is the typo companion to the email validation API checklist. Syntax still fails closed. You add a repair path so a one-character hostname miss does not look like a disposable block.
suggested_email / suggested_domain on GET /api/v1/emails/detect.Prerequisites: Server-side detect, email:detect on the key, and a signup form you control. Staging first.
notareal@gnail.com can pass a sloppy regex (something@something.something). A librarian-grade RFC parser still accepts many lookalike hostnames. The user meant Gmail. You stored a domain that will bounce forever.
A blocklist of gnail.com in the browser is easy to bypass and always incomplete. New TLD fat-fingers appear (ccom, con, comm). You want a rule catalog that can chain a domain typo and a TLD typo. In our matcher tests, bogpond.ccom is the kind of input that should land on bigpond.com, not on a generic "invalid email" with no hint.
That chain is first-hand EmailGuard behavior (domain_typo then tld_typo in the filter catalog). We do not publish the pattern list on the public API. You get the candidate strings, not the regexes.
From the public detect contract:
| Field | When it appears | What to do |
|---|---|---|
syntax_validation | Always on 2xx | If false, do not create the user on the raw email |
suggested_domain | Domain or TLD typo rule matched | Show "Did you mean @fixed.com?" |
suggested_email | Same, with local-part preserved | Prefer this in the UI |
matched_rules | Catalog or staff rules fired | id, label, and signal only. signal can be typo. Do not persist patterns |
syntax_validation remains false on an invalid TLD even when we suggest a fix. That is intentional. The typed string is still wrong. If you create the account on the raw value, you stored garbage. If you create it on the suggestion without asking, you may store an address they did not type (and did not own).
confidence is low when syntax failed. Do not read disposable or role as a finished verdict on that body. Details: confidence and fail-open.
1. User submits jane@gnail.com
2. Server calls detect
3. syntax_validation false, suggested_email jane@gmail.com
4. Return 422 with { code: EMAIL_TYPO, suggested: "jane@gmail.com" }
5. Form shows: "Did you mean jane@gmail.com?" [Use suggestion] [Edit]
6. If they accept, submit the suggested string as a new detect
7. Only then run disposable / role / public_domain policy
Re-detect after accept. The first response was about a broken hostname. The second is about gmail.com.
Do not:
suggested_email as verified ownership.Consumer Gmail vs Google Workspace still uses public_domain on the accepted address. A suggestion to gmail.com is still freemail.
type Detect = {
syntax_validation: boolean;
suggested_email?: string;
suggested_domain?: string;
disposable: boolean;
};
export function signupGate(d: Detect):
| { action: "fix_syntax"; suggested?: string }
| { action: "block_disposable" }
| { action: "continue" } {
if (!d.syntax_validation) {
return { action: "fix_syntax", suggested: d.suggested_email };
}
if (d.disposable) return { action: "block_disposable" };
return { action: "continue" };
}Timeouts stay on the fail-open path. A skipped check is not a typo.
Cache 2xx by normalized with a short TTL. Do not cache syntax-fail suggestions as if they were durable mailbox facts. See cache detect results.
We do not run SMTP to see whether jane@gmail.com exists. Kickbox-style "did you mean" on a deliverable mailbox is a different product. Our suggestion is hostname repair from catalog rules.
We do not correct every brand. If no rule fires, you get syntax false and no suggested_*. Show a plain "check the domain" message. Do not invent a guess in your app.
Homograph / mixed-script lookalikes are on the product roadmap as classification, not as this typo catalog. Do not stretch suggested_email to cover mixed-script lookalikes until that signal exists.
Bad: "Invalid email." Better: "Check the domain. Did you mean jane@gmail.com?"
Put the suggestion in the same error surface as syntax, not in a toast that disappears. Keyboard users need a button, not only a clickable domain.
If they dismiss the suggestion twice, stop nagging. Store email_typo_dismissed_at on the attempt and show the generic syntax error. Some people own odd domains. Your catalog will be wrong for them.
Internationalized domains: we validate TLD and IDNA on the syntax path. A suggestion is still an ASCII or IDNA hostname from the catalog. Do not punycode in the UI unless you also show the Unicode form they will recognize.
Checkout plus account create: run detect on each submit. A typo fixed on the account step can reappear if billing copies a stale field. Re-read the input. Do not reuse the first suggested_email after they edited the box.
CRM import: do not auto-apply suggestions in bulk. A 10,000-row "fix gnail" job will rewrite real hosts that collided with a rule. Queue a review for rows that have suggested_email and leave the raw value until a human or the subscriber confirms.
Mobile keyboards add .con and .cpm more than desktop. Log suggested_domain in your analytics (the domain, not the local-part) so you can see whether the catalog is earning its keep.
gnail.com / gmial.com style host: syntax false, suggestion present, no user created.email_check=skipped.email:detect: 401, fail closed, page ops. Not a typo.Wire these next to the disposable tests in block disposables at signup.
Kickbox and peers attach a suggestion to a verification result: they think the mailbox is close to a real one after SMTP or their own corpus. EmailGuard attaches a suggestion to a syntax failure: the hostname looks like a catalog fat-finger. You can use both products in one company. Do not copy their credit rules or risky buckets onto our 2xx body.
If you already show Kickbox's hint on a marketing form, do not show two competing suggestions. Pick the hostname repair when syntax_validation is false. Pick the verifier hint only when syntax passed and you are in a send-prep job.
Users mash the "use suggestion" button. That is two detect calls: the fail, then the accepted host. Cache the 2xx for the accepted normalized value so a third click does not spend a third unit. Do not cache the failed hostname as if it were a valid key. The cache guide is the quota counterpart.
matched_rules with signal: typo is for support, not for a public "we know your regex" UI. Patterns stay off the wire. If you open a ticket, send the rule id and the typed domain, not a guessed pattern.
Auto-replacing on blur. Users who own a weird but real domain will fight you. Offer, do not overwrite.
Only fixing gnail / gmial in JavaScript. You will ship the three jokes everyone copies and miss TLD doubles.
Showing a suggestion when syntax already passed. If they typed jane+news@gmail.com, that is plus addressing, not a typo.
Logging the raw misspelled address in a public Slack. Treat it as personal data, same as the rest of detect.
A check that the hostname looks like a known fat-finger of a real mail domain, then a candidate correction. EmailGuard exposes that as suggested_email / suggested_domain when a domain or TLD typo rule matches.
Block the raw invalid address. Warn with a one-click accept for the suggestion. After they accept, apply disposable and policy flags on the new detect.
No. It means the hostname looks like a catalog repair. Prove ownership with a confirmation email if you need the mailbox.
Because they have not submitted a valid addr-spec yet. The suggestion is a candidate, not the request email.
Yes. Use the syntax checker and detect in staging. Create a key on a free plan.
suggested_email.