auth/email and auth/username validate and normalize user input into a
canonical form before you store it. See the error reference and the
runnable email and username
examples.
emailMod, err := email.New(auth)
if err != nil {
log.Fatal(err)
}
// Close is an optional no-op kept for backward compatibility — the module runs
// no background goroutine, so no cleanup is required.Always call ValidateAndNormalize instead of validating and normalizing
separately. It lowercases, trims whitespace, and validates in a single call —
ensuring the value you store is always in canonical form:
normalized, err := emailMod.ValidateAndNormalize(req.Email)
switch {
case errors.Is(err, email.ErrInvalidEmail):
// 400 — tell the user exactly what failed (message is descriptive)
c.JSON(400, map[string]string{"error": errors.Unwrap(err).Error()})
return
case err != nil:
// 500 — unexpected error
}
// Store normalized — always lowercase, trimmed.
db.StoreUser(normalized, ...)Validation rules (RFC 5321 / RFC 5322):
| Rule | Requirement |
|---|---|
| Total length | 1 – 254 characters |
| Format | One @ separating a non-empty local part and domain |
| Local part | ≤ 64 characters |
| Domain | At least one dot; no leading, trailing, or consecutive dots |
| Domain labels | 1 – 63 characters each |
Always normalize before storing and before querying. This ensures consistent lookup —
User@EXAMPLE.COManduser@example.comare the same address.
Note
Internationalised domains (IDN) are supported. Unicode domains are converted to ASCII punycode during normalisation:
user@münchen.de → user@xn--mnchen-3ya.de
user@例え.jp → user@xn--r8jz45g.jp
Store the ASCII form so DNS lookups and database comparisons are deterministic.
VerifyDomain performs an optional DNS MX lookup to confirm the domain is
configured to receive email. Call it after ValidateAndNormalize when you want
to reject obviously fake domains before sending a verification email.
Results are cached per domain (default 5 minutes) and DNS lookups for the same
domain are deduplicated via singleflight — safe for high-concurrency workloads.
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
err = emailMod.VerifyDomain(ctx, normalized)
switch {
case errors.Is(err, email.ErrDomainNoMX):
// 400 — domain exists but cannot receive email
c.JSON(400, map[string]string{"error": "email domain cannot receive messages"})
return
case errors.Is(err, email.ErrDomainUnresolvable):
// DNS lookup failed — do NOT block the user; log and continue
log.Warn("DNS check unavailable: %v", err)
}
ErrDomainUnresolvableis a soft failure. DNS can be temporarily unavailable due to network issues unrelated to the user's input. Never block a registration on this error — log it and proceed.
userMod, err := username.New(auth)
if err != nil {
log.Fatal(err)
}Always call ValidateAndNormalize — it lowercases, trims whitespace, and
validates in a single call, ensuring the value you store is always in canonical
form:
normalized, err := userMod.ValidateAndNormalize(req.Username)
if err != nil {
// errors.Unwrap(err).Error() contains the specific rule that failed.
c.JSON(400, map[string]string{"error": errors.Unwrap(err).Error()})
return
}
db.StoreUser(normalized, ...) // always lowercase, trimmed, validatedValidation rules:
| Rule | Requirement |
|---|---|
| Length | 3 – 32 characters (fixed) |
| Allowed characters | [a-z0-9_-] only |
| First character | Letter or digit (not _ or -) |
| Last character | Letter or digit (not _ or -) |
| Consecutive specials | __, --, _-, -_ are rejected |
| Reserved names | Built-in blocklist (fixed) |
Always normalize before storing and before querying.
Alice123andalice123are the same username — store only the canonical (normalized) form.