Files
picpeak/backend/src/utils/emailNormalization.js
T
Paul Nothaft 075b45f020 fix(email): preserve dots + subaddresses across all normalization sites (#574)
Closes #574.

Reporter (@blazmaric) identified the root cause cleanly:
express-validator's `.normalizeEmail()` applies provider-specific
canonicalization by default — Gmail dot-stripping, +tag stripping,
googlemail → gmail folding, etc. That's wrong for identity: PicPeak
uses email as a login identifier, so `john.doe@gmail.com` getting
silently stored as `johndoe@gmail.com` means the user can't log in
with the address they were invited with.

The bug existed at 17 call sites across the codebase (auth, admin user
create/update, customer create/update, event create/update on three
different routes, customer login, feedback submission). All of them
are identity-bearing — none had a legitimate reason to strip dots
for deduplication.

Fix: introduce one shared options object in `utils/emailNormalization`
disabling every provider-specific normalization
(gmail_remove_dots, gmail_remove_subaddress,
gmail_convert_googlemaildotcom, outlookdotcom_remove_subaddress,
yahoo_remove_subaddress, icloud_remove_subaddress). The only default
left enabled is `all_lowercase`, which is safe — local-parts are
case-insensitive in practice on every major provider, and lowercasing
keeps login lookup consistent.

Every call site updated to pass the shared options. 7 unit tests pin
the preserved-dots, preserved-subaddress, preserved-googlemail-domain,
and still-lowercase behaviours so a future refactor can't silently
regress.

## Migration note

Existing accounts whose emails were already stripped before this fix
remain with the stripped form in the DB. The fix takes effect for new
invitations going forward. If an admin re-invites an existing user
with the un-stripped address, that would create a duplicate account —
out of scope here; if it becomes a real problem we can add a
backward-compat login fallback (try lookup with dot-stripped form too)
as a separate change.
2026-05-29 11:42:25 +02:00

37 lines
1.7 KiB
JavaScript

/**
* Identity-preserving email normalization options for express-validator.
*
* express-validator's `.normalizeEmail()` defaults to provider-specific
* "canonicalization" — Gmail dot-stripping, +tag stripping, googlemail
* → gmail domain folding, etc. That's appropriate for *deduplication*
* (treating the same mailbox as the same identity for, say, a free-tier
* abuse check) but it's wrong for *identity* (the user expects to log
* in with the exact address they were invited with).
*
* PicPeak uses email as a login identifier across admin users, customer
* accounts, and customer-portal invitations. Stripping dots/+tags
* silently means an invitation sent to `john.doe@gmail.com` is stored
* as `johndoe@gmail.com`, and the user then can't log in with the
* address they were given — see #574.
*
* Use this options object on every `.normalizeEmail()` call. The only
* default left enabled is `all_lowercase` (true by default), which is
* safe — local-parts are case-insensitive in practice on every major
* provider, and lowercasing keeps login lookup consistent.
*/
// NOT Object.frozen — validator.js's `merge()` mutates the options
// object to add its own defaults (notably `all_lowercase: true`).
// Freezing would crash at the first call site. The mutation is
// idempotent (validator only adds keys it has defaults for, not ones
// we already set), so subsequent calls reuse the same enriched object.
const IDENTITY_PRESERVING_NORMALIZE_EMAIL = {
gmail_remove_dots: false,
gmail_remove_subaddress: false,
gmail_convert_googlemaildotcom: false,
outlookdotcom_remove_subaddress: false,
yahoo_remove_subaddress: false,
icloud_remove_subaddress: false,
};
module.exports = { IDENTITY_PRESERVING_NORMALIZE_EMAIL };