emailnorm

Go library for canonicalizing email addresses to prevent duplicate accounts and registration spam.
Why normalization matters
Most web services store email addresses as raw, lowercased strings. Spammers exploit provider-specific routing quirks to register multiple accounts that route into one mailbox:
f.o.o.b.a.r@gmail.com
foobar+trial1@gmail.com
foobar+promo@googlemail.com
To a standard database query, these look like distinct users. To Google, every one of these delivers to foobar@gmail.com.
Naive normalizers often cause serious deliverability bugs by over-stripping:
- Stripping dots on Google Workspace or Fastmail can collide separate coworkers.
- Stripping plus signs on Yahoo, Tuta, GMX, Web.de, Naver, QQ Mail, or Brave aliases breaks deliverability because those services reject sub-addressing (
550 User not found).
- Merging Hotmail and Outlook causes collisions because Microsoft allows different people to own the same username across different domains.
emailnorm encodes verified routing rules for major email providers without collapsing distinct mailboxes.
Recommended storage pattern
When using emailnorm for user accounts, store both the raw email and the normalized email in separate database columns:
sql CREATE TABLE users ( id UUID PRIMARY KEY, email TEXT NOT NULL, -- Raw email as entered (e.g., Jane.Doe+receipts@googlemail.com) normalized_email TEXT NOT NULL UNIQUE -- Canonical email (e.g., janedoe@gmail.com) );
Why store both?
- Outbound delivery: Send all transactional and marketing emails to the raw address. Users often configure mail filters based on their aliases (such as +news or +invoices) and expect mail to arrive at the address they entered.
- User preference: Preserves the user's preferred casing and domain alias in the interface.
- Deduplication and security: Use
ormalized_email for unique constraints and login lookup queries. This prevents malicious or accidental duplicate registrations, multiple trial abuse, and referral fraud.
Supported providers
- Google (
gmail.com, googlemail.com): maps googlemail.com to gmail.com, strips dots, and removes +tag suffixes. Custom domains on Google Workspace preserve dots.
- Proton (
proton.me, protonmail.com, protonmail.ch, pm.me): maps alias domains to proton.me, strips plus tags, and preserves dots.
- Fastmail (over 100 domains): folds subdomain addressing (
alias@username.fastmail.com to username@fastmail.com).
- Apple iCloud (
icloud.com, me.com, mac.com): maps me.com and mac.com to icloud.com, and strips plus tags.
- Yandex (
yandex.ru, ya.ru, and country TLDs): maps regional domains to yandex.ru, strips plus tags, and converts hyphens to dots (first-last to first.last).
- Microsoft (
outlook.com, hotmail.com, live.com, msn.com): strips plus tags while keeping separate domains distinct.
- Mail.ru (
mail.ru, bk.ru, inbox.ru, list.ru, internet.ru): strips plus tags while keeping separate domains distinct.
- Zoho Mail and DuckDuckGo: strips plus tags and preserves dots.
- Yahoo, Tuta, GMX, Web.de, Naver, Daum, QQ, NetEase, and Brave: keeps literal addresses without plus stripping so outgoing mail does not bounce.
- Internationalized domains: normalizes Unicode to NFC and converts non-ASCII domain names to Punycode.
Installation
go get github.com/fumbledlol/emailnorm
Usage
Normalizing email addresses
package main
import (
"fmt"
"github.com/fumbledlol/emailnorm"
)
func main() {
// Gmail: removes dots and plus tags, unifies googlemail
fmt.Println(emailnorm.Normalize(" John.Doe+promo@googlemail.com "))
// Output: johndoe@gmail.com
// Proton: maps alias domains to proton.me, removes plus tag
fmt.Println(emailnorm.Normalize("user+secret@protonmail.ch"))
// Output: user@proton.me
// Yandex: maps regional domain, unifies hyphens and dots
fmt.Println(emailnorm.Normalize("first-last+tag@yandex.kz"))
// Output: first.last@yandex.ru
// Fastmail: folds subdomain addressing
fmt.Println(emailnorm.Normalize("shopping@myaccount.fastmail.com"))
// Output: myaccount@fastmail.com
// GMX, Naver, Brave: keeps plus signs to prevent delivery failure
fmt.Println(emailnorm.Normalize("user+tag@gmx.de"))
// Output: user+tag@gmx.de
// Internationalized domain names: converts to ASCII Punycode
fmt.Println(emailnorm.Normalize("contact@München.de"))
// Output: contact@xn--mnchen-3ya.de
}
Inspecting provider rules
provider := emailnorm.DetectProvider("user@gmx.de")
fmt.Println(provider) // GMX
fmt.Println(provider.SupportsPlusAddressing()) // false
fmt.Println(provider.StripsDots()) // false
gmail := emailnorm.DetectProvider("user@gmail.com")
fmt.Println(gmail.StripsDots()) // true
Normalization rules
| Provider family |
Strip dots |
Strip plus tag |
Domain folding |
Example input |
Normalized output |
| Gmail |
Yes |
Yes |
googlemail.com -> gmail.com |
f.o.o+bar@googlemail.com |
foo@gmail.com |
| Proton |
No |
Yes |
protonmail.*, pm.me -> proton.me |
user.name+ref@protonmail.com |
user.name@proton.me |
| Fastmail |
No |
Yes |
Subdomain fold (*@user.<dom> -> user@<dom>) |
newsletter@john.fastmail.com |
john@fastmail.com |
| iCloud |
No |
Yes |
me.com, mac.com -> icloud.com |
user+tag@me.com |
user@icloud.com |
| Yandex |
No (- -> .) |
Yes |
ya.ru, yandex.* -> yandex.ru |
first-last+tag@ya.ru |
first.last@yandex.ru |
| Outlook / Hotmail |
No |
Yes |
Distinct domains |
john.doe+trial@outlook.com |
john.doe@outlook.com |
| Mail.ru |
No |
Yes |
Distinct domains |
user+tag@mail.ru |
user@mail.ru |
| Zoho Mail |
No |
Yes |
Distinct domains |
user+tag@zoho.com |
user@zoho.com |
| DuckDuckGo |
No |
Yes |
Distinct domains |
user+tag@duck.com |
user@duck.com |
| Brave aliases |
No |
No |
Literal routing |
user+tag@bravealias.com |
user+tag@bravealias.com |
| Yahoo |
No |
No |
Literal routing |
user+tag@yahoo.com |
user+tag@yahoo.com |
| Tuta |
No |
No |
Literal routing |
user+tag@tutanota.com |
user+tag@tutanota.com |
| GMX / Web.de |
No |
No |
Literal routing |
user+tag@gmx.de |
user+tag@gmx.de |
| Naver / Daum |
No |
No |
Literal routing |
user+tag@naver.com |
user+tag@naver.com |
| QQ / NetEase |
No |
No |
Literal routing |
123456+tag@qq.com |
123456+tag@qq.com |
| Custom / Generic |
No |
Yes |
Literal domain |
dev+test@example.org |
dev@example.org |
Benchmarks
BenchmarkNormalize_Gmail-12 2113540 565.2 ns/op 112 B/op 4 allocs/op
BenchmarkNormalize_Proton-12 3206744 380.1 ns/op 56 B/op 2 allocs/op
BenchmarkNormalize_Fastmail-12 2263084 540.5 ns/op 72 B/op 3 allocs/op
BenchmarkNormalize_Yandex-12 3018289 407.6 ns/op 72 B/op 3 allocs/op
License
MIT