Email validation at signup: what to check, and what not to
Validate signup addresses with syntax checks, domain and MX lookups and typo suggestions, but skip SMTP probing. A layered approach with code.

On this page(10 sections)
Every bad address that gets past your signup form costs you twice: once when the welcome email bounces, and again when your bounce rate and reputation take the hit. Good validation catches most mistakes in milliseconds.
Overzealous validation rejects real customers. The trick is knowing which checks belong in which layer.
The layers
Think of validation as layers of increasing cost and certainty:
| Layer | Where | Cost | Catches |
|---|---|---|---|
| 1. Syntax | Browser and server | Free | Malformed input, obvious garbage |
| 2. Typo suggestion | Browser or server | Free | Misspelled popular domains |
| 3. Domain and MX lookup | Server | One DNS query | Nonexistent domains, domains that cannot receive mail |
| 4. Risk checks | Server | Lookup in a list | Disposable addresses, if you care |
| 5. Confirmation email | Server, then user | One email | Everything else: the address works and belongs to this person |
The first four are fast and cheap. Only the fifth proves the address works and that the person who typed it controls it.
Layer 1: syntax, but not too strict
The formal grammar for email addresses in RFC 5322 allows far more than most people expect, including quoted local parts and unusual characters. Trying to implement the full grammar with a regular expression produces unreadable patterns that still get edge cases wrong. Trying to be strict produces something worse: rejecting valid addresses.
A practical server-side check:
import re
# Deliberately permissive: one @, something before it, a dotted domain after it.
BASIC = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def normalize(addr: str) -> str:
addr = addr.strip()
local, _, domain = addr.rpartition("@")
return f"{local}@{domain.lower()}"
def looks_like_email(addr: str) -> bool:
return len(addr) <= 254 and bool(BASIC.match(addr))
Notes on the details:
- Trim whitespace. Pasted addresses often include spaces.
- Lowercase only the domain. Domains are case-insensitive. Local parts are technically case-sensitive under the standards, even though nearly every provider treats them case-insensitively. Lowercasing the local part for comparison is common and usually fine; just store what the user typed if you are being careful.
- Length. A widely used practical limit is 254 characters for the whole address, derived from SMTP path length limits.
- Allow plus signs.
[email protected]is valid and widely used. Forms that reject+frustrate users and gain nothing. - Allow international addresses where you can. Addresses with non-ASCII characters exist and are standardized (SMTPUTF8), though support across sending systems varies.
Layer 2: suggest corrections for typos
A large share of bad signups are simple typos in popular domains: a missing letter, swapped letters, a wrong top-level domain. Compare the domain against a list of common mail domains and suggest a correction when it is one or two edits away.
from difflib import get_close_matches
COMMON = ["gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "icloud.com"]
def suggest(domain: str):
if domain in COMMON:
return None
match = get_close_matches(domain, COMMON, n=1, cutoff=0.85)
return match[0] if match else None
Show the suggestion ("Did you mean [email protected]?") and let the user accept or ignore it. Never silently rewrite the address; some near-miss domains are real.
Typo prevention matters beyond bounces. Some typo domains accept mail, which means your welcome email and account details go to a stranger, and some are operated as spam traps.
Layer 3: domain and MX lookup
Check that the domain can receive mail:
- Look up MX records for the domain.
- If there are none, RFC 5321 allows falling back to the domain's A or AAAA record as an implicit MX.
- If the domain publishes a null MX (
MX 0 ., RFC 7505), it explicitly does not accept mail. Reject. - If the domain does not exist (NXDOMAIN), reject.
import dns.resolver
def domain_accepts_mail(domain: str) -> bool:
try:
mx = dns.resolver.resolve(domain, "MX", lifetime=3)
hosts = [r.exchange.to_text() for r in mx]
return not (len(hosts) == 1 and hosts[0] == ".")
except dns.resolver.NXDOMAIN:
return False
except dns.resolver.NoAnswer:
try:
dns.resolver.resolve(domain, "A", lifetime=3)
return True
except Exception:
return False
except Exception:
return True # DNS trouble: do not block the user, let confirmation decide
Fail open on DNS errors. A slow resolver should not cost you a signup.
Layer 4: risk checks, chosen deliberately
Disposable address services provide temporary inboxes. Whether to block them is a product decision: they are popular with people trying a product without commitment, and also with abusers creating many accounts. Lists of disposable domains exist, but they go stale quickly. If you block, make the error message clear and offer an alternative.
What not to do: SMTP probing
Some services "verify" addresses by connecting to the recipient's mail server and issuing RCPT TO without sending a message, to see whether the server accepts the address. Avoid doing this yourself at signup:
- Many large providers accept all recipients at the SMTP stage, or answer ambiguously, so results are unreliable.
- Repeated probing from your IPs looks like directory harvesting and can hurt your reputation or get you blocked.
- It adds seconds of latency to a form submission.
A confirmation email gives a definitive answer without any of these problems.
Layer 5: confirm the address
For accounts and subscriptions, send a confirmation email with a single-use link or code. Until the address is confirmed:
- Do not send marketing or digests.
- Limit what the account can do, if appropriate.
- Expire unconfirmed signups after a reasonable period.
Confirmation proves the address works, proves the person controls it, and eliminates typos and maliciously entered addresses at once. Make the email fast and simple, because a slow confirmation step loses signups.
Protect the form itself
Signup forms are abused by bots that submit victims' addresses, sometimes in large volumes, to flood inboxes with confirmation emails. Rate-limit by IP and by address, add bot protection on high-traffic forms, and never send more than one confirmation email to the same address within a short window.
Checklist
- Trim input and use a permissive syntax check.
- Allow plus addressing and long top-level domains.
- Suggest corrections for typos in common domains; never auto-correct.
- Check MX records, handle null MX and NXDOMAIN, and fail open on DNS errors.
- Decide deliberately about disposable domains.
- Do not perform SMTP probing at signup.
- Confirm addresses before sending ongoing mail.
- Rate-limit the form against bot abuse.
Bottom line
Layered validation keeps garbage out without rejecting real users: permissive syntax checks, typo suggestions and DNS lookups cost almost nothing and catch most mistakes. Confirmation emails catch the rest with certainty. Skip SMTP probing, fail open on DNS problems, and protect the form from bots, and your bounce rate and trap exposure will both stay low.
Start with Koltrix
Your domain, one inbox, and an API that sends.
A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.


