Creates openspec/changes/email-loop-prevention/ with all artifacts: - proposal.md: Why (MAILER-DAEMON bounce loop incident), what changes (automated sender detection + message-count cap), capabilities affected - design.md: Two-layer defence rationale, detection signal table, decisions on channel/agent boundary split, 20-message cap, one-shot alert, CC-only routing; risks and trade-offs documented - specs/email-channel/spec.md: ADDED requirements for detect_automated_message(), is_automated/automated_reason message dict fields, no-reply guarantee - specs/registration-notifications/spec.md: ADDED requirements for notify_loop_escalation() — alert content, one-shot guarantee, CC-only routing, dev-mode guard - tasks.md: All 6 sections fully checked (implementation already complete) https://claude.ai/code/session_01KwvR5hDPjSuJg4kvw5b5e5
3.6 KiB
3.6 KiB
1. Automated Sender Detection (email_channel.py)
- 1.1 Add
_AUTOMATED_SENDER_REregex for known non-human local-parts (mailer-daemon, postmaster, noreply, no-reply, donotreply, bounce, …) - 1.2 Add
_AUTOMATED_SUBJECT_REregex for bounce/OOO subject patterns (German + English) - 1.3 Implement
detect_automated_message(raw_msg, from_addr) → (bool, str)checking all signals in priority order: sender pattern → Auto-Submitted → X-Auto-Response-Suppress → multipart/report → X-Loop → Precedence → subject - 1.4 Add
is_automatedandautomated_reasonfields to the dict returned byfetch_unread_messages()
2. Poll Loop Guard (main.py)
- 2.1 In
run_poll_loop(), checkmsg.get("is_automated")before callingagent.process_message() - 2.2 If automated: log a warning, call
agent.handle_automated_message(), andcontinue(skipsend_reply)
3. Agent — Automated Message Handler (agent/core.py)
- 3.1 Add
MAX_USER_MESSAGES = 20module-level constant - 3.2 Implement
handle_automated_message(sender_email, subject, reason, inbound_message_id)method - 3.3 In
handle_automated_message: load or create state; setloop_escalated = True; callnotify_loop_escalation()once; silently skip if already escalated; save state - 3.4 In
process_message(), after appending the user message, count user messages; if count >MAX_USER_MESSAGESand not escalated: setloop_escalated = True, callnotify_loop_escalation(), return"" - 3.5 If already escalated and over limit: silently save state and return
""
4. Conversation State (models/conversation.py)
- 4.1 Add
loop_escalated: bool = Falsefield toConversationState - 4.2 Include
loop_escalatedinto_dict() - 4.3 Restore
loop_escalatedinfrom_dict()with defaultFalsefor backward compatibility
5. Admin Notification (notifications/notifier.py)
- 5.1 Implement
notify_loop_escalation(sender_email, conversation_id, reason, message_count)method - 5.2 Route alert to
self._cc_emailsonly (not playgroup leaders) - 5.3 Subject:
[WARNUNG] Automatische E-Mail / Endlosschleife erkannt: {sender_email} - 5.4 Body: sender, conversation ID, message count, reason, call-to-action in German
- 5.5 Guard: if no CC emails configured, log warning and return without SMTP call
6. Tests
- 6.1
TestDetectAutomatedMessageBySender— mailer-daemon, postmaster, noreply, no-reply, donotreply, bounce; normal parent address not flagged - 6.2
TestDetectAutomatedMessageByHeaders— Auto-Submitted (auto-replied, auto-generated, no); X-Auto-Response-Suppress; multipart/report; X-Loop; Precedence bulk/junk; Precedence list not flagged - 6.3
TestDetectAutomatedMessageBySubject— Undelivered Mail, Mail Delivery Failed, Out of Office, Abwesenheitsnotiz, Automatische Antwort; case-insensitive; normal subject not flagged - 6.4
TestHandleAutomatedMessage— sets loop_escalated; calls notifier once; creates state when none exists; drops silently if already escalated; notifier failure does not propagate; inbound message ID stored - 6.5
TestProcessMessageCountCap— at limit still processes; over limit returns ""; sets loop_escalated; calls notifier once; no duplicate alert; notifier failure does not propagate; constant equals 20 - 6.6
TestNotifyLoopEscalation— sends to CC; [WARNUNG] in subject; sender in subject; reason in body; message count in body; no-CC guard; no-SMTP guard - 6.7
TestConversationStateLoopEscalated(test_models.py) — default False; to_dict includes key; True round-trip; from_dict backward compatibility