44 lines
1.8 KiB
Python
44 lines
1.8 KiB
Python
"""One way to write an email address down.
|
|
|
|
`users.email` had a plain unique index, so `Alice@example.com` and
|
|
`alice@example.com` were two accounts. Nobody types their address the same way
|
|
twice: a signup form gets `Alice@Example.com`, the password reset gets
|
|
`alice@example.com`, and the second finds nothing. Worse, an invitation or an
|
|
identity provider matching a user by address can create a duplicate of a person
|
|
who already exists.
|
|
|
|
Addresses are stored normalised — trimmed and lower-cased — and every lookup
|
|
normalises its input, so a row written before this still matches.
|
|
|
|
**Only the case is changed.** The local part of an address is technically
|
|
case-sensitive, and a handful of mail servers honour that; in practice none that
|
|
anyone signs up with does, and treating `Alice@` and `alice@` as different people
|
|
causes far more trouble than the theoretical correctness buys. This is the same
|
|
call the base application made.
|
|
|
|
Nothing else is touched — no dot-stripping, no plus-tag removal. `a.b+x@gmail.com`
|
|
routes to the same mailbox as `ab@gmail.com` at one provider and to a different
|
|
one elsewhere, so deciding they are the same person is a guess about somebody
|
|
else's mail server.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
|
|
def normalise(email: Optional[str]) -> Optional[str]:
|
|
"""The canonical form: trimmed and lower-cased. `None` stays `None`."""
|
|
if email is None:
|
|
return None
|
|
return email.strip().lower()
|
|
|
|
|
|
def matches(left: Optional[str], right: Optional[str]) -> bool:
|
|
"""Whether two addresses are the same person, for comparison in code.
|
|
|
|
Database comparisons should filter on the normalised column instead; this is
|
|
for the places holding two strings already.
|
|
"""
|
|
return normalise(left) == normalise(right)
|