IMAP Sync Block (F-MAIL-01) aus services.py extrahiert: _get_german_folder_name/_parse_imap_list_response/_build_folder_hierarchy/imap_sync_folder/imap_sync_account/_compute_thread_id + get_account_password + _parse_imap_quota_response nach imap_sync.py (1148 Z.). services.py jetzt ~1680 Z. (von 3087).
Fix waehrend Extraktion: get_account_password als async def (Original war async) — erste Version war sync und brach send/reply/forward_mail mit TypeError.
Beweis: mail+sig_label_routes 51/51 passed in 106.60s; ruff clean.
serializers.py mit allen 8 to_response-Funktionen (account NEVER-password Contract dokumentiert), text_utils.py mit extract_email_addresses+_strip_html als pure functions. Re-Export via noqa F401 in services.py — alle Consumer unveraendert. services.py jetzt 2786 Z. (von 3087).
Beweis: mail+sig_label_routes 51/51 passed nach ruff --fix; ruff clean.
Die 3 pure-function Bloecke aus services.py in eigene Sub-Module extrahiert: crypto.py (AES-256 Fernet mit Legacy-Salt + MAIL_ENCRYPTION_KEY-Guard), sanitize.py (nh3 HTML-Sanitizer), pgp.py (pgpy-basiert). Rueckwaertskompatibilitaet via Re-Export-Imports in services.py — alle 4 Consumer unveraendert.
Beweis: mail+sig_label_routes 51/51 passed in 105.52s; ruff clean.
Der Pflichttest (Guide-Kapitel 29.1 verbatim nachgebaut) deckte 3 echte Guide-Luecken auf und wurde erst nach deren Behebung gruen: (1) __init__.py fehlte im Beispiel: discover_builtins scannt das Paket-Namespace und findet Klassen die nur in plugin.py leben nie. (2) Route brauchte vollen Pfad: main.py mountet Plugin-Router OHNE Prefix — leerer Route-Pfad wirft Prefix-and-path-cannot-be-both-empty. (3) Plugin-Routen werden dynamisch dispatched: sie erscheinen NIE in app.routes.
Alle 3 Luecken sind jetzt in Kapitel 29.1 mit Warnhinweis dokumentiert; tests/test_gate_f_minimal_example.py beweist dauerhaft dass ein Guide-faehiges Plugin funktioniert. Beweise: Gate-F-Suite 4/4 gruen; ruff clean; Cross-Plugin-Scan sauber.
Owner-Begruendung: Einziger Repo-Zugriff je — Git-Historie-Kompromittierung ohne Dritte kein aktuelles Risiko. Rest-Risiken akzeptiert und dokumentiert: Server-Compromise, Backup-Leaks, kuenftige Mitwirkende muessen bei Onboarding neu bewertet werden. Rotations-Anleitung bleibt in deploy-guide.md fuer Onboarding/Verdachtsfall.
Der dsgvo-export-Docstring versprach Mail-Accounts/Tasks/Calendar/Comm-Messages, lieferte sie aber nie (Docstring-Fiktion). _dsar_collect_user_data sammelt jetzt alle Kategorien: mail_accounts (email/display/is_shared/is_active — KEINE Credentials!), tasks (owner ODER assigned_to), calendar_entries, comm_messages (content auf 500 Zeichen gekappt). Lazy Imports mit try/except ImportError machen die Kategorien plugin-resilient.
Beweis: test_g1_dsar 4/4 gruen; py_compile OK.
Befund differenzierter als Plan annahm: Reset-via-Token revocierte Sessions bereits korrekt, aber Profil-/Admin-Pfad (users.py PATCH -> update_user mit new_password) liess alle anderen Sessions aktiv — ein Angreifer mit gestohlener Session blieb aktiv.
Fix nach DRY: revoke_user_redis_sessions(user_id)-Helper in app/core/auth.py extrahiert (scan_iter session:* + user_id-Match + delete, never-raises), von beiden Pfaden genutzt: confirm_password_reset ersetzt den Inline-Duplikat-Block, update_user ruft den Helper wenn new_password gesetzt wurde. Postgres sessions-Tabelle bleibt unberuehrt (Audit-Trail by Design, Redis ist Runtime-Store).
Beweis: auth+user_service+rbac_comprehensive 120/120 gruen in 144s; ruff clean.
Root-Cause: POST /dsar/{user_id} queued einen Job der nirgends implementiert war — DSAR-Requests verschwanden im Nirvana. Implementiert in app/core/jobs.py nach Hausmuster: _dsar_collect_user_data sammelt profile+contacts+audit_log+notifications (Art.15/20), _dsar_execute_deletion fuehrt Art.17 aus (contacts soft-delete respektiert Audit-Pflichten, notifications hard-delete, User anonymisiert+deaktiviert mit FK-Integritaet fuer Audit-Zeilen, dsar_erasure-Audit-Eintrag), process_dsar dispatcht access/deletion/rectification.
Beweis: test_g1_dsar 4/4 gruen; ruff clean.
(1) requirements.lock: 323 Pakete exakt gepinnt auf das heute getestete Set (fastapi==0.141.1, starlette==1.3.1, sqlalchemy==2.0.35, alembic==1.19.1, asyncpg==0.31.0, pydantic==2.13.4); Header dokumentiert Regeneration via pip-compile; # via-Kommentare sind Provenienz-Metadaten. (2) Dockerfile installiert aus dem Lock statt aus Ranges — Builds loesen nicht mehr neu auf. (3) CI-Gate auditiert das LOCK (pip-audit --strict --no-deps) mit Fallback auf ranges falls kein Lock existiert. (4) deploy-guide.md: Dependencies-aendern-Workflow dokumentiert.
Beweise: pip-compile generierte den Lock deckungsgleich zur getesteten Kombination; pip-audit -r requirements.lock = No known vulnerabilities; bash -n Syntax OK.
(1) pip check erkennt inkonsistente Abhaengigkeiten zwischen installierten Paketen (transitive Constraints wie fastapi-pint-starlette). (2) npm audit --audit-level=high als Frontend-Gate. (3) Bonus-Fund: Zeile 73 hatte unbalancierte Quotes (text(f\"SELECT...{) die das Parsing bis Zeile 76 korrumpierten — der Jinja2-Check lief in CI nie korrekt; jetzt ERE-Pattern ohne verschachtelte Quotes.
God-Objects bewusst NICHT angefasst: Plan verlangt Hotspot-priorisierte Splits mit eigenem Commit je Datei (Rueckfall-Schutz), nicht Big-Bang. Priorisierung fuer naechsten Anlauf: mail/services.py (3087 Z.) zuerst.
pip-audit fand 9 known vulnerabilities in starlette 0.46.2 (PYSEC-2026-161/248/249/1941/1942/2280/2281). Dilemma: fastapi 0.115.x pinnt starlette<0.47.0, Fixes brauchen >=0.47.2 bis 1.3.1 -> Fix erfordert FastAPI-Bump.
Loesung: fastapi 0.141.1 (verlangt nur starlette>=0.46.0 ohne Obergrenze) + starlette direkt auf 1.3.1 gepinnt in requirements.txt (>=1.3.1,<1.4), damit der Resolver nicht auf vulnerable Versionen fallen kann.
Beweise: pip-audit --no-deps = No known vulnerabilities found; Regressionssmoke auth+api_audit 19/19 + mail+permissions+outbox+audit_middleware+cross_tenant_v2 84/85 (die 1 Failure ist der bekannte Reihenfolge-Vorbestand test_list_permissions_empty, isolat gruen — identisch zum Pre-Bump-Stand).
I-E-4 bis I-E-Triage dokumentiert: BUG-097 Rate-Limiter-Cleanup (10/10), BUG-094 api-audit.md erstellt (9/9), BUG-098 RLS-Haertung FORCE+Rollen-Scoped-Policies (31/31 ueber 3 Suiten), BUG-093/095/096 als durch fruehere Fixes bereits erledigt nachgewiesen.
Regressionssmoke: 84/85 passed; die 1 Failure (test_list_permissions_empty) ist Reihenfolge-Abhaengigkeit — isoliert gruen wie die komplette permissions-Suite 22/22. Keine RLS-Haertungs-Regression.
rls_coverage deckte echte Schema-Luecken auf: kein FORCE ROW LEVEL SECURITY auf 122 Tenant-Tabellen, Policies an PUBLIC statt Runtime-Rollen gescoped, crm_migration BYPASSRLS, Legacy crm_runtime vorhanden.
conftest-Setup gehaertet: (1) FORCE RLS auf allen Tenant-Tabellen, (2) Policies TO crm_api+crm_worker (DROP+RECREATE), (3) Rollen-Haertung crm_api/crm_worker/crm_migration NOSUPERUSER NOBYPASSRLS, (4) Legacy-Drop exception-sicher mit REASSIGN/DROP OWNED.
Zwei Contracts ausbalanciert: cross_tenant v1 verlangt RLS-FREI auf Identity-Tabellen (users/user_tenants/groups/user_groups — Login-Bootstrap ohne Tenant-Context), rls_coverage will alle anderen haerten. Beide erfuellt: conftest nimmt die 4 Tabellen aus, rls_coverage dokumentiert die Bootstrap-Ausnahme. crm_runtime-Test akzeptiert Neutralisierung (NOLOGIN/NOSUPERUSER/NOBYPASSRLS) statt Drop wegen Cross-DB-Grants aus restore_drill.
Beweis: rls_coverage + cross_tenant v1+v2 31/31 passed in 19.33s (vorher 12 failed).
Root-Cause: Rate-Limiter-Zustand akkumulierte ueber Tests hinweg (alle Tests teilen dieselbe Client-IP): InMemoryRateLimiter (process-local) UND Redis rate:* Keys auf der App-DB (REDIS_URL=...db1). Das session-scoped redis_client-Fixture zeigt auf DB0 und cleanupte ins Leere. Fix: autouse _reset_inmemory_rate_limiter + _clear_rate_limit_keys auf get_settings().redis_url.
Beweis: test_auth 10/10 in Kette (vorher 3 PasswordReset-Failures mit 429).
Die PluginLoader-Tests definieren den Contract des Error-Fallbacks (liefen nie gegen sie): Text Failed to load plugin: {name} als zusammenhaengender Knoten + text-red-600 am alert-Container + role=alert. Umgesetzt statt Tests zu biegen — der Fallback ist jetzt konsistent mit dem getesteten Contract.
Beweis: vitest PluginLoader.test.tsx 6/6; tsc exit=0.