Olympus Docs
CookbookAuth flows

Email aliases and plus-addressing

How to handle `alice+test@example.com` consistently

A common user pattern: signing up multiple times with alice+demo@example.com, alice+test@example.com, etc. The user thinks of these as "the same person." Your system probably treats them as different.

You have to pick: are aliases the same person or different?

What + addressing is

RFC 5233: characters after + in the local part are ignored by the MTA for delivery. So:

  • alice@example.com → delivered to alice's mailbox.
  • alice+demo@example.com → delivered to alice's mailbox.
  • alice+foo@example.com → delivered to alice's mailbox.

Google, FastMail, ProtonMail, most providers support this. Outlook/Hotmail historically didn't (using - instead). Some corporate systems don't support it.

The decision

Option A: Treat aliases as same identity

Normalize at signup:

function normalizeEmail(email: string): string {
  const [local, domain] = email.split("@");
  const cleanLocal = local.split("+")[0];
  return `${cleanLocal}@${domain.toLowerCase()}`;
}

alice+demo@example.com and alice@example.com both become alice@example.com. Same identity.

Pros: prevents abuse (one person creating 100 accounts). Cons: legitimate uses broken (testing, alias delegation).

Option B: Treat aliases as separate identities

Don't normalize. Each is unique.

Pros: simpler, RFC-compliant. Cons: abusable.

Option C: Hybrid

Don't normalize, but flag suspicious patterns:

-- Find users with shared root
SELECT
  REGEXP_REPLACE(traits->>'email', '\+[^@]*', '') AS root_email,
  COUNT(*) AS account_count
FROM identities
GROUP BY root_email
HAVING COUNT(*) > 3
ORDER BY account_count DESC;

Investigate manually. Block if abusive.

Best practice

Most B2C: Option B (treat as separate). Aliases are legitimate. B2B / fraud-sensitive: Option A or C.

Document your choice in your terms of service so users know.

Gmail dots

Google ignores dots in local part:

  • alice@gmail.com
  • a.lice@gmail.com
  • al.ice@gmail.com

All same Gmail mailbox.

To normalize:

function normalizeGmail(email: string) {
  const [local, domain] = email.split("@");
  if (domain === "gmail.com" || domain === "googlemail.com") {
    return `${local.replace(/\./g, "")}@gmail.com`;
  }
  return email;
}

But this breaks for organizations using Workspace with .com aliases. Don't apply blindly.

Disposable email domains

Beyond aliasing, there are explicit disposable services: Mailinator, GuerrillaMail, etc. Often abused.

Allow-list / block-list approach:

const blocked = new Set([
  "mailinator.com",
  "guerrillamail.com",
  "10minutemail.com",
  "tempmail.com",
  // ... ~5000 known disposable domains
]);

function isDisposable(email: string) {
  const domain = email.split("@")[1].toLowerCase();
  return blocked.has(domain);
}

For an up-to-date list: https://github.com/ivolo/disposable-email-domains

Pre-registration hook:

export async function POST(req: Request) {
  const { traits } = await req.json();
  if (isDisposable(traits.email)) {
    return Response.json({
      reject: true,
      error: "disposable_email",
      message: "Please use a non-disposable email address.",
    });
  }
  return Response.json({ ok: true });
}

Subdomain games

alice@subdomain.example.com is technically a different domain from alice@example.com. But for a service letting users register, treat them as related?

Generally: no. Subdomains can be totally separate orgs.

International (IDN) emails

Punycode and emoji emails exist:

  • alice@münchen.de
  • 📧@🚀.io

Most apps don't handle these well. Postel's Law: be lenient in what you accept, strict in what you emit.

Recommend normalizing IDN domains via punycode for storage:

import { toASCII } from "punycode";
const normalized = toASCII("münchen.de"); // "xn--mnchen-3ya.de"

Store the punycode form; display the unicode form.

Case sensitivity

Per RFC, local-parts are case-sensitive (Alice@example.com != alice@example.com). In practice, virtually every MTA is case-insensitive on the local-part.

Olympus normalizes to lowercase on registration:

const email = traits.email.toLowerCase();

If you don't, Alice@example.com and alice@example.com create separate identities. Annoying for users.

Audit traces

When normalizing, store BOTH the original and the normalized:

"email": "Alice+demo@Example.com",
"normalized_email": "alice@example.com"

Search/uniqueness on normalized. Display the original.

On this page