← back to George Gmail
Add drain script: permanently delete info@ drafts older than 30 days
7106f29ea96eddf2d7a064b1c729de30d20b193e · 2026-08-20 11:14:36 -0700 · Steve Abrams
Batched drain (drafts.list caps at 500, no pagination): discovers the
authoritative in:drafts older_than:30d message-id set once, then loops
list->delete-matches until none remain. Quota backoff, dry-run default,
per-deletion jsonl log. Steve-authorized run 2026-08-20: 3665 deleted, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A delete-old-drafts-info.js
Diff
commit 7106f29ea96eddf2d7a064b1c729de30d20b193e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 11:14:36 2026 -0700
Add drain script: permanently delete info@ drafts older than 30 days
Batched drain (drafts.list caps at 500, no pagination): discovers the
authoritative in:drafts older_than:30d message-id set once, then loops
list->delete-matches until none remain. Quota backoff, dry-run default,
per-deletion jsonl log. Steve-authorized run 2026-08-20: 3665 deleted, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
delete-old-drafts-info.js | 141 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 141 insertions(+)
diff --git a/delete-old-drafts-info.js b/delete-old-drafts-info.js
new file mode 100644
index 0000000..d94fb98
--- /dev/null
+++ b/delete-old-drafts-info.js
@@ -0,0 +1,141 @@
+#!/usr/bin/env node
+/*
+ * delete-old-drafts-info.js — drain old drafts from info@designerwallcoverings.com
+ *
+ * Permanently deletes EVERY draft older than 30 days (Gmail `older_than:30d`).
+ * Steve-authorized 2026-08-20 ("delete all drafts over 30 days old", info-office).
+ * Confirmed scope: ~3,665 old drafts (garbage sample-follow-up dumps).
+ *
+ * SAFETY:
+ * - Authoritative age filter: the server-side set `in:drafts older_than:30d`.
+ * A draft is deleted ONLY if its messageId is in that >30d set, so newer
+ * drafts are structurally excluded no matter what drafts.list returns.
+ * - drafts.list caps at 500 (George's route has no pageToken), so deletion
+ * runs as a DRAIN LOOP: list 500 -> delete the >30d matches -> repeat,
+ * until a full pass finds zero matches (or the set is exhausted).
+ * - DRY RUN by default. CONFIRM=1 to actually delete.
+ * - Gmail drafts.delete is PERMANENT (no Trash). Every deletion is logged to
+ * a local jsonl for the record. The full >30d set is cached to a SET file
+ * so a re-run can skip the expensive rediscovery (pass SET=/path).
+ *
+ * USAGE:
+ * node delete-old-drafts-info.js # dry run: counts + writes SET cache
+ * CONFIRM=1 node delete-old-drafts-info.js # discover (or reuse SET) then drain-delete
+ * SET=/tmp/...set.json CONFIRM=1 node ... # reuse a cached set, skip discovery
+ */
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const BASE = process.env.GEORGE_BASE || 'http://127.0.0.1:9850';
+const ACC = 'info';
+const QUERY = 'in:drafts older_than:30d';
+const DELETE = process.env.CONFIRM === '1';
+const SET_IN = process.env.SET || '';
+const PAGE = parseInt(process.env.PAGE || '40', 10);
+const PAGE_SLEEP = parseInt(process.env.PAGE_SLEEP || '3000', 10);
+const DEL_SLEEP = parseInt(process.env.DEL_SLEEP || '250', 10);
+const MAX_BATCHES = parseInt(process.env.MAX_BATCHES || '40', 10);
+
+function resolveAuth() {
+ if (process.env.GEORGE_AUTH && process.env.GEORGE_AUTH.includes(':')) {
+ const [u, ...rest] = process.env.GEORGE_AUTH.split(':');
+ return { u, p: rest.join(':') };
+ }
+ let u = 'admin', p = process.env.GEORGE_BASIC_AUTH_PASS || '';
+ const envPath = path.join(process.env.HOME || '', 'Projects/Designer-Wallcoverings/DW-MCP/.env');
+ try {
+ const t = fs.readFileSync(envPath, 'utf8');
+ const m = t.match(/^GEORGE_BASIC_AUTH=(.+)$/m);
+ if (m) { const v = m[1].trim(); if (v.includes(':')) { const s = v.split(':'); u = s[0]; p = s.slice(1).join(':'); } }
+ if (!p) { const mp = t.match(/^GEORGE_BASIC_AUTH_PASS=(.+)$/m); if (mp) p = mp[1].trim(); }
+ } catch (_) { /* fall back */ }
+ return { u, p };
+}
+
+const { u, p } = resolveAuth();
+const H = { Authorization: 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64') };
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+async function jreq(url, opts = {}, tries = 8) {
+ let wait = 20000;
+ for (let i = 0; i < tries; i++) {
+ let r;
+ try { r = await fetch(url, { ...opts, headers: H }); }
+ catch (e) { if (i === tries - 1) throw e; await sleep(wait); wait = Math.min(wait * 1.6, 120000); continue; }
+ if (r.ok) return r;
+ const body = (await r.text()).slice(0, 300);
+ const quota = r.status === 429 || /Quota exceeded|rateLimitExceeded|userRateLimit/i.test(body);
+ if (quota && i < tries - 1) { console.warn(` quota (${r.status}); backoff ${Math.round(wait / 1000)}s`); await sleep(wait); wait = Math.min(wait * 1.6, 120000); continue; }
+ throw new Error(`${opts.method || 'GET'} ${r.status} ${url} :: ${body}`);
+ }
+}
+const jget = async (url) => (await jreq(url)).json();
+
+async function discoverSet() {
+ const older = new Set();
+ let token = '', page = 0;
+ do {
+ const url = `${BASE}/api/messages?account=${ACC}&q=${encodeURIComponent(QUERY)}&maxResults=${PAGE}` + (token ? `&pageToken=${token}` : '');
+ const d = await jget(url);
+ (d.messages || []).forEach((m) => m && m.id && older.add(m.id));
+ token = d.nextPageToken || '';
+ page++;
+ process.stdout.write(`\r discovery page ${page}: ${older.size} >30d draft-msgs `);
+ if (token) await sleep(PAGE_SLEEP);
+ } while (token);
+ console.log('');
+ return older;
+}
+
+(async () => {
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+
+ // ---- build / load the authoritative >30d message-id set ----
+ let older;
+ if (SET_IN) {
+ older = new Set(JSON.parse(fs.readFileSync(SET_IN, 'utf8')));
+ console.log(`loaded >30d set from ${SET_IN}: ${older.size} ids`);
+ } else {
+ older = await discoverSet();
+ const setFile = `/tmp/george-old-drafts-set-${stamp}.json`;
+ fs.writeFileSync(setFile, JSON.stringify([...older]));
+ console.log(`>30d draft messages: ${older.size} (set cached: ${setFile})`);
+ }
+
+ if (!DELETE) {
+ // dry-run: show how many of the first 500 drafts are deletable right now
+ const drafts = await jget(`${BASE}/api/drafts?account=${ACC}&maxResults=500`);
+ const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id));
+ console.log(`\n--- DRY RUN (no deletions). CONFIRM=1 to drain. ---`);
+ console.log(`total >30d drafts to remove : ${older.size}`);
+ console.log(`deletable in first 500 window: ${matches.length}`);
+ console.log(`(deletion runs as a drain loop across ~${Math.ceil(older.size / 422)} batches)`);
+ return;
+ }
+
+ // ---- DRAIN LOOP ----
+ const logFile = `/tmp/george-old-drafts-deleted-${stamp}.jsonl`;
+ let totalOk = 0, totalFail = 0, batch = 0;
+ const target = older.size;
+ while (batch < MAX_BATCHES) {
+ batch++;
+ const drafts = await jget(`${BASE}/api/drafts?account=${ACC}&maxResults=500`);
+ const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id));
+ if (matches.length === 0) { console.log(`\nbatch ${batch}: 0 matches — drain complete.`); break; }
+ console.log(`\nbatch ${batch}: ${matches.length} matches (of ${Array.isArray(drafts) ? drafts.length : '?'} listed) — deleting...`);
+ for (const t of matches) {
+ try {
+ await jreq(`${BASE}/api/drafts/${t.id}?account=${ACC}`, { method: 'DELETE' });
+ fs.appendFileSync(logFile, JSON.stringify({ ts: new Date().toISOString(), draftId: t.id, messageId: t.message.id }) + '\n');
+ older.delete(t.message.id);
+ totalOk++;
+ if (totalOk % 50 === 0) process.stdout.write(`\r deleted ${totalOk}/${target} `);
+ } catch (e) { totalFail++; console.error(`\n FAIL draft=${t.id}: ${e.message}`); }
+ await sleep(DEL_SLEEP);
+ }
+ }
+ console.log(`\n\nDONE. deleted=${totalOk} failed=${totalFail} remaining-in-set=${older.size}`);
+ console.log(`Deletion log: ${logFile}`);
+ if (older.size > 0) console.log('Some >30d drafts remain (batch cap or failures). Re-run to finish.');
+})().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });
← f1e3c56 auto-data-snapshot: 2026-08-20T08:00:08 (1 data files) — dat
·
back to George Gmail
·
Schedule daily info@ old-draft drain (launchd) with runaway 0fd5c29 →