[object Object]

← back to Claude Mail

gate: add RFC 3834 mail-loop breakers (auto-submitted/bulk/list/no-reply/own-address) before allowlist

f2f1dc0b5b04b7ba6f5ed7774023b3a561430572 · 2026-06-25 10:17:05 -0700 · Steve

Files touched

Diff

commit f2f1dc0b5b04b7ba6f5ed7774023b3a561430572
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 10:17:05 2026 -0700

    gate: add RFC 3834 mail-loop breakers (auto-submitted/bulk/list/no-reply/own-address) before allowlist
---
 lib.js | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/lib.js b/lib.js
index 7376cc0..80ef6a0 100644
--- a/lib.js
+++ b/lib.js
@@ -33,6 +33,32 @@ const cfg = {
 function gate(parsed) {
   const from = (parsed.from?.value?.[0]?.address || '').toLowerCase();
   if (!from) return { ok: false, reason: 'no From address' };
+
+  // Mail-loop breakers (RFC 3834) — these run BEFORE the allowlist so an
+  // auto-responder, bounce, or our own outbound copy can never start a
+  // ping-pong even if its From were somehow allowlisted. Purely additive:
+  // legitimate human mail carries none of these headers.
+  if (cfg.user && from === cfg.user.toLowerCase()) {
+    return { ok: false, reason: 'own-address mail (loop guard)' };
+  }
+  const autoSubmitted = headerStr(parsed, 'auto-submitted').toLowerCase();
+  if (autoSubmitted && autoSubmitted !== 'no') {
+    return { ok: false, reason: `auto-submitted mail (loop guard): ${autoSubmitted}` };
+  }
+  const precedence = headerStr(parsed, 'precedence').toLowerCase();
+  if (/\b(bulk|list|junk|auto_reply)\b/.test(precedence)) {
+    return { ok: false, reason: `bulk/auto precedence (loop guard): ${precedence}` };
+  }
+  // Vacation/auto-reply and list-management headers (Gmail/Outlook/Mailman).
+  for (const h of ['x-autoreply', 'x-autorespond', 'x-auto-response-suppress',
+                   'list-id', 'list-unsubscribe', 'feedback-id']) {
+    if (headerStr(parsed, h)) return { ok: false, reason: `auto/list header (loop guard): ${h}` };
+  }
+  const localFrom = from.split('@')[0];
+  if (/^(mailer-daemon|postmaster|no-?reply|do-?not-?reply|bounce)/.test(localFrom)) {
+    return { ok: false, reason: `system/no-reply sender (loop guard): ${from}` };
+  }
+
   if (!cfg.allowed.includes(from)) return { ok: false, reason: `sender not allowlisted: ${from}` };
 
   const subject = (parsed.subject || '');

← 2afc049 mailer: archive every reply to IMAP Sent (best-effort) + rea  ·  back to Claude Mail  ·  poller: guard against invoking claude on an empty prompt (em 1a8facd →