Gmail inbox cleanup — dan@dare.co.uk halved (139,926 → 72,137)

XLAB · OPS REPORT · 2026-06-25

Reopened the gmail_triage.py two-pass inbox-cleanup engine, swept the high-confidence junk pool, and hardened the engine against transient API timeouts along the way. All destructive actions are 30-day reversible by construction.


Outcome

The dan@dare.co.uk mailbox was carrying 139,926 messages, ~80% of it aged bulk Promotions/Updates. A single two-pass sweep trashed 67,791 of them — roughly half the mailbox.

Metric Before After
Total messages 139,926 72,137
Promotions 46,561 7,223
Updates 64,214 35,763
Trash 0 67,791

Everything trashed was 4+ year old marketing-category mail. ebay.com and amazon.com (purchase records) and the own-domains (dare.co.uk / gf.cx) were allowlist-protected throughout; a per-window metadata spot-check confirmed zero allowlisted senders in the queue before anything was trashed. Live verification after the run: label:Triage/Junk-Auto -in:trash = 0; label:Triage/Junk-Auto (from:ebay OR from:amazon) = 0.

Safety model (why this was low-risk)

The triage engine never holds the https://mail.google.com/ scope — only gmail.modify — so permanent delete is structurally impossible. Every “delete” is a move to Gmail Trash, which auto-purges after 30 days. So the entire sweep is recoverable until ~2026-07-26 via in:trash label:Triage/Junk-Auto.

Two human-gated passes, with the label as the durable queue between them:

  1. Pass 1 — classify → label Triage/Junk-Auto (reversible; nothing deleted)
  2. review window in Gmail (label:Triage/Junk-Auto)
  3. Pass 2 — trash exactly that label (30-day recoverable)

Distribution finding

The junk skews old — there was effectively nothing droppable in years 1–4 (recent bulk mail is dominated by the allowlisted ebay/amazon notifications). The whole 67,791 came from the 4y+ windows:

Window Labeled
4y–5y 6,573
5y–7y 24,709
7y–10y 24,685
10y+ 11,824

Top non-allowlisted senders cleared: everbridge.net (5.6k mass-alerts), and a long tail of newsletters/marketing (New Scientist, AFI, TipRanks, Economist, AIGA, eToro, Etsy, Dezeen, AuctionTime, Medium, Dow Jones…) most at 96–100% unsubscribe-able. Also swept, by explicit decision, ~57k of aged Amex/Apple/PNC/Capital One/PayPal/proxyvote marketing copies — all recoverable for 30 days if any account records are wanted back.

Engine hardening (the durable win)

The first sweep attempts died on TimeoutError: The read operation timed out — paging a 140k-message mailbox routinely drops a socket mid-page, and the bare .execute() calls had no retry, so one transient blip aborted the whole run with no progress saved. (Same transient-network class as the claude.gf.cx limit-event tripwire false-positive fixed earlier the same day.)

Fix (gmail_triage.py, committed ea98104): an _execute() wrapper that retries read-timeouts / connection-resets / 429 / 5xx with bounded backoff, plus socket.setdefaulttimeout(90), wired into every paging and write call (list_ids, estimate, fetch_metadata, ensure_label, batch_modify, the trash loop). With that in place the 67,791-message sweep completed cleanly. Future sweeps inherit the resilience.

How to re-run

gmail_triage.py overview                       # current label totals
gmail_triage.py histogram --query "..."        # top-sender assessment
gmail_triage.py classify --apply               # Pass 1 (needs modify token)
gmail_triage.py trash --apply                  # Pass 2 (30-day recoverable)

The Triage/Junk-Auto label is now empty and reusable. Years 1–4 hold ~0 droppable junk, so this won’t need re-running until newer promo mail ages past a year.

Source: xlab_gmail_inbox_cleanup_2026-06-25.md · Rendered
Built with — component scripts