# Typo-proofing signup emails: suggest a fix, never silently correct

> A typo like gmial.com passes browser validation, then bounces or reaches a stranger. Suggest the likely fix, let the user decide, and never rewrite the address.

- Published: 2026-11-05
- Author: Koltrix Team
- Category: Guides
- Source: https://koltrix.com/blog/typo-proofing-signup-emails-suggest-never-silently-correct

To typo-proof a signup form, compare the domain the user typed against a short list of popular mail domains and, when it is one or two edits away from a match, show "Did you mean name@gmail.com?" with a one-click accept. Never rewrite the address for them: the near-miss might be real, and the user must stay in control of where their account email goes.

This matters because `user@gmial.com` is a perfectly well-formed address. It passes every syntax check, your welcome email goes out, and then one of three things happens: it bounces, it vanishes, or it lands with a stranger.

## Why doesn't the browser catch gmial.com?

Browser validation only checks format. MDN documents that `type="email"` validation is [format-only](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/email): it "doesn't necessarily mean the email address exists, but it is at least formatted correctly." A typo in a popular domain is still valid syntax, so `type="email"` waves it through. MDN also warns that the check can be bypassed from the browser, so it is never a security control.

Catching a typo needs your own logic. That is a small amount of code, and it is worth writing because the other options are worse.

## What actually happens to a mistyped address?

| What the typo produced | What you see | Cost to you |
|---|---|---|
| A domain that does not exist (`gmail.con`) | Hard bounce, or a send error before handoff | A wasted welcome email and a bounce on your record |
| A real domain, no such mailbox | Permanent bounce from that provider | Same, and the user never gets the link |
| A real domain that accepts any mail | Delivered, no bounce | Account details sent to a stranger; the real user is locked out |
| A domain run as a spam trap | Accepted silently | Reputation damage; see [spam traps explained](/blog/spam-traps-explained) |

Only the first two give you a signal, and the signal arrives after the user has already left. The third row is the dangerous one: you do not know it happened. For why bounces count against you, see [hard vs. soft bounces](/blog/hard-vs-soft-bounces).

## How do you decide it is a typo?

Use edit distance, the number of single-character insertions, deletions, substitutions or swaps needed to turn one string into another. `gmial.com` to `gmail.com` is a swap, so the distance is small. A reasonable rule has three parts:

1. **Only suggest against a short allowlist** of domains you are sure about, such as `gmail.com`, `yahoo.com`, `outlook.com`, `hotmail.com`, `icloud.com`, and perhaps `proton.me`.
2. **Skip the check if the domain already matches the list exactly.** An exact match is never a typo.
3. **Suggest only when the distance is 1 or 2**, and prefer the closest match. Compare the full domain and, separately, the top-level domain, because `gmail.con` and `gmail.cmo` are the same mistake in the ending.

Here is a compact version in JavaScript. It does not need a library:

```js
const COMMON = ["gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "icloud.com"];

function distance(a, b) {
  const d = Array.from({ length: a.length + 1 }, (_, i) => [i]);
  for (let j = 1; j <= b.length; j++) d[0][j] = j;
  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
    }
  }
  return d[a.length][b.length];
}

export function suggest(email) {
  const [local, domain] = email.trim().toLowerCase().split("@");
  if (!local || !domain || COMMON.includes(domain)) return null;
  let best = null;
  for (const c of COMMON) {
    const n = distance(domain, c);
    if (n <= 2 && (!best || n < best.n)) best = { n, domain: c };
  }
  return best ? `${local}@${best.domain}` : null;
}
```

Note that this is a plain Levenshtein distance, so a swap like `gmial` counts as 2 edits. That is why the cutoff is 2, not 1. The older [mailcheck](https://postmarkapp.com/blog/two-ways-to-reduce-bounced-welcome-emails) approach, which Postmark described years ago, works the same way: compare against a list of common domains and suggest the closest one.

## When should you not suggest anything?

A suggestion engine that nags is worse than none. Stay quiet in these cases:

- **The domain is already on your list.** No hint at all.
- **The domain belongs to a company.** `acme.io` is within two edits of other real domains. Never run the check against anything that is not a consumer mail provider.
- **The user already dismissed the hint.** Remember it for that form session and do not ask again.
- **The suggestion would change the local part.** Only offer fixes to the domain. A person's name cannot be "corrected."

If your list only holds consumer providers and your threshold is tight, false positives are rare. The ones that remain are why you ask instead of rewriting.

## Why not just correct it automatically?

Because you cannot tell a typo from a real address. Some near-miss domains exist, are run by real people, and receive real mail. If you silently change `name@gmai.com` to `name@gmail.com`, you may lock a real person out of their own account, or worse, send a verification link to someone else. A silent rewrite also breaks a basic expectation: the field shows what the user typed, and the system should not disagree without saying so.

Show the suggestion and let them choose. Good copy is friendly and specific:

> Did you mean **name@gmail.com**? [Use this] [Keep what I typed]

Avoid error-red styling, avoid blocking the submit button, and always let "keep what I typed" go through. The user knows their address better than your list does.

## Do you need to make people type the email twice?

No. A confirm-email field slows signup, and people copy and paste the same typo into both boxes. Postmark's guidance on [reducing bounced welcome emails](https://postmarkapp.com/blog/two-ways-to-reduce-bounced-welcome-emails) rejects it for the same reason and recommends two other things: a suggestion at entry, and a bounce notice after the fact. Both are cheap.

## What should you do after the user submits?

The suggestion is one layer of several. Pair it with these:

1. **Send a confirmation link and require it** before the account is active. This is the only check that proves the address reaches a person who controls it. See [signup email validation](/blog/signup-email-validation) for the full layered approach and [email confirmation links that survive security scanners](/blog/email-confirmation-links-that-survive-security-scanners).
2. **Check that the domain can receive mail** with an MX lookup. A domain with no mail records will never work, and you can tell the user right on the form.
3. **Listen for the bounce and tell the person in the app.** If a welcome email bounces, show a banner on their next visit: "We could not reach name@gmial.com. Update your address." A [bounce webhook](/blog/bounce-complaint-webhook-pipeline) makes this a few lines of code.
4. **Let the user fix the address from the waiting screen.** Offer "Wrong address? Fix it" on the "check your inbox" screen, so a typo is recoverable without a support ticket.

On Koltrix, a `message.bounced` [webhook](https://docs.koltrix.com/webhooks) fires when a message is rejected at handoff. A permanent rejection also adds the address to your suppression list, so a corrected address is the next send that actually goes out. Check the [sending documentation](https://docs.koltrix.com/sending-email) for the exact statuses before you build on them.

## What about addresses already in your database?

Do not bulk-rewrite old addresses. Run the same `suggest` function over existing contacts as a report, not a migration. For each hit, show the owner a one-click prompt next time they log in, or send a short plain message to the address on file asking them to confirm it. Anything you change without the owner's say-so is a guess.

## A checklist you can ship this week

- [ ] Keep `type="email"` and `autocomplete="email"` on the field.
- [ ] Trim whitespace and lowercase the domain before checking.
- [ ] Compare the domain to a short consumer-provider list with edit distance 1 or 2.
- [ ] Show "Did you mean...?" inline with accept and keep buttons.
- [ ] Never change the local part; never submit a corrected value without a click.
- [ ] Require the confirmation link before the account goes live.
- [ ] On a bounce, show an in-app prompt to fix the address.
- [ ] Log how often users accept the suggestion, so you can tune the list.

## Does it actually raise conversions?

Be skeptical of any number. Vendors quote large drop-off reductions, but the pages we read gave no method, and one even contradicted itself between its body and its FAQ. We found no independent data worth quoting. The honest argument is simpler: each accepted suggestion is one welcome email that arrives instead of bouncing, and one fewer person who thinks your product never replied. Measure the accept rate on your own form and judge from that.

## Bottom line

A typo-proofing prompt is about 30 lines of code: an allowlist of consumer mail domains, an edit-distance check, and an inline "Did you mean...?" with a one-click accept. Suggest, never silently correct. Then back it up with a confirmation link and a bounce notice so the typos you miss are still recoverable. If you want to see how sending, bounces and suppression fit together for a small SaaS, start with [bounce webhooks](/blog/bounce-complaint-webhook-pipeline) and the [signup validation layers](/blog/signup-email-validation).
