Sperrliste: automatisches Lernen aus dem Spam-Ordner #2

Merged
DasAoD merged 3 commits from feature/auto-blacklist-spam-senders into main 2026-08-14 16:30:08 +02:00
Owner

Closes #1

Was

Neue, per Account aktivierbare Sperrliste (⛔) unter „Filterregeln":

  • Manuell: Absenderadressen wie bei der bestehenden Whitelist eintragen/entfernen — werden bei jedem Lauf unabhängig vom Header-basierten Spam-Filter in den Spam-Zielordner verschoben. Whitelist hat immer Vorrang.
  • „Automatisch lernen" (Toggle, standardmäßig aus): der Dispatcher gleicht bei jedem Lauf die Sperrliste mit dem aktuellen Inhalt des Spam-Zielordners ab (Option A aus dem Issue: nur hinzufügen).
    • Zusätzlich Option B umgesetzt: wird eine zuvor im Spam-Ordner gesehene Mail von dort wieder in die INBOX verschoben, wird ihr Absender wieder entsperrt — außer er hat noch eine andere Mail im Spam-Ordner. Löschen oder Verschieben in einen anderen Ordner entsperrt nie.
    • Beim ersten Aktivieren wird der komplette aktuelle Spam-Ordner-Inhalt sofort übernommen.
    • Der bestehende Spam-Zielordner (aus dem Spam-Filter-Block) wird wiederverwendet, kein zusätzliches Ordnerfeld nötig.

Wie

  • lib/blacklist.php (neu): sync_spam_blacklist() verbindet sich per IMAP mit dem Spam-Zielordner, trackt den Inhalt Message-ID→Absender in einer per-User blacklist_state.json und diffed gegen den letzten Lauf.
  • lib/generate.php: gesperrte Absender werden im generierten Lua durchgesetzt (inbox:move_messages(...)), unabhängig vom „Automatisch lernen"-Schalter, sobald Einträge vorhanden sind.
  • cron/dispatcher.php: ruft den Abgleich nach jedem erfolgreichen imapfilter-Lauf auf und regeneriert Lua bei Änderungen; der Lockfile pro User wird jetzt bis nach dem Abgleich gehalten (Cron läuft jede Minute).
  • lib/atomic.php: neuer with_file_lock()-Helper, von public/api/rules.php und lib/blacklist.php gemeinsam genutzt, damit Web-UI-Speichern und Cron-Sync sich beim Schreiben von rules.json nicht gegenseitig überschreiben.
  • public/assets/app.js: neue Sperrlisten-Karte, analog zur bestehenden Whitelist-UI.
  • ANLEITUNG.md, README.md: Doku ergänzt.

Review

Zwei Runden code-reviewer + security-checker auf dem jeweils finalen Stand durchlaufen. Behoben u. a.:

  • Race Condition Web-UI ↔ Cron-Dispatcher beim Schreiben von rules.json (Lock + Merge statt Überschreiben)
  • Kein Locking-Verlust bei überlappenden Dispatcher-Läufen auf blacklist_state.json
  • IMAP-SEARCH-String-Injection beim Entsperr-Check entfernt (Vergleich jetzt rein über imap_fetch_overview() + PHP-Stringvergleich statt String-Interpolation in eine IMAP-Query)
  • Strikte FILTER_VALIDATE_EMAIL-Prüfung, bevor automatisch gelernte (spammer-kontrollierte) Absenderadressen in generiertes Lua wandern
  • Log-Injection über Absenderadressen/IMAP-Fehlertexte in dispatcher.log
  • Obergrenze für den INBOX-Scan beim Entsperr-Check, damit ein sehr großes Postfach keinen Dispatcher-Zyklus blockiert

Bekannte, akzeptierte Rand-Einschränkung: Speichert ein User über die Web-UI mit einem veralteten, lokal zwischengespeicherten Formularstand, während der Cron zwischenzeitlich neue Sperreinträge gelernt hat, kann der gelernte Eintrag kurzzeitig wieder verschwinden (kein Datenverlust/-korruption, self-heals beim nächsten Lern-Zyklus). Ein vollständiger 3-Wege-Merge im Frontend wäre für diesen Rand-Fall unverhältnismäßig aufwändig.

🤖 Generated with Claude Code

Closes #1 ## Was Neue, per Account aktivierbare Sperrliste (⛔) unter „Filterregeln": - **Manuell**: Absenderadressen wie bei der bestehenden Whitelist eintragen/entfernen — werden bei jedem Lauf unabhängig vom Header-basierten Spam-Filter in den Spam-Zielordner verschoben. Whitelist hat immer Vorrang. - **„Automatisch lernen"** (Toggle, standardmäßig aus): der Dispatcher gleicht bei jedem Lauf die Sperrliste mit dem aktuellen Inhalt des Spam-Zielordners ab (Option A aus dem Issue: nur hinzufügen). - Zusätzlich Option B umgesetzt: wird eine zuvor im Spam-Ordner gesehene Mail von dort wieder in die INBOX verschoben, wird ihr Absender wieder entsperrt — außer er hat noch eine andere Mail im Spam-Ordner. Löschen oder Verschieben in einen anderen Ordner entsperrt nie. - Beim ersten Aktivieren wird der komplette aktuelle Spam-Ordner-Inhalt sofort übernommen. - Der bestehende Spam-Zielordner (aus dem Spam-Filter-Block) wird wiederverwendet, kein zusätzliches Ordnerfeld nötig. ## Wie - `lib/blacklist.php` (neu): `sync_spam_blacklist()` verbindet sich per IMAP mit dem Spam-Zielordner, trackt den Inhalt Message-ID→Absender in einer per-User `blacklist_state.json` und diffed gegen den letzten Lauf. - `lib/generate.php`: gesperrte Absender werden im generierten Lua durchgesetzt (`inbox:move_messages(...)`), unabhängig vom „Automatisch lernen"-Schalter, sobald Einträge vorhanden sind. - `cron/dispatcher.php`: ruft den Abgleich nach jedem erfolgreichen imapfilter-Lauf auf und regeneriert Lua bei Änderungen; der Lockfile pro User wird jetzt bis nach dem Abgleich gehalten (Cron läuft jede Minute). - `lib/atomic.php`: neuer `with_file_lock()`-Helper, von `public/api/rules.php` und `lib/blacklist.php` gemeinsam genutzt, damit Web-UI-Speichern und Cron-Sync sich beim Schreiben von `rules.json` nicht gegenseitig überschreiben. - `public/assets/app.js`: neue Sperrlisten-Karte, analog zur bestehenden Whitelist-UI. - `ANLEITUNG.md`, `README.md`: Doku ergänzt. ## Review Zwei Runden `code-reviewer` + `security-checker` auf dem jeweils finalen Stand durchlaufen. Behoben u. a.: - Race Condition Web-UI ↔ Cron-Dispatcher beim Schreiben von `rules.json` (Lock + Merge statt Überschreiben) - Kein Locking-Verlust bei überlappenden Dispatcher-Läufen auf `blacklist_state.json` - IMAP-SEARCH-String-Injection beim Entsperr-Check entfernt (Vergleich jetzt rein über `imap_fetch_overview()` + PHP-Stringvergleich statt String-Interpolation in eine IMAP-Query) - Strikte `FILTER_VALIDATE_EMAIL`-Prüfung, bevor automatisch gelernte (spammer-kontrollierte) Absenderadressen in generiertes Lua wandern - Log-Injection über Absenderadressen/IMAP-Fehlertexte in `dispatcher.log` - Obergrenze für den INBOX-Scan beim Entsperr-Check, damit ein sehr großes Postfach keinen Dispatcher-Zyklus blockiert **Bekannte, akzeptierte Rand-Einschränkung:** Speichert ein User über die Web-UI mit einem veralteten, lokal zwischengespeicherten Formularstand, während der Cron zwischenzeitlich neue Sperreinträge gelernt hat, kann der gelernte Eintrag kurzzeitig wieder verschwinden (kein Datenverlust/-korruption, self-heals beim nächsten Lern-Zyklus). Ein vollständiger 3-Wege-Merge im Frontend wäre für diesen Rand-Fall unverhältnismäßig aufwändig. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- lib/blacklist.php: new sync_spam_blacklist() diffs the spam target folder
  against the previous run (by Message-ID) to auto-add new senders and
  auto-remove ones restored to INBOX, unless still present elsewhere in spam
- lib/generate.php: blacklist.senders are now enforced in generated Lua
  (moved to the spam target folder), independent of the header-based rule,
  whitelist always takes precedence
- cron/dispatcher.php: calls the sync after each successful imapfilter run
  and regenerates Lua when the blacklist changed
- lib/atomic.php: with_file_lock() helper; used by rules.php and
  blacklist.php to serialize concurrent read-modify-write on rules.json
  between the web UI and the cron dispatcher
- public/api/rules.php, public/assets/app.js: new "blacklist" section in
  rules.json (default + sanitization) and a UI card with a manual sender
  list plus an opt-in "auto-learn" toggle per account
- ANLEITUNG.md, README.md: document the new feature

Co-authored-by: Claude <claude@anthropic.com>
Follow-up to the security review of the spam blacklist feature (issue #1):

- lib/blacklist.php: only auto-add addresses that pass FILTER_VALIDATE_EMAIL;
  drop the raw IMAP SEARCH string-interpolation for the unblock check and
  compare Message-IDs via imap_fetch_overview() + PHP string equality instead
- lib/generate.php: lua_str() strips control characters (defense in depth for
  any string reaching generated Lua, not just blacklist senders)
- cron/dispatcher.php: strip CR/LF from logged sender addresses to prevent
  log-line forging

Co-authored-by: Claude <claude@anthropic.com>
- cron/dispatcher.php: hold the per-user lockfile through sync_spam_blacklist()
  instead of releasing it right after the imapfilter run, so an overlapping
  dispatcher invocation (cron runs every minute) can't race on the same
  user's blacklist_state.json; sanitize $sync['error'] (server-controlled
  IMAP error text) before logging it too
- lib/blacklist.php: report an error instead of failing silently when the
  lock-protected re-read of rules.json is corrupt; cap the INBOX scan used
  for the unblock check so a very large mailbox can't stall a dispatcher
  cycle (falls back to retrying next run, same as a connection failure)

Co-authored-by: Claude <claude@anthropic.com>
DasAoD merged commit eeca5f9dee into main 2026-08-14 16:30:08 +02:00
Sign in to join this conversation.
No description provided.