> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnifence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hash an email

> The email normalisation rules, a reference implementation, and test vectors

Every member must turn an email into exactly the same hash, or the registry cannot match a user
across platforms. This page is the specification. Implement it once, test it against the vectors
below, and use the same function for checks and for reports.

## The rules

Apply these steps in order:

1. Remove whitespace from the start and end. Apply Unicode NFKC normalisation. Convert to lowercase.

2. If whitespace remains inside the value, it is not an email.

3. Split the value at the **last** `@` into a local part and a domain. If either part is empty, the
   value is not an email.

4. If the domain ends with one `.`, remove it. If the domain is then empty, the value is not an email.

5. If the domain is `googlemail.com`, change it to `gmail.com`.

6. If the domain is one of the providers below, remove everything from the first `+` in the local
   part:

   `gmail.com`, `outlook.com`, `hotmail.com`, `live.com`, `msn.com`, `icloud.com`, `me.com`,
   `mac.com`, `proton.me`, `protonmail.com`, `pm.me`, `fastmail.com`, `fastmail.fm`

7. If the domain is `gmail.com`, remove every `.` from the local part.

8. If the local part is now empty, the value is not an email.

9. Join the local part, `@` and the domain. Hash the result with SHA-256, and encode the digest as
   **lowercase hexadecimal** (64 characters).

Step 1 lowercases the whole address, including the local part. Almost no mail system treats the local
part as case-sensitive, and if the hash kept case, a user could avoid a report by changing the case
of one letter.

If a value is not an email after these steps, do not check it and do not report it.

### Why these rules and no others

The rules only join spellings that a mail provider delivers to the same inbox. Gmail ignores dots,
and the listed providers deliver `name+anything@` to `name@`. Other providers and company domains
can treat `+` and `.` as part of the address, so the rules leave them alone. Joining two different
inboxes would make two different people match, and a false match can refuse an innocent person a
service.

## Reference implementation

This JavaScript (Node.js 18 or later) implementation matches the specification and passes every test
vector on this page.

```javascript theme={null}
import { createHash } from 'node:crypto';

const PLUS_ADDRESSING_DOMAINS = new Set([
  'gmail.com',
  'outlook.com',
  'hotmail.com',
  'live.com',
  'msn.com',
  'icloud.com',
  'me.com',
  'mac.com',
  'proton.me',
  'protonmail.com',
  'pm.me',
  'fastmail.com',
  'fastmail.fm',
]);

export function normaliseEmail(raw) {
  const value = raw.trim().normalize('NFKC').toLowerCase();
  const at = value.lastIndexOf('@');
  if (at <= 0 || at === value.length - 1 || /\s/.test(value)) return null;

  let local = value.slice(0, at);
  let domain = value.slice(at + 1);
  if (domain.endsWith('.')) domain = domain.slice(0, -1);
  if (!domain) return null;
  if (domain === 'googlemail.com') domain = 'gmail.com';

  if (PLUS_ADDRESSING_DOMAINS.has(domain)) {
    const plus = local.indexOf('+');
    if (plus >= 0) local = local.slice(0, plus);
  }
  if (domain === 'gmail.com') local = local.replaceAll('.', '');
  return local ? `${local}@${domain}` : null;
}

export function registryDigest(email) {
  const normalised = normaliseEmail(email);
  return normalised && createHash('sha256').update(normalised).digest('hex');
}
```

Use the same function for every call:

```javascript theme={null}
const digest = registryDigest(signupEmail);
if (digest === null) {
  // Not an email address. Skip the registry for this value.
}
```

## Test vectors

Run your implementation against every row before you send a request. The SHA-256 column is the exact
value to send.

| Input | Normalised | SHA-256 |
| - | - | - |
| `Jane.Doe@Example.com` | `jane.doe@example.com` | `86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d` |
| `␠␠jane.doe@example.com␠␠` | `jane.doe@example.com` | `86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d` |
| `J.a.n.e.D.o.e+promo@gmail.com` | `janedoe@gmail.com` | `d6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69` |
| `janedoe@googlemail.com` | `janedoe@gmail.com` | `d6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69` |
| `jane+news@outlook.com` | `jane@outlook.com` | `5a0b70300b36ef454b660f484ad2a9353e2b4e199f623fe55789406eb07055f5` |
| `jane+news@example.com` | `jane+news@example.com` | `5a70ec8b461e4cac30f3a4b74cc1281e8ae664423f94429b738152ca14d81b77` |
| `jane-news@yahoo.com` | `jane-news@yahoo.com` | `394afddf1f3a96c4f742ab6ca3c470a1b8a5eb386eddcb27e9ca88ce2df71355` |
| `jane.doe@example.com.` | `jane.doe@example.com` | `86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d` |
| `ＪＡＮＥ@example.com` | `jane@example.com` | `8c87b489ce35cf2e2f39f80e282cb2e804932a56a213983eeeb428407d43b52d` |
| `+tag@gmail.com` | Not an email | None |
| `not-an-email` | Not an email | None |
| `@example.com` | Not an email | None |

In the input column, `␠` marks a space character.

## Common mistakes

<AccordionGroup>
  <Accordion title="Hashing the raw email">
    Hash the **normalised** value. `Jane.Doe@Example.com` and `jane.doe@example.com` must produce
    the same digest.
  </Accordion>

  <Accordion title="Uppercase or base64 output">
    Send lowercase hexadecimal. The API rejects any other format with `400 INVALID_REQUEST`.
  </Accordion>

  <Accordion title="Hashing twice">
    Hash the normalised email once. Do not hash the hexadecimal digest again before you send it.
  </Accordion>

  <Accordion title="Applying Gmail rules to every domain">
    Remove dots only for `gmail.com`, and remove `+tags` only for the listed providers.
    `first.last+news@company.com` stays exactly as it is.
  </Accordion>

  <Accordion title="Splitting at the first @">
    Split at the last `@`. A quoted local part can contain `@`.
  </Accordion>

  <Accordion title="Different code for checks and reports">
    Use one function for both. If your check and your report hash differently, your own reports
    never match your own checks, and nobody else's do either.
  </Accordion>
</AccordionGroup>

## Versioning

The rules on this page are version `1`. Every check response includes `normalisation_version`, so you
can confirm which rules the registry expects. If the rules change, the version number changes, and
members receive notice before the change.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.