🍱 Lunchbox Hands

email

Email Normalization: Gmail Dots, Plus Tags, and the Rules That Only Apply to One Provider

Why [email protected] and [email protected] are one inbox but [email protected] and [email protected] are two, what RFC 5321 says about case, and why stripping +tags for every domain quietly merges different people.

Two rows in your users table: [email protected] and [email protected]. Same person, same inbox, two accounts, two trial periods. The obvious fix is to canonicalize addresses before you store them — strip the dots, drop anything after a +, lowercase the lot. The catch is that every one of those transformations is a property of one specific mail provider, not of email, and the standard explicitly forbids you from assuming otherwise. Get it wrong in the safe direction and you keep some duplicates. Get it wrong in the other direction and you merge two strangers into one account.

What the standard actually says

RFC 5321 is unambiguous about who owns the meaning of the part before the @ (§2.3.11):

The local-part MUST be interpreted and assigned semantics only by the host specified in the domain part of the address.

And about case (§2.4):

The local-part of a mailbox MUST BE treated as case sensitive.

Mailbox domains follow normal DNS rules and are hence not case sensitive.

So [email protected] and [email protected] are, by the letter of RFC 5321 §2.4, two different mailboxes, while [email protected] and [email protected] are one. The same section immediately concedes reality — “exploiting the case sensitivity of mailbox local-parts impedes interoperability and is discouraged” — which is why lowercasing the local part is usually safe and still, strictly, a guess about someone else’s mail server.

The +tag convention has a spec too, and it says the same thing about ownership. RFC 5233 defines subaddressing as “the practice of augmenting the local-part of an address with some ‘detail’ information,” and states plainly that “the encoding of detailed addresses are site and/or implementation specific.” There is no standard separator. + is a convention, not a rule.

Gmail’s two rules — and the exception nobody codes for

Gmail is the reason everyone writes a normalizer: dots in the local part are ignored, and everything after a + is ignored. Google documents the dots rule directly; the plus rule is long-standing behavior that Google’s own help pages treat as a given rather than spell out, which is itself a hint about how much of this is convention rather than specification.

The part that breaks implementations is the exception Google spells out:

If you use Gmail through work, school, or other organization (like yourdomain.com or yourschool.edu), dots do change your address.

So the dots rule applies to @gmail.com (and its alias @googlemail.com) — not to the millions of custom domains hosted by Google Workspace, which look identical from the outside. You cannot tell from [email protected] whether the MX is Google, and even if you could, the dots still matter there. Any normalizer that strips dots based on “the MX points at Google” is wrong.

The + is not guaranteed to be a tag

Microsoft’s implementation shows why stripping + for arbitrary domains is unsafe. In Exchange Online, plus addressing is enabled by default, but resolution order matters:

When Exchange receives an email for an address that contains a +, it will try to resolve the full email address (for example, [email protected]) to a known mailbox. If the first resolution attempt fails, Exchange does a second attempt to resolve the email address without the plus sign and tag.

A mailbox whose real address contains a plus sign wins. And an administrator can turn the whole feature off org-wide with Set-OrganizationConfig -DisablePlusAddressInRecipients $true, at which point [email protected] is simply undeliverable — while your database has happily filed it under [email protected] and is sending password resets to a colleague.

Yahoo is a third shape again. Its disposable addresses use a hyphen — “They consist of 2 parts, a ‘nickname’ and a ‘keyword’ ([email protected])” — and, crucially, they must be created in settings and the feature is tied to Yahoo Mail Plus. It is not a catch-all. That makes hyphen-stripping the most destructive normalization of all: [email protected] is an ordinary address belonging to an ordinary person, and collapsing it to [email protected] invents a different human being.

ProviderDots+tagOther
@gmail.com, @googlemail.comIgnoredIgnoredThe two domains are equivalent
Google Workspace custom domainSignificantIgnoredLooks identical from outside
Outlook.com / Exchange OnlineSignificantIgnored by default, unless a real mailbox matches, and admins can disable it—
YahooSignificantSignificantbase-keyword@ only for addresses created in settings
iCloudSignificantIgnored—
Fastmail and most self-hosted serversSignificantUsually ignored, configurableSubdomain addressing is common
Everything elseUnknownUnknownRFC 5321: only that host knows

The Gmail, Workspace, Exchange Online and Yahoo rows come from those providers’ own documentation, linked above. The rest reflect the assumptions baked into the most widely used normalizer, validator.js, which is the next section — and “unknown” is the honest entry for every domain that isn’t on this list.

What the libraries do (they don’t agree)

Django’s BaseUserManager.normalize_email is the conservative end. Its entire docstring is “Normalize the email address by lowercasing the domain part of it” — it lowercases the domain and touches nothing else, exactly matching RFC 5321.

validator.js’s normalizeEmail is the aggressive end, and notably it is per provider: gmail_remove_dots, gmail_remove_subaddress, gmail_convert_googlemaildotcom, outlookdotcom_remove_subaddress, icloud_remove_subaddress and yahoo_remove_subaddress all default to true, with Yahoo’s removing everything after a -. Its own documentation flags the risk of all_lowercase: true, which “may violate RFC 5321” for providers that treat the local part as case-sensitive.

Neither is wrong. They’re answering different questions — “what is this person’s canonical login?” versus “how do I not lose mail?” — and you have to pick which question you’re asking before you pick a function.

The failure mode is not duplicate rows

In 2018, James Fisher documented the scam that falls out of Gmail’s dot rule: an attacker signed up for Netflix as [email protected] with a throwaway card. Netflix treated the dotted address as a new account; Gmail delivered every notification to the real owner of [email protected], who received genuine Netflix billing emails about an account he didn’t create and was one click away from attaching his own card to a stranger’s subscription.

The deeper version of the bug is inconsistent normalization inside one system: signup canonicalizes, password reset doesn’t (or vice versa). Then an attacker registers a variant that collides with a victim’s canonical key in one code path and not another, and verification or reset mail lands in the wrong inbox. The rule that prevents it:

  1. Store the address exactly as the user typed it. That is the only string you’re allowed to send mail to.
  2. Store a separate canonical key for uniqueness and lookup — and compute it in exactly one function, used by signup, login, reset, and invite alike.
  3. Never deliver to the canonical form. Send to what they typed; the mail server will do its own normalization far better than you can.
  4. Default to lowercase-the-domain only. Add provider-specific rules deliberately, per domain, knowing what you’d break.
  5. Treat aggressive normalization as a business decision, not a correctness one. “One free trial per human” is worth some false merges; “one account per human” is usually not.

What our normalizer does, exactly

The Email Normalizer is built for the list-deduping job, not the auth job, and it is aggressive in one specific way. Run against real input, here is its exact behavior:

[email protected]     -> [email protected]
[email protected]     -> [email protected]
[email protected]             -> [email protected]
[email protected] -> [email protected]
[email protected]          -> [email protected]
[email protected]        -> [email protected]
  • Dots are removed only for gmail.com/googlemail.com, and googlemail.com is folded to gmail.com. Outlook keeps its dots, as it should.
  • +tags are stripped for every domain. That’s the deliberate aggressive choice — for deduping a mailing list it’s almost always what you want, and it is exactly the rule you should not copy into a login system for the Exchange reason above.
  • Hyphens are never touched, for the jean-luc reason. Yahoo subaddresses won’t dedupe, and that’s the safer failure.
  • Everything is lowercased, including the local part.
  • Internationalized domains are compared as typed: plain@例え.jp and [email protected] are the same mailbox but won’t dedupe here. If you’re mixing both spellings, convert first with the Punycode converter — why some URLs start with xn-- explains the encoding.

Normalizing tells you nothing about whether an address can receive mail, which is a separate check with a separate tool — the Email Checker does syntax plus a live MX lookup, and valid syntax doesn’t mean deliverable covers why even that isn’t a guarantee.