Skip to content

Removing duplicate mail: a worked example

Duplicates come from two places: mail that one account auto-forwards to another, so both mailboxes hold a copy, and mail that arrived twice in the same account (a re-sent message, an import run twice). dedup finds them in your backups and removes the extra copies from Gmail, in stages you check. Nothing is deleted permanently: the last step moves messages to Gmail's Trash, which Gmail empties after 30 days.

This page walks through one run. The mail and addresses in it are invented. Every option is in the reference.

On an IMAP account the stages are the same, with a keyword or a review folder in place of the label; see Backing up an IMAP account.

Stage Command What changes
1. Back up, index backup, dedup index the backup grows; mailcellar's own index
2. Match dedup match nothing
3. Plan dedup plan only mailcellar's own plan file
4. Dry run dedup apply nothing
5. Label dedup apply --label Gmail: adds the label dedup-redundant
6. Review in Gmail you remove the label from anything to keep
7. Trash dedup apply --trash Gmail: moves labelled mail to Trash
8. Check count nothing

Stages 1 to 4 can be repeated as often as you like. Stages 5 and 7 need modify access to the account: each asks you to sign in in the browser when it runs, and keeps that access only until it ends.

1. Back up, then index

dedup works on the backup, not on Gmail, and before it removes a copy it checks that the copy is in the backup. So back up first, then catalogue:

mailcellar backup
mailcellar dedup index

index reads each backup's meta.sqlite and builds a small index in <data root>/dedup/. It is only an index: deleting it is safe, and dedup index builds it again. If an account's meta.sqlite lacks rows for older mail, run mailcellar meta first.

2. See what matches

mailcellar dedup match

This prints the groups of copies of the same message, split into cross groups (copies in different accounts) and within groups (copies in one account), and how big they are. Two copies are the same message when their visible text and attachments are identical and they share a Message-ID or the same From, Date and Subject. Sender, date and subject alone would group different messages, such as two invoices, so they are never used on their own. How it works has the reasoning.

Message-IDs that were reused for different content are reported, not grouped, so they can't cause a removal. --csv FILE writes every matched copy to a file if you want to look through them.

3. Plan

The plan decides, for each copy, keep, remove or review. You choose how by saying which copies win.

Across accounts. Suppose work auto-forwards to personal, so every work message exists twice.

mailcellar dedup plan --keep origin

--keep origin removes only copies created by auto-forwarding between your accounts, and keeps the one in the account the message came from. Mail you received in both accounts independently is left alone, because it isn't a copy of anything. This is the safe choice when you don't have a reason for another.

mailcellar dedup plan --keep personal

--keep personal keeps every copy in personal and removes the other accounts' copies of the same messages, forwarded or not. Use it when one account is where you want everything to live. A group with no copy in personal goes to review.

Within one account.

mailcellar dedup plan --scope within

For each group it keeps one copy from the largest set of copies with the same labels and removes the others. A copy with different labels goes to review, since removing it would lose a label. --scope both plans both kinds, and --keep is then needed for the cross part.

What goes to review, not remove. The plan won't remove a copy when:

  • it is a sent message or a draft;
  • the copy that would be kept is in Trash or Spam;
  • the copy carries stars or user labels that the kept copy lacks.

If a label is of no value, name it with --ignore-label NAME (repeat it for more) and copies that differ only by it are removed. Nothing in the review rows is ever touched by apply: they are for you to decide by hand in Gmail.

The plan is a file you can open, <data root>/dedup/dedup-plan.csv. Each row has the action, the reason, the group, the account and Gmail ID, the copy that is kept, and the sender, subject, labels and size. Open it in a spreadsheet and sort by action and reason. If something looks wrong, change the options and plan again: a new plan replaces the old file.

4. Dry run

mailcellar dedup apply

With no flag, apply changes nothing. It checks every planned removal against live Gmail and the backup and reports what it would do. A removal passes only if:

  • the copy's .eml is in the backup;
  • the message and the copy that is kept are both still in Gmail;
  • the kept copy isn't in Trash or Spam;
  • the message's labels haven't changed since the plan was made.

Anything that fails is skipped, with the reason. A plan that is days old may skip some; plan again after a new backup. A passing removal shows would label or trash.

5. Label

Start small:

mailcellar dedup apply --label --limit 5

This looks at the first five removals in the plan and adds the label dedup-redundant (--label NAME to choose another) to those that pass the checks. Open Gmail and look at the label: each message should be an extra copy of one you can see elsewhere.

When you are satisfied, label the rest by leaving out --limit, or in steps.

6. Review in Gmail

The label is the veto. Remove dedup-redundant from any message you want to keep, and apply --trash skips it, for good. Messages you leave labelled are the ones that will go.

7. Trash

mailcellar backup                      # so the backup is current
mailcellar dedup apply --trash --limit 5

--trash moves labelled messages to Trash. It repeats the checks of the dry run, so a message whose labels changed since the plan is skipped, and it asks once per account: Move N messages in <account> to Trash? [y/N] (--yes skips the question). Trashing takes about a minute for 2,000 messages, so for tens of thousands run it in the background or with --limit.

Each run is appended to <data root>/dedup/dedup-apply-log.csv.

Gmail empties Trash after 30 days. Until then you can move a message back by hand in Gmail.

8. Check

mailcellar count

count compares Gmail with the backup. The trashed copies stay in the backup, so after Gmail empties its Trash they will show as in the backup but not in Gmail.

Do not use forget --deleted to clear them out unthinkingly. It protects mail that was offloaded or restored, because those are recorded in the backup. dedup records nothing, so removed duplicates look like mail Gmail lost, and forget --deleted will offer to take them out of the backup. They are duplicates, and they are held where --undo can put them back until --empty deletes them, but read what it lists before you type the count. See Backing up.

If it went wrong

  • Before you trashed: remove the label from what you want to keep, or delete the label from every message. Nothing else has changed.
  • After trashing, within 30 days: open Gmail's Trash and move the messages back. Search label:dedup-redundant in:trash.
  • After Gmail emptied the Trash: the copy is in the backup. Put it back with mailcellar restore ACCOUNT --id GMAIL_ID; the ID is in the plan and in the apply log. A copy you removed was a duplicate of one that is still in Gmail, so this is rarely needed. See Restoring mail.