🧹 List Hygiene & Validation

How to Remove Duplicate Emails From a CSV Without Losing Real Contacts

Remove duplicate emails from a CSV by matching on a normalized key: trim, lowercase, and apply Gmail dot and plus rules only where they hold. Worked example.

Illustration of spreadsheet rows funnelling into a filter that merges matching rows into single entries
Quick answer

Do not compare the raw email column. Build a match key for each row: trim spaces, lowercase the address, and for gmail.com only, drop the dots and anything after a +. Rows with the same key are duplicates. Decide which row wins (usually the most recent), keep a file of what you removed, and leave dots and +tags alone on company domains, where they can belong to different people.

Most spreadsheet guides stop at "select the column and click Remove Duplicates". That only catches rows that are character-for-character identical, so Anna.Lee@Gmail.com, anna.lee@gmail.com and annalee+promo@gmail.com all survive as three contacts, and the same person gets three copies of every campaign. Going too far in the other direction is just as harmful: merge j.smith@company.co with jsmith@company.co and you may delete a real colleague. This guide shows how to choose the matching rules, which row to keep, and how to do it in a spreadsheet, in a short script, or in your browser.

Why exact matching misses most duplicates

Duplicates in a contact export rarely look identical. They come from different sign-up forms, imports from several tools, and people typing their address slightly differently each time. The common variations are:

  • Stray whitespace: a trailing space pasted from another system.
  • Letter case: Omar@Example.COM versus omar@example.com.
  • Gmail dots: anna.lee@gmail.com versus annalee@gmail.com.
  • Plus tags: annalee+promo@gmail.com, created by a user who labels sign-ups.

Each variation needs its own decision, because the email standards and the big mailbox providers treat them differently.

What the standards say about case, dots and plus signs

RFC 5321, the SMTP standard, says the domain part of an address follows DNS rules and is not case-sensitive. The local part (before the @) must be treated as case-sensitive by mail software, because some hosts really do distinguish smith from Smith. The same section adds that exploiting this difference harms interoperability and is discouraged. In practice, two addresses that differ only in case almost always reach the same person, which is why most marketing lists compare them in lowercase.

Dots and plus signs are provider rules, not standard ones:

  • Gmail's help centre states that dots do not matter in Gmail addresses, so anna.lee@gmail.com and annalee@gmail.com are one inbox. The same page says that for accounts run by an employer or school, dots do change the address (according to Gmail Help, as of October 2026).
  • Gmail also delivers janedoe+school@gmail.com to janedoe@gmail.com: the part after + is a label the owner chooses.
  • Microsoft documents plus addressing in Exchange Online as on by default, but an administrator can turn it off. When a message arrives for an address with a +, Exchange first looks for a mailbox with that full address and only then retries without the tag (according to Microsoft Learn, as of October 2026).

That last point is the reason not to strip plus tags on every domain: on a company domain, sales+emea@ may be a separate mailbox, or plus addressing may be switched off, so the tagged address is not the same as the plain one.

Choose your matching rules: a decision table

RuleApply toSafe?Why
Trim leading and trailing spacesAll rowsAlwaysSpaces are never part of a valid address.
Lowercase the domainAll rowsAlwaysDomains are case-insensitive under RFC 5321.
Lowercase the local partAll rowsAlmost alwaysAllowed to differ by the standard, but discouraged, and rare in practice.
Remove dots in the local partgmail.com onlyYes, for that domainGmail documents that dots are ignored; work and school accounts are excluded.
Remove +taggmail.com only, by defaultYes, for that domainGmail documents the label behaviour; other providers may use the full address or disable the feature.
Merge "similar" addresses (typo guesses)None automaticallyNoCorrect only known typo domains, and review every change.

The first three rules form a match key you can use on any list. The Gmail rules are worth adding when a large share of your list is on gmail.com; on a B2B list of company addresses they rarely matter.

Worked example: one file, five rule sets

Here is a 12-row export with an email, name and signed_up column. Row 3 is wrapped in spaces, the second Lina row (row 11) has a trailing space, and row 12 has no email at all.

email,name,signed_up
anna.lee@gmail.com,Anna Lee,2026-03-02
Anna.Lee@Gmail.com,Anna,2026-05-11
 annalee@gmail.com ,Anna L.,2026-06-20
annalee+promo@gmail.com,Anna Lee,2026-07-01
omar@example.com,Omar,2026-01-15
Omar@Example.COM,Omar H.,2026-04-09
omar+news@example.com,Omar,2026-08-30
j.smith@company.co,John Smith,2026-02-10
jsmith@company.co,Jane Smith,2026-02-12
lina@outlook.com,Lina,2026-05-05
lina@outlook.com ,Lina,2026-09-14
,Unknown,2026-06-01

We counted unique addresses among the 11 non-blank rows under each rule set with a Python script:

Match keyUnique contactsDuplicates removed
Raw value (exact match)110
Trimmed101
Trimmed, domain lowercased (strict RFC reading)101
Trimmed and fully lowercased83
Trimmed, lowercased, Gmail dots and +tag removed65

The last rule set merges Anna's four rows into one and Omar's two case variants into one, while it correctly leaves omar+news@example.com, j.smith@company.co and jsmith@company.co as separate contacts. John and Jane Smith show why: on a company domain, a dot can be the only thing that separates two people.

Trimremove spacesLowercasewhole addressGmail rulesgmail.com onlySort, then keep onelog removed rows
Steps to build a match key before removing duplicates

Decide which duplicate row to keep

Removing a duplicate removes the whole row, not just the email cell. Microsoft's Excel documentation states this directly for Remove Duplicates: when a match is found in the selected columns, the entire row is deleted, including the other columns. So the order of your rows decides which name, date and consent record survive.

  1. Newest wins: sort by sign-up or last-activity date, newest first, before deduplicating. Good for names and preferences that change.
  2. Oldest wins: sort oldest first. Good when the earliest row holds the original consent record you may need as proof.
  3. Most complete wins: sort so rows with more filled fields come first. Good for CRM imports.
  4. Merge: keep one row but copy in fields that are empty in it from the others. Needs a script or a CRM merge feature.

Whatever you choose, save the removed rows to a separate file with the reason. It is your audit trail if someone asks why they stopped receiving mail, and it lets you undo a bad rule.

Method 1: Excel or Google Sheets with a helper column

  1. Make a copy of the file. Deduplication deletes rows.
  2. Insert a helper column next to the emails and fill it with =LOWER(TRIM(A2)), then copy it down.
  3. Sort the sheet so the row you want to keep comes first (for example by signed_up, newest to oldest).
  4. Select the data and use Data → Remove Duplicates (Excel) or Data → Data cleanup → Remove duplicates (Sheets), ticking only the helper column.
  5. Delete the helper column and export as CSV with UTF-8 encoding.

Both apps keep one row per match key and delete the others. Microsoft states that Excel keeps the first occurrence and compares the value shown in the cell, not the value stored in it, and it recommends filtering or highlighting unique values first to check the result. Google's help page says Sheets already treats values that differ only in letter case as duplicates, and its separate Trim whitespace option does not remove non-breaking spaces, which is another reason to build the key explicitly (according to Microsoft Support and Google Docs Editors Help, as of October 2026). The helper column makes that check simple: sort by it and scan neighbouring rows. Spreadsheet formulas for Gmail-only dot and plus rules get long, so if you need those, use a script or the tool below.

Method 2: a short Python script you can audit

This uses only the standard library. It keeps the newest row for each key, writes the trimmed address, and saves every removed row with a reason.

import csv

def match_key(email, gmail_rules=True):
    e = email.strip().lower()
    local, _, domain = e.rpartition('@')
    if gmail_rules and domain == 'gmail.com':
        local = local.split('+')[0].replace('.', '')
    return local + '@' + domain

with open('contacts.csv', newline='', encoding='utf-8') as f:
    reader = csv.DictReader(f)
    fields = reader.fieldnames
    rows = list(reader)

rows.sort(key=lambda r: r['signed_up'], reverse=True)  # newest first wins

kept, removed = {}, []
for r in rows:
    if not r['email'].strip():
        removed.append({**r, 'reason': 'blank email'})
        continue
    k = match_key(r['email'])
    if k in kept:
        removed.append({**r, 'reason': 'duplicate of ' + kept[k]['email']})
    else:
        r['email'] = r['email'].strip()
        kept[k] = r

with open('contacts_clean.csv', 'w', newline='', encoding='utf-8') as f:
    w = csv.DictWriter(f, fieldnames=fields)
    w.writeheader()
    w.writerows(kept.values())

with open('contacts_removed.csv', 'w', newline='', encoding='utf-8') as f:
    w = csv.DictWriter(f, fieldnames=fields + ['reason'])
    w.writeheader()
    w.writerows(removed)

On the example file it keeps 6 rows and moves 6 to contacts_removed.csv: five duplicates and the blank row. Note that Anna's surviving address is annalee+promo@gmail.com, because it is her newest sign-up. That is fine: it delivers to the same inbox and it is the address she most recently gave you. ISO dates (YYYY-MM-DD) sort correctly as text; other date formats need parsing first.

Method 3: in your browser, without uploading the list

If you would rather not write formulas or code, the cleaner below accepts a pasted list or a CSV, keeps your other columns, compares addresses in lowercase, and has an option to treat Gmail dots and +tag as duplicates on gmail.com and googlemail.com. It keeps the first occurrence, so sort the file first if you want the newest row to win. It also reports how many rows were removed and why, and it runs entirely in your browser.

Email List CleanerRemove duplicates and invalid addresses, fix typos and split by providerOpen the tool →

Common mistakes

  • Deduplicating on the whole row. Two rows for the same person with different dates are not identical rows, so nothing is removed. Match on the email key only.
  • Applying Gmail rules to every domain. This merges different people on company domains and Google Workspace accounts, where dots matter.
  • Forgetting the order. "Keep first" with an unsorted file keeps whichever row happened to be imported first, often the oldest name and preferences.
  • Not keeping the removed rows. Without them you cannot answer a consent question or undo a mistake.
  • Stopping at duplicates. A deduplicated list can still contain invalid addresses that bounce. Check syntax and remove known hard bounces before you send.

Deduplication is one step of list care. For the rest of the routine, read our email list hygiene guide, the rules for valid email address syntax, and how email validation works (syntax, MX and SMTP checks). If addresses still bounce after cleaning, the 550 5.1.1 bounce code guide explains what to remove.

Frequently asked questions

Are email addresses case sensitive?

The domain is not. RFC 5321 says mail software must treat the part before the @ as case-sensitive, but it also says relying on that is discouraged. In practice, addresses that differ only in case reach the same person, so most lists compare them in lowercase.

Is john.smith@gmail.com the same as johnsmith@gmail.com?

Yes for personal Gmail accounts: Google states that dots do not matter in Gmail addresses. For accounts managed by a company or school on their own domain, dots do change the address.

Should I remove plus addresses from my email list?

Do not delete them; the person chose that address. You can treat name+tag@gmail.com as a duplicate of name@gmail.com. On other domains, keep them separate unless you know the provider ignores the tag.

How do I remove duplicates in a CSV without Excel?

Use Google Sheets (Data, Data cleanup, Remove duplicates), a short Python script that builds a normalized key per row, or a browser-based list cleaner that does the comparison locally.

Which duplicate does Excel keep?

Remove Duplicates keeps the first matching row in the current order and deletes the entire later rows. Sort the sheet first so the row you want to keep is on top.

References & further reading

  1. RFC 5321 — Simple Mail Transfer Protocol, section 2.4
  2. Gmail Help — Dots don't matter in Gmail addresses
  3. Gmail Help — Send emails from a different address or alias
  4. Microsoft Learn — Plus Addressing in Exchange Online
  5. Microsoft Support — Filter for unique values or remove duplicate values
  6. Google Docs Editors Help — Split text, remove duplicates, or trim whitespace

Related articles

List Hygiene & Validation

Double Opt-In vs Single Opt-In: Which One Fits Your Email List?

Double opt-in vs single opt-in compared on consent proof, bad addresses, complaints and list growth, with a break-even formula to test both on your own numbers.

8 min read
List Hygiene & Validation

How to Validate Email Addresses in Bulk: What Syntax, MX and SMTP Checks Really Prove

How to validate email addresses in bulk: the layers from syntax and MX records to SMTP probing, what each proves and how to avoid hurting reputation.

7 min read
List Hygiene & Validation

SMTP 550 5.1.1 Bounce Code: What It Means and How to Fix It

550 5.1.1 means the recipient mailbox does not exist. Learn how to read the code, tell it from 5.1.2, 5.2.2 and 5.7.1, and clean your list correctly.

5 min read

Newest tools

Recently added free tools — they run in your browser without uploading your data

All tools →

Remove image backgrounds in the browser with the small U²-Net model through ONNX Runtime Web, with a colour or image replacement and transparent PNG download

Local model

Build a valid n8n workflow JSON with a wizard: pick the trigger, steps and error handling with the latest node versions from the catalog

Local

Search n8n nodes (430+) by name, type and versions, read explanations of the common ones, and paste a workflow to explain its nodes and spot older versions

Local

Paste an OpenClaw log line (gateway, config, cron, heartbeat, channels) to get the likely cause, fix steps and a useful command, with a local gateway test

Local