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:
- Pass 1 — classify → label
Triage/Junk-Auto(reversible; nothing deleted) - review window in Gmail (
label:Triage/Junk-Auto) - 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.
xlab_gmail_inbox_cleanup_2026-06-25.md · Rendered Built with — component scripts
seo_render_html.py— wraps the source.mdin the dash.gf.cx design language (+anchor_enricher.pyfor inline-link promotion & rollover thumbnails)dare_dev_reports_publish.py— bundles the day’s reports into the catalog and ships to dash.gf.cx/reports