Plus addressing and Gmail dots: how to deduplicate sign-ups

Plus addressing sends jane+news@acme.io to jane@acme.io, and Gmail ignores dots. How to deduplicate sign-ups with a key, without blocking anyone.

  • #signup
  • #fraud
  • #engineering
  • #api
Three sign-up addresses, Jane+Trial@Gmail.com, j.a.n.e@googlemail.com and jane+2@gmail.com, with arrows merging into one dedupe key, jane@gmail.com. // 3 sign-ups Jane+Trial@Gmail.com j.a.n.e@googlemail.com jane+2@gmail.com dedupe key jane@gmail.com one inbox, one trial // send mail to what they typed; check uniqueness on the key

Plus addressing (also called subaddressing) lets anyone add a tag after a + in their address: mail to jane+news@acme.io is delivered to jane@acme.io. Consumer Gmail goes further and ignores dots, and treats googlemail.com as gmail.com, so Jane+Trial@Gmail.com, j.a.n.e@googlemail.com and jane+2@gmail.com all land in one inbox. To deduplicate sign-ups, don’t block these addresses: build a normalized key, put a unique index on it, and keep the original address for sending mail.

What plus addressing is

An email address has a local part before the @ and a domain after it. With plus addressing, the local part splits in two: the mailbox name, and a free-form tag after the +. Mail servers that support it deliver on the mailbox name and ignore the tag for routing. RFC 5233, the subaddress extension for the Sieve mail-filtering language, describes this user+detail convention.

Address Delivered to Tag
jane+news@acme.io jane@acme.io news
jane+invoices@gmail.com jane@gmail.com invoices
jane.doe+trial-2@outlook.com jane.doe@outlook.com trial-2

The tag isn’t stripped on arrival. The message still shows the full address it was sent to, which is the whole point: the recipient can sort, filter and search on it.

Who supports it

Support depends on the mail host, not on the person. These are the well-known cases:

Mail host Plus addressing Notes
Gmail (gmail.com, googlemail.com) Yes Also ignores dots in the local part
Google Workspace (company domain) Yes Dots are part of the address
Outlook.com Yes
Microsoft 365 (company domain) Yes An organization setting; admins can turn it off
Fastmail, Proton Mail Yes
Self-hosted servers such as Postfix Configurable The admin picks the separator; + is common, - also appears

Because support varies, a tag is something the person chooses to use with their own inbox. It’s not something you should add or remove on their behalf when you send mail.

Gmail dots: one inbox, many spellings

Consumer Gmail ignores dots in the local part. jane.doe@gmail.com, janedoe@gmail.com and j.a.n.e.d.o.e@gmail.com are one mailbox, and googlemail.com is the same service under another name. Add plus addressing and case (local parts are technically case-sensitive under RFC 5321, but no major provider treats them that way), and one Gmail inbox has an effectively unlimited number of spellings.

This rule belongs to consumer Gmail only. On a Google Workspace domain, jane.doe@acme.io and janedoe@acme.io are different addresses: the admin might have created both, for two different people, or only one of them. Google hosts both kinds of mailbox, but they are different products with different rules, as Google Workspace vs Gmail in DNS explains. The same goes for Outlook.com, Microsoft 365 and every other host: dots are characters like any other.

So dot folding is safe for exactly two domains, gmail.com and googlemail.com. Apply it anywhere else and you will eventually merge two real people into one account.

Why people use tags, and why you shouldn’t block them

Plus addresses are a habit of careful users:

  • Filtering. jane+invoices@gmail.com goes straight to an Invoices label, and jane+newsletters@ skips the inbox.
  • Tracking leaks. If spam arrives addressed to jane+shopname@gmail.com, Jane knows which service lost or sold her address.
  • Testing. Developers and QA teams create many test accounts against one real inbox: sam+test1@acme.io, sam+test2@acme.io.

Blocking the + punishes exactly these people, many of whom are the technical buyers a B2B product wants. It also rejects a perfectly valid character: + is allowed in local parts, and forms that refuse it are simply broken. Check your own sign-up validation for this before anything else.

A plus address is also not a disposable address. A tag points at a real, long-lived inbox the person reads every day. Disposable inboxes and privacy relays hide the real inbox, which is a different problem with different answers; see the disposable email problem and email relay addresses.

Why it matters at sign-up

The trouble starts when you treat the raw address as a person’s identity:

  • Repeat free trials. jane+1@gmail.com, jane+2@gmail.com and j.a.n.e@gmail.com each pass a unique constraint on the email column, and each gets a fresh trial.
  • Referral credits and per-account promotions. One person can refer themselves as many times as they can type a new tag.
  • Duplicate CRM records. The same lead arrives as two contacts, gets two owners and receives every sequence twice. The CRM hygiene playbook covers the clean-up side.
  • Inflated metrics. Sign-up counts and activation rates drift when one person is three users.

The signal is the collision, not the tag. One jane+news@acme.io is ordinary. The fifth account that reduces to the same inbox is the thing to act on.

What isBusinessEmail returns

Every check of an address returns two fields for this, alongside the usual verdict:

  • has_subaddress: true when the address has a +tag, with the reason code subaddress.
  • normalized_email: the address lowercased, Unicode-normalized (NFKC), with an internationalized domain converted to punycode and the +tag removed.
{
  "email": "Jane.Doe+Trial@Acme.io",
  "normalized_email": "jane.doe@acme.io",
  "domain": "acme.io",
  "category": "business",
  "recommendation": "allow",
  "has_subaddress": true,
  "reasons": ["mx_google_workspace", "subaddress"]
}

subaddress is a flag only. It never changes category or recommendation under any policy, so jane+news@acme.io is allowed exactly like jane@acme.io. Role detection also looks past the tag: info+eu@acme.io is still a role account. The full field list is in response fields, and every code is in reason codes.

What normalized_email deliberately does not do is fold Gmail dots, because it’s a general rule applied to every domain:

Input normalized_email has_subaddress
Jane+Trial@Gmail.com jane@gmail.com true
j.a.n.e@googlemail.com j.a.n.e@googlemail.com false
jane+2@gmail.com jane@gmail.com true
Jane.Doe+news@Acme.io jane.doe@acme.io true
jane@bücher.de jane@xn--bcher-kva.de false

The Gmail-specific step is yours to add.

Build a dedupe key

Start from normalized_email and apply the Gmail rules only when the domain is Gmail:

// Uniqueness key built from the API's normalized_email.
// Gmail-only rules: dots don't matter, and googlemail.com is gmail.com.
// Never apply them to other domains, Google Workspace included.
const GMAIL_DOMAINS = new Set(['gmail.com', 'googlemail.com']);

export function dedupeKey(normalizedEmail) {
  const at = normalizedEmail.lastIndexOf('@');
  const local = normalizedEmail.slice(0, at);
  const domain = normalizedEmail.slice(at + 1);
  if (!GMAIL_DOMAINS.has(domain)) return normalizedEmail;
  return `${local.replace(/\./g, '')}@gmail.com`;
}

dedupeKey('jane@gmail.com');         // 'jane@gmail.com'
dedupeKey('j.a.n.e@googlemail.com'); // 'jane@gmail.com'
dedupeKey('jane.doe@acme.io');       // 'jane.doe@acme.io' (dots kept)

Your sign-up handler should fail open when the check is slow or down, so keep a local fallback that follows the same rules and recompute the key from the API’s answer when you re-check the account later:

// Fallback when the check fails open. Assumes the address already passed
// your syntax check; new URL() converts an IDN domain to punycode.
function normalizeLocally(email) {
  const s = email.trim().normalize('NFKC').toLowerCase();
  const at = s.lastIndexOf('@');
  let local = s.slice(0, at);
  const plus = local.indexOf('+');
  if (plus > 0) local = local.slice(0, plus);
  const domain = new URL(`http://${s.slice(at + 1)}`).hostname;
  return `${local}@${domain}`;
}

const key = dedupeKey(result.normalized_email ?? normalizeLocally(email));

The request itself is the usual POST /v1/check; checking for a business email in code has it in JavaScript, Python and PHP.

Store both, and let the database enforce it

Keep two columns:

  • email: what the person typed, trimmed. Every message you send goes here. Their filters may depend on the tag, and on a custom domain the exact spelling can matter.
  • email_key: the dedupe key. Use it for uniqueness checks, trial eligibility and finding an existing account.

Then let the database guarantee uniqueness, so two sign-ups submitted at the same moment can’t both succeed:

ALTER TABLE users ADD COLUMN email_key text;

-- Backfill email_key for existing rows, then look for collisions:
SELECT email_key, count(*) FROM users
GROUP BY email_key HAVING count(*) > 1;

CREATE UNIQUE INDEX users_email_key_unique ON users (email_key);

The index creation fails if existing rows already collide, which is why the query comes first. Decide what to do with those accounts (merge them, or leave them and enforce the rule from now on) before you add the constraint. Catch the unique violation in your sign-up code and treat it as “account already exists”.

Two more details:

  • Sign-in and password reset can look up by key, so someone who signed up as jane+trial@gmail.com and later types jane@gmail.com still finds their account. Send the link to the stored email, never to the key.
  • If you change the key rules later, recompute the column for every row before you rely on it again.

Decide what a collision means

A matching key tells you the inbox is the same. What you do about it is a product decision:

  1. Free trials: one trial per key. Let the second account exist on the free plan, or send the person to their existing account.
  2. Messaging: don’t confirm that an account exists to whoever is typing. “If you already have an account, we’ve sent a sign-in link to it” is friendlier and avoids leaking who your users are.
  3. Teams and agencies: one person may genuinely need several workspaces, for example a consultant working for several clients. Support several workspaces under one login rather than several logins per inbox.
  4. Rare false matches: a few self-hosted company servers don’t treat + as a separator, so sam+1@acme.io could in theory be its own mailbox. That is one more reason to answer a collision with a sign-in path, not a dead end.

The key also has limits. It won’t connect two separate Gmail accounts, a person who owns a domain with a catch-all mailbox, or relay addresses, which are unique per site by design. For those, combine signals as described in free trial abuse: email signals and the free trial abuse playbook.

What to do next

  1. Make sure your form accepts + in addresses.
  2. Check each sign-up on submit and store category and normalized_email with the account.
  3. Add an email_key column, backfill it, resolve collisions and add the unique index.
  4. Send every email to the original address.
  5. Test the path with the reserved test addresses: business+trial@test.isbusinessemail.com comes back with has_subaddress: true and normalized_email set to business@test.isbusinessemail.com, without using quota.

Then get a free API key and run the same check on the addresses you already have.

Frequently asked questions

What is plus addressing in email?

Plus addressing, also called subaddressing, lets you add a tag after a plus sign in the part before the @, such as jane+news@acme.io. The mail is delivered to the main mailbox, jane@acme.io, and the tag stays visible so you can filter or sort on it.

Does Gmail ignore dots in email addresses?

Yes, for consumer Gmail addresses on gmail.com and googlemail.com: jane.doe@gmail.com and janedoe@gmail.com reach the same inbox. On Google Workspace and other custom domains, dots are part of the address, and two spellings can belong to two different people.

Should I block plus addresses at sign-up?

No. People use tags to filter mail and to trace which service leaked their address, and blocking them punishes careful users. Compare a normalized key instead, so a second trial from the same inbox is caught without rejecting anyone.

Does Outlook support plus addressing?

Yes. Outlook.com and Microsoft 365 both support plus addressing; on Microsoft 365 it is an organization setting that admins can turn off. Gmail, Google Workspace, Fastmail and Proton Mail support it too.

Which address should I send email to, the original or the normalized one?

Always the original address the person typed. Their filters may depend on the tag, and on custom domains the exact spelling can matter. Use the normalized key only to check uniqueness.

Does isBusinessEmail remove Gmail dots in normalized_email?

No. normalized_email is lowercased, Unicode-normalized, has the domain in punycode and drops the +tag, on every domain. Removing Gmail dots and mapping googlemail.com to gmail.com is a separate step you add when you build your dedupe key.