[object Object]

← back to Claude Mail

mailer: archive every reply to IMAP Sent (best-effort) + read-only thread-dump tool

2afc0490759b4904e48dbce35300e8d695e03bcf · 2026-06-25 10:08:28 -0700 · Steve

Files touched

Diff

commit 2afc0490759b4904e48dbce35300e8d695e03bcf
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 10:08:28 2026 -0700

    mailer: archive every reply to IMAP Sent (best-effort) + read-only thread-dump tool
---
 lib.js         | 34 +++++++++++++++++++++++++---------
 thread-dump.js | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 61 insertions(+), 9 deletions(-)

diff --git a/lib.js b/lib.js
index 6aaa324..7376cc0 100644
--- a/lib.js
+++ b/lib.js
@@ -4,6 +4,8 @@
 
 const { spawn } = require('node:child_process');
 const nodemailer = require('nodemailer');
+const MailComposer = require('nodemailer/lib/mail-composer');
+const { ImapFlow } = require('imapflow');
 
 const cfg = {
   user: process.env.MAIL_USER,
@@ -123,17 +125,31 @@ function transport() {
 }
 
 async function sendMail({ to, subject, text, inReplyTo, references }) {
-  const t = transport();
-  const headers = {};
-  const info = await t.sendMail({
+  // Build the raw message ONCE so the SMTP-sent bytes and the archived copy match.
+  const mail = new MailComposer({
     from: `"${cfg.fromName}" <${cfg.user}>`,
-    to,
-    subject,
-    text,
-    inReplyTo,
-    references,
-    headers,
+    to, subject, text, inReplyTo, references,
   });
+  const raw = await mail.compile().build();
+
+  const info = await transport().sendMail({
+    envelope: { from: cfg.user, to },
+    raw,
+  });
+
+  // Archive a copy to the IMAP Sent folder so every reply is auditable.
+  // Best-effort: a failed append must never fail the send.
+  try {
+    const c = new ImapFlow({
+      host: cfg.imapHost, port: cfg.imapPort, secure: true,
+      auth: { user: cfg.user, pass: cfg.pass }, logger: false,
+    });
+    await c.connect();
+    await c.append('Sent', raw, ['\\Seen']);
+    await c.logout();
+  } catch (e) {
+    console.error(new Date().toISOString(), `sent-archive append failed: ${e.message}`);
+  }
   return info;
 }
 
diff --git a/thread-dump.js b/thread-dump.js
new file mode 100644
index 0000000..abe6dd4
--- /dev/null
+++ b/thread-dump.js
@@ -0,0 +1,36 @@
+'use strict';
+// READ-ONLY: dump recent INBOX messages + list folders (to find Sent). No writes.
+const { ImapFlow } = require('imapflow');
+const { simpleParser } = require('mailparser');
+const { cfg } = require('./lib');
+
+(async () => {
+  const client = new ImapFlow({
+    host: cfg.imapHost, port: cfg.imapPort, secure: true,
+    auth: { user: cfg.user, pass: cfg.pass }, logger: false,
+  });
+  await client.connect();
+  console.log('=== mailboxes ===');
+  const boxes = await client.list();
+  for (const mb of boxes) console.log('  ', mb.path);
+
+  const dump = async (box) => {
+    let lock;
+    try { lock = await client.getMailboxLock(box); } catch { console.log(`\n(no ${box})`); return; }
+    try {
+      console.log(`\n=== ${box} (most recent 6) ===`);
+      const all = await client.search({ all: true });
+      const last = (all || []).slice(-6);
+      for (const uid of last) {
+        const { content } = await client.download(uid, undefined, { uid: true });
+        const p = await simpleParser(content);
+        const body = (p.text || '').replace(/\s+\n/g, '\n').trim().slice(0, 600);
+        console.log(`\n--- uid ${uid} | from=${p.from?.text} | to=${p.to?.text}\n    subj=${p.subject}\n${body}`);
+      }
+    } finally { lock.release(); }
+  };
+
+  await dump('INBOX');
+  for (const s of ['Sent', 'INBOX.Sent', 'Sent Items']) { await dump(s); }
+  await client.logout().catch(() => {});
+})().catch((e) => { console.error('DUMP FAILED:', e.message); process.exit(1); });

← 92f1320 docs: agentabrams.com Cloudflare migration package (DTD verd  ·  back to Claude Mail  ·  gate: add RFC 3834 mail-loop breakers (auto-submitted/bulk/l f2f1dc0 →