This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/docs/klausur-modul/MAIL-RBAC-DEVELOPER-SPECIFICATION.md
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

66 KiB

Mail-RBAC Developer Specification

Version: 1.0.0 Datum: 2026-01-10 Status: Entwicklungsspezifikation Autor: BreakPilot Development Team


1. Executive Summary

Dieses Dokument spezifiziert die technische Implementierung eines DSGVO-konformen Mail-Systems mit rollenbasierter Zugriffskontrolle (RBAC) und Mitarbeiter-Anonymisierungsfunktion.

1.1 Projektziele

Ziel Beschreibung Priorität
Rollenbasierte E-Mail Funktionale Mailboxen statt personengebundener Adressen P0
DSGVO-Anonymisierung Vollständige Anonymisierung bei Mitarbeiter-Ausscheiden P0
Audit-Trail Lückenlose Nachverfolgbarkeit aller E-Mail-Aktionen P0
Kalender-Integration CalDAV mit Jitsi-Meeting-Links P1
Groupware-UI Webmail-Interface für Mitarbeiter P1

1.2 Nicht-Ziele (Out of Scope)

  • Exchange/Outlook-Protokoll-Kompatibilität
  • Mobile Push-Notifications (Phase 2)
  • Externe E-Mail-Domain-Routing (Phase 2)

2. DSGVO-Konformität

2.1 Rechtliche Grundlagen

Artikel Anforderung Umsetzung
Art. 5 DSGVO Datenminimierung Nur notwendige Metadaten speichern
Art. 17 DSGVO Recht auf Löschung Anonymisierungs-Workflow
Art. 20 DSGVO Datenportabilität MBOX/EML Export
Art. 30 DSGVO Verarbeitungsverzeichnis Automatische Protokollierung
Art. 32 DSGVO Sicherheit Verschlüsselung at-rest & in-transit

2.2 Datenschutz-Prinzipien

┌─────────────────────────────────────────────────────────────┐
│                   DATENSCHUTZ-BY-DESIGN                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. TRENNUNG: Person ≠ Rolle ≠ Mailbox                     │
│     ├── Person: Max Mustermann (anonymisierbar)            │
│     ├── Rolle: klassenlehrer (persistent)                  │
│     └── Mailbox: klassenlehrer.5a@... (rollengebunden)     │
│                                                             │
│  2. MINIMIERUNG: Nur speichern was nötig                   │
│     ├── E-Mail-Inhalte: Verschlüsselt                      │
│     ├── Metadaten: Nur Subject-Hash, keine Namen           │
│     └── Audit: Rollenbasiert, nicht personenbasiert        │
│                                                             │
│  3. LÖSCHBARKEIT: Jederzeit anonymisierbar                 │
│     ├── Personendaten: Pseudonymisierung                   │
│     ├── E-Mail-Archive: Header-Anonymisierung              │
│     └── Audit-Trail: Bleibt für Nachvollziehbarkeit        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.3 Aufbewahrungsfristen

Datentyp Frist Rechtsgrundlage
E-Mail-Inhalte 10 Jahre § 147 AO (Geschäftskorrespondenz)
Audit-Logs 10 Jahre § 257 HGB
Personenbezogene Daten Bis Löschungsantrag Art. 17 DSGVO
Anonymisierte Daten Unbegrenzt Keine personenbezogenen Daten

3. Systemarchitektur

3.1 Komponenten-Übersicht

┌──────────────────────────────────────────────────────────────────────┐
│                        BREAKPILOT MAIL-RBAC                          │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                    PRESENTATION LAYER                       │     │
│  ├────────────────────────────────────────────────────────────┤     │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │     │
│  │  │  Admin UI    │  │  Webmail UI  │  │ Calendar UI  │      │     │
│  │  │  (Next.js)   │  │  (SOGo)      │  │ (SOGo)       │      │     │
│  │  │  :3000       │  │  :20000      │  │  :20000      │      │     │
│  │  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │     │
│  └─────────┼─────────────────┼─────────────────┼──────────────┘     │
│            │                 │                 │                     │
│  ┌─────────┴─────────────────┴─────────────────┴──────────────┐     │
│  │                    APPLICATION LAYER                        │     │
│  ├────────────────────────────────────────────────────────────┤     │
│  │                                                            │     │
│  │  ┌─────────────────────────────────────────────────────┐  │     │
│  │  │              RBAC-MAIL-BRIDGE (Python)              │  │     │
│  │  │              Port: 8087                              │  │     │
│  │  ├─────────────────────────────────────────────────────┤  │     │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │  │     │
│  │  │  │ Mailbox     │  │ Anonymizer  │  │ Audit       │  │  │     │
│  │  │  │ Manager     │  │ Service     │  │ Logger      │  │  │     │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘  │  │     │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │  │     │
│  │  │  │ RBAC        │  │ Calendar    │  │ Jitsi       │  │  │     │
│  │  │  │ Sync        │  │ Bridge      │  │ Integrator  │  │  │     │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘  │  │     │
│  │  └─────────────────────────────────────────────────────┘  │     │
│  │                           │                                │     │
│  │  ┌────────────────────────┼────────────────────────────┐  │     │
│  │  │                        │                            │  │     │
│  │  │  ┌──────────────┐  ┌───┴────────┐  ┌─────────────┐ │  │     │
│  │  │  │ Stalwart     │  │ Existing   │  │ MinIO       │ │  │     │
│  │  │  │ Mail Server  │  │ RBAC API   │  │ Storage     │ │  │     │
│  │  │  │ :25/:143/:993│  │ :8000      │  │ :9000       │ │  │     │
│  │  │  └──────────────┘  └────────────┘  └─────────────┘ │  │     │
│  │  └────────────────────────────────────────────────────┘  │     │
│  └────────────────────────────────────────────────────────────┘     │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                      DATA LAYER                             │     │
│  ├────────────────────────────────────────────────────────────┤     │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │     │
│  │  │ PostgreSQL   │  │ Stalwart    │  │ MinIO        │      │     │
│  │  │ (RBAC Data)  │  │ (Mail Data) │  │ (Attachments)│      │     │
│  │  │ :5432        │  │ Internal    │  │ :9000        │      │     │
│  │  └──────────────┘  └──────────────┘  └──────────────┘      │     │
│  └────────────────────────────────────────────────────────────┘     │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

3.2 Datenfluss

┌─────────────────────────────────────────────────────────────────┐
│                    E-MAIL SENDEN (OUTBOUND)                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Lehrer klickt "Senden" in Webmail                          │
│     │                                                           │
│     ▼                                                           │
│  2. SOGo → Stalwart SMTP (Port 25)                             │
│     │   ┌──────────────────────────────────────┐               │
│     │   │ From: klassenlehrer.5a@schule.bp.app │               │
│     │   │ (Rollenbasiert, nicht personenbasiert)│               │
│     │   └──────────────────────────────────────┘               │
│     ▼                                                           │
│  3. Stalwart → RBAC-Mail-Bridge (Milter Hook)                  │
│     │   ┌──────────────────────────────────────┐               │
│     │   │ - Validiere Absender-Berechtigung    │               │
│     │   │ - Logge Audit-Event                  │               │
│     │   │ - Füge X-BP-Role Header hinzu        │               │
│     │   └──────────────────────────────────────┘               │
│     ▼                                                           │
│  4. Stalwart → Internet (MX Lookup)                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    E-MAIL EMPFANGEN (INBOUND)                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Internet → Stalwart SMTP (Port 25)                         │
│     │                                                           │
│     ▼                                                           │
│  2. Stalwart → RBAC-Mail-Bridge (Milter Hook)                  │
│     │   ┌──────────────────────────────────────┐               │
│     │   │ - Identifiziere Ziel-Mailbox         │               │
│     │   │ - Lookup aktuelle Rollenzuweisung    │               │
│     │   │ - Logge Audit-Event                  │               │
│     │   └──────────────────────────────────────┘               │
│     ▼                                                           │
│  3. Stalwart → Mailbox Store (IMAP)                            │
│     │                                                           │
│     ▼                                                           │
│  4. SOGo zeigt E-Mail an (für zugewiesene Person)              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4. Datenmodell

4.1 Entity-Relationship-Diagramm

┌─────────────────────────────────────────────────────────────────────┐
│                        MAIL-RBAC DATENMODELL                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌───────────────┐       ┌───────────────────┐                     │
│  │    users      │       │ functional_       │                     │
│  │ (bestehend)   │       │ mailboxes         │                     │
│  ├───────────────┤       ├───────────────────┤                     │
│  │ id (PK)       │       │ id (PK)           │                     │
│  │ email         │       │ role_key (FK)     │──────┐              │
│  │ name          │       │ email_address     │      │              │
│  │ is_active     │       │ display_name      │      │              │
│  │ anonymized_at │       │ tenant_id (FK)    │      │              │
│  └───────┬───────┘       │ is_active         │      │              │
│          │               └─────────┬─────────┘      │              │
│          │                         │                │              │
│          │    ┌────────────────────┘                │              │
│          │    │                                     │              │
│          ▼    ▼                                     ▼              │
│  ┌───────────────────┐               ┌───────────────────┐        │
│  │ mailbox_          │               │ roles             │        │
│  │ assignments       │               │ (bestehend)       │        │
│  ├───────────────────┤               ├───────────────────┤        │
│  │ id (PK)           │               │ role_key (PK)     │        │
│  │ mailbox_id (FK)   │───────────────│ display_name      │        │
│  │ user_id (FK)      │               │ category          │        │
│  │ valid_from        │               └───────────────────┘        │
│  │ valid_to          │                                            │
│  │ assigned_by (FK)  │                                            │
│  │ revoked_at        │                                            │
│  └─────────┬─────────┘                                            │
│            │                                                       │
│            │     ┌────────────────────────────────────────────┐   │
│            │     │                                            │   │
│            ▼     ▼                                            │   │
│  ┌───────────────────┐         ┌───────────────────┐         │   │
│  │ email_audit_      │         │ anonymization_    │         │   │
│  │ trail             │         │ log               │         │   │
│  ├───────────────────┤         ├───────────────────┤         │   │
│  │ id (PK)           │         │ id (PK)           │         │   │
│  │ mailbox_id (FK)   │         │ entity_type       │         │   │
│  │ direction         │         │ entity_id         │         │   │
│  │ subject_hash      │         │ anonymization_type│         │   │
│  │ timestamp         │         │ fields_affected   │         │   │
│  │ external_domain   │         │ reason            │         │   │
│  │ role_key          │         │ performed_by (FK) │◄────────┘   │
│  └───────────────────┘         │ performed_at      │             │
│                                │ legal_basis       │             │
│                                └───────────────────┘             │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

4.2 SQL Schema

-- ============================================================
-- MAIL-RBAC SCHEMA
-- Version: 1.0.0
-- ============================================================

-- Erweiterung der bestehenden users-Tabelle
ALTER TABLE users ADD COLUMN IF NOT EXISTS
    anonymized_at TIMESTAMP;
ALTER TABLE users ADD COLUMN IF NOT EXISTS
    anonymization_token VARCHAR(64);

-- Funktionale Mailboxen (rollengebunden)
CREATE TABLE functional_mailboxes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Rollen-Verknüpfung
    role_key VARCHAR(100) NOT NULL,
    email_address VARCHAR(255) UNIQUE NOT NULL,
    display_name VARCHAR(255) NOT NULL,

    -- Tenant/Schule
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    resource_type VARCHAR(50) DEFAULT 'class',
    resource_id VARCHAR(100),

    -- Stalwart Mailbox ID (nach Erstellung)
    stalwart_mailbox_id VARCHAR(255),

    -- Status
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW(),
    created_by UUID REFERENCES users(id),

    -- Indizes
    CONSTRAINT fk_tenant FOREIGN KEY (tenant_id)
        REFERENCES tenants(id) ON DELETE CASCADE
);

CREATE INDEX idx_fm_role ON functional_mailboxes(role_key);
CREATE INDEX idx_fm_tenant ON functional_mailboxes(tenant_id);
CREATE INDEX idx_fm_email ON functional_mailboxes(email_address);

-- Mailbox-Zuweisungen (Person ↔ Funktionale Mailbox)
CREATE TABLE mailbox_assignments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Verknüpfungen
    mailbox_id UUID NOT NULL REFERENCES functional_mailboxes(id),
    user_id UUID NOT NULL REFERENCES users(id),

    -- Gültigkeitszeitraum
    valid_from TIMESTAMP DEFAULT NOW(),
    valid_to TIMESTAMP,

    -- Audit
    assigned_by UUID REFERENCES users(id),
    assigned_at TIMESTAMP DEFAULT NOW(),
    revoked_by UUID REFERENCES users(id),
    revoked_at TIMESTAMP,
    revocation_reason VARCHAR(255),

    -- Constraints
    CONSTRAINT unique_active_mailbox_assignment
        EXCLUDE USING gist (
            mailbox_id WITH =,
            tsrange(valid_from, COALESCE(valid_to, 'infinity'::timestamp)) WITH &&
        ) WHERE (revoked_at IS NULL)
);

CREATE INDEX idx_ma_mailbox ON mailbox_assignments(mailbox_id);
CREATE INDEX idx_ma_user ON mailbox_assignments(user_id);
CREATE INDEX idx_ma_active ON mailbox_assignments(revoked_at) WHERE revoked_at IS NULL;

-- E-Mail Audit Trail (DSGVO-konform, ohne personenbezogene Daten)
CREATE TABLE email_audit_trail (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Mailbox-Referenz (nicht Person!)
    mailbox_id UUID REFERENCES functional_mailboxes(id),

    -- E-Mail-Metadaten (anonymisiert)
    direction VARCHAR(10) NOT NULL CHECK (direction IN ('inbound', 'outbound')),
    message_id_hash VARCHAR(64),  -- SHA-256 der Message-ID
    subject_hash VARCHAR(64),      -- SHA-256 des Betreffs
    timestamp TIMESTAMP NOT NULL,

    -- Externe Partei (nur Domain, nicht volle Adresse)
    external_party_domain VARCHAR(255),

    -- Rolle zum Zeitpunkt (für Nachvollziehbarkeit)
    role_key VARCHAR(100) NOT NULL,

    -- Keine personenbezogenen Daten!
    -- Die Person ist nur über mailbox_assignments nachvollziehbar

    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_eat_mailbox ON email_audit_trail(mailbox_id);
CREATE INDEX idx_eat_timestamp ON email_audit_trail(timestamp);
CREATE INDEX idx_eat_role ON email_audit_trail(role_key);

-- Anonymisierungsprotokoll
CREATE TABLE anonymization_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Was wurde anonymisiert
    entity_type VARCHAR(50) NOT NULL CHECK (entity_type IN (
        'user', 'email_account', 'email_content', 'calendar_event'
    )),
    entity_id UUID NOT NULL,

    -- Wie
    anonymization_type VARCHAR(50) NOT NULL CHECK (anonymization_type IN (
        'pseudonymization', 'deletion', 'header_anonymization'
    )),
    fields_affected JSONB NOT NULL,

    -- Warum
    reason VARCHAR(100) NOT NULL CHECK (reason IN (
        'employee_departure', 'dsgvo_request', 'retention_expiry', 'manual'
    )),

    -- Audit
    performed_by UUID NOT NULL REFERENCES users(id),
    performed_at TIMESTAMP DEFAULT NOW(),

    -- Rechtliche Dokumentation
    legal_basis VARCHAR(255),
    retention_period_days INTEGER,
    confirmation_token VARCHAR(64)
);

CREATE INDEX idx_al_entity ON anonymization_log(entity_type, entity_id);
CREATE INDEX idx_al_performed ON anonymization_log(performed_at);

-- Kalender-Events mit Jitsi-Integration
CREATE TABLE calendar_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Event-Daten
    title VARCHAR(255) NOT NULL,
    description TEXT,
    start_time TIMESTAMP NOT NULL,
    end_time TIMESTAMP NOT NULL,
    location VARCHAR(255),

    -- Jitsi-Integration
    jitsi_room_id VARCHAR(100),
    jitsi_url TEXT,

    -- Organisator (Mailbox, nicht Person)
    organizer_mailbox_id UUID REFERENCES functional_mailboxes(id),

    -- CalDAV-Synchronisation
    caldav_uid VARCHAR(255) UNIQUE,
    caldav_etag VARCHAR(100),

    -- Status
    is_cancelled BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_ce_organizer ON calendar_events(organizer_mailbox_id);
CREATE INDEX idx_ce_time ON calendar_events(start_time, end_time);

-- Kalender-Teilnehmer
CREATE TABLE calendar_attendees (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id UUID NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
    mailbox_id UUID REFERENCES functional_mailboxes(id),
    external_email VARCHAR(255),
    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN (
        'pending', 'accepted', 'declined', 'tentative'
    )),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_ca_event ON calendar_attendees(event_id);

5. API-Spezifikation

5.1 REST API Endpoints

# ============================================================
# MAIL-RBAC API SPECIFICATION
# OpenAPI 3.0
# ============================================================

openapi: 3.0.3
info:
  title: BreakPilot Mail-RBAC API
  version: 1.0.0
  description: DSGVO-konformes Mail-System mit Rollen-Integration

servers:
  - url: http://localhost:8087/api/v1
    description: Development

paths:
  # ==================== MAILBOXEN ====================

  /mailboxes:
    get:
      summary: Liste aller funktionalen Mailboxen
      tags: [Mailboxes]
      parameters:
        - name: tenant_id
          in: query
          schema:
            type: string
            format: uuid
        - name: role_key
          in: query
          schema:
            type: string
      responses:
        200:
          description: Mailbox-Liste
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FunctionalMailbox'

    post:
      summary: Erstellt eine neue funktionale Mailbox
      tags: [Mailboxes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMailboxRequest'
      responses:
        201:
          description: Mailbox erstellt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FunctionalMailbox'

  /mailboxes/{mailbox_id}:
    get:
      summary: Details einer Mailbox
      tags: [Mailboxes]
      parameters:
        - name: mailbox_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        200:
          description: Mailbox-Details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FunctionalMailboxDetail'

  /mailboxes/{mailbox_id}/assign:
    post:
      summary: Weist Mailbox einem Benutzer zu
      tags: [Mailboxes]
      parameters:
        - name: mailbox_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignMailboxRequest'
      responses:
        201:
          description: Zuweisung erstellt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MailboxAssignment'

  /mailboxes/{mailbox_id}/revoke:
    post:
      summary: Widerruft Mailbox-Zuweisung
      tags: [Mailboxes]
      parameters:
        - name: mailbox_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user_id:
                  type: string
                  format: uuid
                reason:
                  type: string
      responses:
        200:
          description: Zuweisung widerrufen

  # ==================== ANONYMISIERUNG ====================

  /users/{user_id}/anonymize:
    post:
      summary: Anonymisiert einen Benutzer vollständig
      tags: [Anonymization]
      description: |
        Führt folgende Schritte aus:
        1. Widerruft alle Mailbox-Zuweisungen
        2. Anonymisiert Benutzerdaten
        3. Anonymisiert E-Mail-Header
        4. Erstellt Audit-Log
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnonymizationRequest'
      responses:
        200:
          description: Anonymisierung erfolgreich
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnonymizationResult'

  /users/{user_id}/anonymize/preview:
    get:
      summary: Vorschau der Anonymisierung
      tags: [Anonymization]
      description: Zeigt was anonymisiert werden würde
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        200:
          description: Anonymisierungs-Vorschau
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnonymizationPreview'

  # ==================== AUDIT ====================

  /audit/logs:
    get:
      summary: Audit-Log abrufen
      tags: [Audit]
      parameters:
        - name: mailbox_id
          in: query
          schema:
            type: string
            format: uuid
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
      responses:
        200:
          description: Audit-Einträge
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AuditLogEntry'

  /audit/anonymizations:
    get:
      summary: Anonymisierungsprotokoll
      tags: [Audit]
      responses:
        200:
          description: Anonymisierungen
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AnonymizationLogEntry'

  /audit/export:
    get:
      summary: DSGVO-Export
      tags: [Audit]
      parameters:
        - name: user_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        200:
          description: DSGVO-Datenexport
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GDPRExport'

  # ==================== KALENDER ====================

  /calendar/events:
    post:
      summary: Erstellt einen Kalender-Eintrag mit optionalem Jitsi-Meeting
      tags: [Calendar]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCalendarEventRequest'
      responses:
        201:
          description: Event erstellt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalendarEvent'

components:
  schemas:
    FunctionalMailbox:
      type: object
      properties:
        id:
          type: string
          format: uuid
        role_key:
          type: string
          example: "klassenlehrer"
        email_address:
          type: string
          format: email
          example: "klassenlehrer.5a@schule.breakpilot.app"
        display_name:
          type: string
          example: "Klassenlehrer 5a"
        is_active:
          type: boolean
        current_assignee:
          $ref: '#/components/schemas/UserSummary'

    CreateMailboxRequest:
      type: object
      required:
        - role_key
        - email_address
        - display_name
        - tenant_id
      properties:
        role_key:
          type: string
        email_address:
          type: string
          format: email
        display_name:
          type: string
        tenant_id:
          type: string
          format: uuid
        resource_type:
          type: string
          enum: [class, department, function]
        resource_id:
          type: string

    AnonymizationRequest:
      type: object
      required:
        - reason
        - confirmed
      properties:
        reason:
          type: string
          enum: [employee_departure, dsgvo_request, manual]
        confirmed:
          type: boolean
          description: Muss true sein für Durchführung
        preserve_audit_trail:
          type: boolean
          default: true
        delete_email_content:
          type: boolean
          default: false

    AnonymizationResult:
      type: object
      properties:
        success:
          type: boolean
        anonymization_id:
          type: string
          format: uuid
        affected_mailboxes:
          type: integer
        affected_emails:
          type: integer
        audit_log_entry_id:
          type: string
          format: uuid

5.2 Internal Events (Message Queue)

# Für zukünftige Skalierung: Event-basierte Kommunikation

class MailRBACEvent:
    """Base Event für Mail-RBAC System"""
    event_type: str
    timestamp: datetime
    correlation_id: str

class MailboxAssignedEvent(MailRBACEvent):
    """Wird ausgelöst wenn eine Mailbox zugewiesen wird"""
    event_type = "mailbox.assigned"
    mailbox_id: str
    user_id: str
    role_key: str

class MailboxRevokedEvent(MailRBACEvent):
    """Wird ausgelöst wenn eine Mailbox-Zuweisung widerrufen wird"""
    event_type = "mailbox.revoked"
    mailbox_id: str
    user_id: str
    reason: str

class UserAnonymizedEvent(MailRBACEvent):
    """Wird ausgelöst wenn ein Benutzer anonymisiert wird"""
    event_type = "user.anonymized"
    user_id: str
    anonymization_id: str
    affected_mailboxes: List[str]

class EmailSentEvent(MailRBACEvent):
    """Wird ausgelöst wenn eine E-Mail gesendet wird"""
    event_type = "email.sent"
    mailbox_id: str
    message_id_hash: str
    external_domain: str

class CalendarEventCreatedEvent(MailRBACEvent):
    """Wird ausgelöst wenn ein Kalender-Event erstellt wird"""
    event_type = "calendar.created"
    event_id: str
    organizer_mailbox_id: str
    has_jitsi: bool

6. Implementierungsdetails

6.1 Anonymisierungs-Service

# rbac_mail_bridge/services/anonymizer.py

from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
import hashlib
import secrets

@dataclass
class AnonymizationResult:
    success: bool
    anonymization_id: str
    affected_mailboxes: int
    affected_emails: int
    audit_log_id: str
    errors: List[str]

class EmployeeAnonymizer:
    """
    DSGVO-konforme Anonymisierung von Mitarbeiterdaten.

    Prinzipien:
    1. Keine personenbezogenen Daten in Audit-Logs
    2. Rollenbasierte Historie bleibt erhalten
    3. Verschlüsselung der Original-Daten für Auskunftsrechte
    """

    def __init__(
        self,
        db: AsyncSession,
        mail_server: StalwartClient,
        encryption_service: EncryptionService
    ):
        self.db = db
        self.mail_server = mail_server
        self.encryption = encryption_service

    async def anonymize(
        self,
        user_id: str,
        reason: str,
        performed_by: str,
        options: AnonymizationOptions
    ) -> AnonymizationResult:
        """
        Führt vollständige Anonymisierung durch.

        Steps:
        1. Validation
        2. Mailbox-Zuweisungen widerrufen
        3. Benutzerdaten pseudonymisieren
        4. E-Mail-Header anonymisieren (optional)
        5. Audit-Log erstellen
        6. Event publizieren
        """
        anonymization_id = str(uuid.uuid4())
        errors = []

        async with self.db.begin():
            # 1. Validierung
            user = await self._get_user(user_id)
            if not user:
                raise UserNotFoundError(user_id)

            if user.anonymized_at:
                raise AlreadyAnonymizedError(user_id)

            # 2. Mailbox-Zuweisungen widerrufen
            assignments = await self._get_active_assignments(user_id)
            for assignment in assignments:
                await self._revoke_assignment(
                    assignment.id,
                    performed_by,
                    reason="employee_anonymization"
                )

            # 3. Benutzerdaten pseudonymisieren
            pseudonym = self._generate_pseudonym()

            # Original-Daten verschlüsseln (für DSGVO Art. 15 Auskunftsrecht)
            encrypted_original = await self.encryption.encrypt({
                "name": user.name,
                "email": user.email,
                "encrypted_at": datetime.utcnow().isoformat()
            })

            await self.db.execute("""
                UPDATE users SET
                    name = :pseudonym,
                    email = :anon_email,
                    anonymized_at = NOW(),
                    anonymization_token = :token,
                    original_data_encrypted = :encrypted
                WHERE id = :user_id
            """, {
                "pseudonym": f"Ehemaliger Mitarbeiter ({pseudonym})",
                "anon_email": f"anon_{pseudonym}@deleted.local",
                "token": secrets.token_hex(32),
                "encrypted": encrypted_original,
                "user_id": user_id
            })

            # 4. E-Mail-Header anonymisieren (falls gewünscht)
            affected_emails = 0
            if options.anonymize_email_headers:
                affected_emails = await self._anonymize_email_headers(
                    user_id, pseudonym
                )

            # 5. Audit-Log
            audit_log_id = await self._create_audit_log(
                entity_type="user",
                entity_id=user_id,
                anonymization_type="pseudonymization",
                fields_affected={
                    "name": True,
                    "email": True,
                    "mailbox_assignments": len(assignments),
                    "email_headers": affected_emails
                },
                reason=reason,
                performed_by=performed_by,
                legal_basis="Art. 17 DSGVO"
            )

            # 6. Event publizieren (für andere Services)
            await self._publish_event(UserAnonymizedEvent(
                user_id=user_id,
                anonymization_id=anonymization_id,
                affected_mailboxes=[a.mailbox_id for a in assignments]
            ))

        return AnonymizationResult(
            success=True,
            anonymization_id=anonymization_id,
            affected_mailboxes=len(assignments),
            affected_emails=affected_emails,
            audit_log_id=audit_log_id,
            errors=errors
        )

    def _generate_pseudonym(self) -> str:
        """Generiert ein eindeutiges Pseudonym."""
        return hashlib.sha256(
            secrets.token_bytes(32)
        ).hexdigest()[:12]

    async def _anonymize_email_headers(
        self,
        user_id: str,
        pseudonym: str
    ) -> int:
        """
        Anonymisiert E-Mail-Header in Stalwart.

        Ersetzt:
        - From: Max Mustermann → Ehemaliger Mitarbeiter
        - Reply-To: max.mustermann@... → anon_xxx@deleted.local

        Behält:
        - Funktionale Absender-Adressen (klassenlehrer.5a@...)
        """
        # Stalwart API Call für Header-Manipulation
        return await self.mail_server.anonymize_headers(
            user_id=user_id,
            replacement_name=f"Ehemaliger Mitarbeiter ({pseudonym})"
        )

6.2 Stalwart Integration

# rbac_mail_bridge/integrations/stalwart.py

class StalwartClient:
    """
    Client für Stalwart Mail Server API.

    Dokumentation: https://stalw.art/docs/api/
    """

    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
        self.session = httpx.AsyncClient(
            base_url=base_url,
            headers={"Authorization": f"Bearer {api_key}"}
        )

    async def create_mailbox(
        self,
        email: str,
        display_name: str,
        quota_mb: int = 1024
    ) -> str:
        """Erstellt eine neue Mailbox in Stalwart."""
        response = await self.session.post(
            "/api/v1/accounts",
            json={
                "email": email,
                "name": display_name,
                "quota": quota_mb * 1024 * 1024,
                "type": "individual"
            }
        )
        response.raise_for_status()
        return response.json()["id"]

    async def update_mailbox_access(
        self,
        mailbox_id: str,
        user_email: str,
        access_type: str  # "full", "send_as", "read_only"
    ):
        """
        Aktualisiert Zugriffsrechte auf eine Mailbox.

        Wird verwendet wenn:
        - Neue Zuweisung erstellt wird
        - Zuweisung widerrufen wird
        """
        response = await self.session.put(
            f"/api/v1/accounts/{mailbox_id}/access",
            json={
                "user": user_email,
                "access": access_type
            }
        )
        response.raise_for_status()

    async def anonymize_headers(
        self,
        user_id: str,
        replacement_name: str
    ) -> int:
        """
        Anonymisiert E-Mail-Header für einen Benutzer.

        Returns: Anzahl der betroffenen E-Mails
        """
        # Stalwart unterstützt dies möglicherweise nicht nativ
        # Alternative: Sieve-Filter oder Post-Processing
        pass

6.3 SOGo Integration

# rbac_mail_bridge/integrations/sogo.py

class SOGoClient:
    """
    Client für SOGo Groupware API.

    SOGo nutzt CalDAV/CardDAV und hat eine eigene REST API.
    """

    def __init__(self, base_url: str, admin_user: str, admin_pass: str):
        self.base_url = base_url
        self.auth = (admin_user, admin_pass)

    async def create_calendar_event(
        self,
        calendar_id: str,
        event: CalendarEvent
    ) -> str:
        """Erstellt einen Kalender-Eintrag."""
        ical_data = self._to_ical(event)

        response = await httpx.put(
            f"{self.base_url}/SOGo/dav/{calendar_id}/Calendar/{event.uid}.ics",
            content=ical_data,
            headers={"Content-Type": "text/calendar"},
            auth=self.auth
        )
        response.raise_for_status()
        return event.uid

    def _to_ical(self, event: CalendarEvent) -> str:
        """Konvertiert Event zu iCalendar Format."""
        lines = [
            "BEGIN:VCALENDAR",
            "VERSION:2.0",
            "PRODID:-//BreakPilot//Mail-RBAC//DE",
            "BEGIN:VEVENT",
            f"UID:{event.uid}",
            f"DTSTART:{event.start_time.strftime('%Y%m%dT%H%M%SZ')}",
            f"DTEND:{event.end_time.strftime('%Y%m%dT%H%M%SZ')}",
            f"SUMMARY:{event.title}",
        ]

        if event.description:
            lines.append(f"DESCRIPTION:{event.description}")

        if event.jitsi_url:
            lines.append(f"LOCATION:{event.jitsi_url}")
            lines.append(f"X-JITSI-URL:{event.jitsi_url}")

        lines.extend([
            "END:VEVENT",
            "END:VCALENDAR"
        ])

        return "\r\n".join(lines)

7. Frontend-Integration

7.1 Admin-Seite: Mail-Management

// website/app/admin/mail-management/page.tsx

'use client'

import { useState, useEffect } from 'react'
import AdminLayout from '@/components/admin/AdminLayout'

type TabType = 'mailboxes' | 'assignments' | 'anonymization' | 'audit'

interface FunctionalMailbox {
  id: string
  role_key: string
  email_address: string
  display_name: string
  is_active: boolean
  current_assignee?: {
    id: string
    name: string
    email: string
  }
}

export default function MailManagementPage() {
  const [activeTab, setActiveTab] = useState<TabType>('mailboxes')
  const [mailboxes, setMailboxes] = useState<FunctionalMailbox[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchMailboxes()
  }, [])

  const fetchMailboxes = async () => {
    try {
      const res = await fetch('/api/admin/mail-rbac/mailboxes')
      const data = await res.json()
      setMailboxes(data)
    } finally {
      setLoading(false)
    }
  }

  return (
    <AdminLayout
      title="Mail-Management"
      description="Rollenbasierte E-Mail-Verwaltung mit DSGVO-Anonymisierung"
    >
      {/* Tabs */}
      <div className="border-b border-slate-200 mb-6">
        <nav className="flex gap-6">
          {[
            { id: 'mailboxes', label: 'Funktionale Mailboxen', icon: '📧' },
            { id: 'assignments', label: 'Zuweisungen', icon: '👤' },
            { id: 'anonymization', label: 'Anonymisierung', icon: '🔒' },
            { id: 'audit', label: 'Audit-Log', icon: '📋' },
          ].map((tab) => (
            <button
              key={tab.id}
              onClick={() => setActiveTab(tab.id as TabType)}
              className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
                activeTab === tab.id
                  ? 'border-primary-600 text-primary-600'
                  : 'border-transparent text-slate-500 hover:text-slate-700'
              }`}
            >
              {tab.icon} {tab.label}
            </button>
          ))}
        </nav>
      </div>

      {/* Mailboxes Tab */}
      {activeTab === 'mailboxes' && (
        <div className="space-y-6">
          {/* Stats */}
          <div className="grid grid-cols-4 gap-4">
            <div className="bg-white rounded-lg shadow p-4">
              <div className="text-3xl font-bold text-primary-600">
                {mailboxes.length}
              </div>
              <div className="text-sm text-slate-500">Funktionale Mailboxen</div>
            </div>
            <div className="bg-white rounded-lg shadow p-4">
              <div className="text-3xl font-bold text-green-600">
                {mailboxes.filter(m => m.current_assignee).length}
              </div>
              <div className="text-sm text-slate-500">Zugewiesen</div>
            </div>
            <div className="bg-white rounded-lg shadow p-4">
              <div className="text-3xl font-bold text-yellow-600">
                {mailboxes.filter(m => !m.current_assignee).length}
              </div>
              <div className="text-sm text-slate-500">Nicht zugewiesen</div>
            </div>
            <div className="bg-white rounded-lg shadow p-4">
              <div className="text-3xl font-bold text-slate-600">
                {new Set(mailboxes.map(m => m.role_key)).size}
              </div>
              <div className="text-sm text-slate-500">Verschiedene Rollen</div>
            </div>
          </div>

          {/* Mailbox Liste */}
          <div className="bg-white rounded-lg shadow">
            <div className="p-4 border-b border-slate-200 flex justify-between items-center">
              <h2 className="text-lg font-semibold">Funktionale Mailboxen</h2>
              <button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">
                + Neue Mailbox
              </button>
            </div>
            <div className="overflow-x-auto">
              <table className="w-full">
                <thead className="bg-slate-50">
                  <tr>
                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
                      E-Mail-Adresse
                    </th>
                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
                      Rolle
                    </th>
                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
                      Aktuell zugewiesen
                    </th>
                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
                      Status
                    </th>
                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-600">
                      Aktionen
                    </th>
                  </tr>
                </thead>
                <tbody className="divide-y divide-slate-100">
                  {mailboxes.map((mailbox) => (
                    <tr key={mailbox.id} className="hover:bg-slate-50">
                      <td className="py-3 px-4">
                        <code className="text-sm bg-slate-100 px-2 py-1 rounded">
                          {mailbox.email_address}
                        </code>
                      </td>
                      <td className="py-3 px-4">
                        <span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
                          {mailbox.role_key}
                        </span>
                      </td>
                      <td className="py-3 px-4">
                        {mailbox.current_assignee ? (
                          <div>
                            <div className="font-medium">
                              {mailbox.current_assignee.name}
                            </div>
                            <div className="text-xs text-slate-500">
                              {mailbox.current_assignee.email}
                            </div>
                          </div>
                        ) : (
                          <span className="text-slate-400 italic">
                            Nicht zugewiesen
                          </span>
                        )}
                      </td>
                      <td className="py-3 px-4">
                        <span className={`px-2 py-1 text-xs rounded-full ${
                          mailbox.is_active
                            ? 'bg-green-100 text-green-800'
                            : 'bg-red-100 text-red-800'
                        }`}>
                          {mailbox.is_active ? 'Aktiv' : 'Inaktiv'}
                        </span>
                      </td>
                      <td className="py-3 px-4">
                        <div className="flex gap-2">
                          <button className="text-sm text-blue-600 hover:underline">
                            Bearbeiten
                          </button>
                          <button className="text-sm text-green-600 hover:underline">
                            Zuweisen
                          </button>
                        </div>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      )}

      {/* Anonymization Tab */}
      {activeTab === 'anonymization' && (
        <div className="space-y-6">
          {/* Warning Banner */}
          <div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
            <div className="flex gap-3">
              <span className="text-2xl">⚠️</span>
              <div>
                <h3 className="font-semibold text-amber-800">
                  Anonymisierung ist irreversibel
                </h3>
                <p className="text-sm text-amber-700">
                  Die Anonymisierung kann nicht rückgängig gemacht werden.
                  Stellen Sie sicher, dass alle Aufbewahrungsfristen eingehalten werden.
                </p>
              </div>
            </div>
          </div>

          {/* Anonymization Form */}
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-lg font-semibold mb-4">
              Mitarbeiter anonymisieren
            </h2>

            <div className="space-y-4">
              <div>
                <label className="block text-sm font-medium text-slate-700 mb-1">
                  Mitarbeiter auswählen
                </label>
                <select className="w-full border border-slate-300 rounded-lg px-3 py-2">
                  <option>-- Bitte wählen --</option>
                </select>
              </div>

              <div>
                <label className="block text-sm font-medium text-slate-700 mb-1">
                  Grund der Anonymisierung
                </label>
                <select className="w-full border border-slate-300 rounded-lg px-3 py-2">
                  <option value="employee_departure">Mitarbeiter-Ausscheiden</option>
                  <option value="dsgvo_request">DSGVO-Löschungsantrag</option>
                  <option value="manual">Manuell (mit Begründung)</option>
                </select>
              </div>

              <div className="space-y-2">
                <label className="flex items-center gap-2">
                  <input type="checkbox" className="rounded" defaultChecked />
                  <span className="text-sm">Audit-Trail beibehalten</span>
                </label>
                <label className="flex items-center gap-2">
                  <input type="checkbox" className="rounded" />
                  <span className="text-sm">E-Mail-Inhalte löschen</span>
                </label>
              </div>

              <div className="pt-4 border-t border-slate-200">
                <button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
                  Anonymisierung durchführen
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </AdminLayout>
  )
}

8. Docker-Konfiguration

8.1 Docker Compose Erweiterung

# docker-compose.mail.yml
# Verwendet mit: docker compose -f docker-compose.yml -f docker-compose.mail.yml up

version: '3.8'

services:
  # Stalwart Mail Server
  stalwart:
    image: stalwartlabs/mail-server:v0.9
    container_name: breakpilot-mail-stalwart
    hostname: mail.breakpilot.local
    ports:
      - "25:25"      # SMTP
      - "143:143"    # IMAP
      - "465:465"    # SMTPS
      - "993:993"    # IMAPS
      - "4190:4190"  # ManageSieve
      - "8787:8080"  # Admin API
    volumes:
      - stalwart-data:/opt/stalwart-mail/data
      - ./config/stalwart/config.toml:/opt/stalwart-mail/etc/config.toml:ro
    environment:
      - STALWART_HOSTNAME=mail.breakpilot.local
    networks:
      - breakpilot-pwa-network
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 30s
      timeout: 10s
      retries: 3

  # SOGo Groupware
  sogo:
    image: sogo/sogo:5.10
    container_name: breakpilot-mail-sogo
    ports:
      - "20000:20000"
    volumes:
      - ./config/sogo/sogo.conf:/etc/sogo/sogo.conf:ro
    environment:
      - MYSQL_HOST=postgres  # SOGo unterstützt auch PostgreSQL
      - SOGO_HOSTNAME=groupware.breakpilot.local
    depends_on:
      - stalwart
      - postgres
    networks:
      - breakpilot-pwa-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:20000/SOGo/"]
      interval: 30s
      timeout: 10s
      retries: 3

  # RBAC-Mail-Bridge
  rbac-mail-bridge:
    build:
      context: ./rbac-mail-bridge
      dockerfile: Dockerfile
    container_name: breakpilot-mail-rbac
    ports:
      - "8087:8087"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - STALWART_API_URL=http://stalwart:8080
      - STALWART_API_KEY=${STALWART_API_KEY}
      - SOGO_URL=http://sogo:20000
      - JITSI_URL=${JITSI_URL:-http://localhost:8443}
      - ENCRYPTION_KEY=${MAIL_ENCRYPTION_KEY}
    depends_on:
      - postgres
      - stalwart
      - sogo
    networks:
      - breakpilot-pwa-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8087/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  stalwart-data:
    driver: local

8.2 Stalwart Konfiguration

# config/stalwart/config.toml

[server]
hostname = "mail.breakpilot.local"

[store]
db.type = "postgresql"
db.url = "postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_mail"

[authentication]
mechanisms = ["PLAIN", "LOGIN"]
directory.type = "internal"

[imap]
bind = ["0.0.0.0:143"]

[smtp]
bind = ["0.0.0.0:25"]

[api]
bind = ["0.0.0.0:8080"]
key = "${STALWART_API_KEY}"

9. Sicherheit

9.1 Verschlüsselung

# rbac_mail_bridge/services/encryption.py

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

class EncryptionService:
    """
    AES-256 Verschlüsselung für Original-Daten.

    Verwendet für:
    - Verschlüsselung der Original-Benutzerdaten bei Anonymisierung
    - DSGVO Art. 15 Auskunftsrecht (Entschlüsselung nur bei Berechtigung)
    """

    def __init__(self, master_key: str):
        self.master_key = master_key.encode()

    def _derive_key(self, salt: bytes) -> bytes:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )
        return base64.urlsafe_b64encode(kdf.derive(self.master_key))

    async def encrypt(self, data: dict) -> bytes:
        """Verschlüsselt Daten mit AES-256."""
        salt = os.urandom(16)
        key = self._derive_key(salt)
        f = Fernet(key)

        plaintext = json.dumps(data).encode()
        ciphertext = f.encrypt(plaintext)

        # Salt + Ciphertext kombinieren
        return salt + ciphertext

    async def decrypt(self, encrypted: bytes, audit_reason: str) -> dict:
        """
        Entschlüsselt Daten.

        WICHTIG: Jede Entschlüsselung wird geloggt!
        """
        salt = encrypted[:16]
        ciphertext = encrypted[16:]

        key = self._derive_key(salt)
        f = Fernet(key)

        plaintext = f.decrypt(ciphertext)

        # Audit-Log
        await self._log_decryption(audit_reason)

        return json.loads(plaintext.decode())

9.2 Zugriffskontrolle

# Berechtigungen für Mail-RBAC

MAIL_RBAC_PERMISSIONS = {
    "mail:mailbox:create": "Kann funktionale Mailboxen erstellen",
    "mail:mailbox:assign": "Kann Mailboxen Benutzern zuweisen",
    "mail:mailbox:revoke": "Kann Mailbox-Zuweisungen widerrufen",
    "mail:user:anonymize": "Kann Benutzer anonymisieren",
    "mail:audit:view": "Kann Audit-Logs einsehen",
    "mail:audit:export": "Kann DSGVO-Export durchführen",
}

# Rollen-Mapping
ROLE_PERMISSIONS = {
    "schul_admin": [
        "mail:mailbox:create",
        "mail:mailbox:assign",
        "mail:mailbox:revoke",
    ],
    "data_protection_officer": [
        "mail:user:anonymize",
        "mail:audit:view",
        "mail:audit:export",
    ],
    "schulleitung": [
        "mail:mailbox:assign",
        "mail:mailbox:revoke",
        "mail:audit:view",
    ],
}

10. Testing

10.1 Unit Tests

# rbac_mail_bridge/tests/test_anonymizer.py

import pytest
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock

from services.anonymizer import EmployeeAnonymizer, AnonymizationResult

@pytest.fixture
def anonymizer():
    db = AsyncMock()
    mail_server = AsyncMock()
    encryption = AsyncMock()
    return EmployeeAnonymizer(db, mail_server, encryption)

class TestAnonymizer:
    @pytest.mark.asyncio
    async def test_anonymize_user_success(self, anonymizer):
        # Arrange
        anonymizer._get_user = AsyncMock(return_value=MagicMock(
            id="user-123",
            name="Max Mustermann",
            email="max@test.de",
            anonymized_at=None
        ))
        anonymizer._get_active_assignments = AsyncMock(return_value=[
            MagicMock(id="assign-1", mailbox_id="mb-1"),
            MagicMock(id="assign-2", mailbox_id="mb-2"),
        ])
        anonymizer._revoke_assignment = AsyncMock()
        anonymizer._create_audit_log = AsyncMock(return_value="audit-123")
        anonymizer._publish_event = AsyncMock()

        # Act
        result = await anonymizer.anonymize(
            user_id="user-123",
            reason="employee_departure",
            performed_by="admin-1",
            options=MagicMock(anonymize_email_headers=False)
        )

        # Assert
        assert result.success is True
        assert result.affected_mailboxes == 2
        assert anonymizer._revoke_assignment.call_count == 2
        assert anonymizer._create_audit_log.called

    @pytest.mark.asyncio
    async def test_anonymize_already_anonymized_raises(self, anonymizer):
        # Arrange
        anonymizer._get_user = AsyncMock(return_value=MagicMock(
            anonymized_at=datetime.utcnow()
        ))

        # Act & Assert
        with pytest.raises(AlreadyAnonymizedError):
            await anonymizer.anonymize(
                user_id="user-123",
                reason="employee_departure",
                performed_by="admin-1",
                options=MagicMock()
            )

    @pytest.mark.asyncio
    async def test_anonymize_user_not_found_raises(self, anonymizer):
        # Arrange
        anonymizer._get_user = AsyncMock(return_value=None)

        # Act & Assert
        with pytest.raises(UserNotFoundError):
            await anonymizer.anonymize(
                user_id="nonexistent",
                reason="employee_departure",
                performed_by="admin-1",
                options=MagicMock()
            )

10.2 Integration Tests

# rbac_mail_bridge/tests/test_integration.py

import pytest
from httpx import AsyncClient

@pytest.mark.integration
class TestMailRBACIntegration:
    @pytest.mark.asyncio
    async def test_create_and_assign_mailbox(self, client: AsyncClient):
        # 1. Mailbox erstellen
        create_res = await client.post(
            "/api/v1/mailboxes",
            json={
                "role_key": "klassenlehrer",
                "email_address": "klassenlehrer.test@school.bp.app",
                "display_name": "Klassenlehrer Test",
                "tenant_id": "tenant-123"
            }
        )
        assert create_res.status_code == 201
        mailbox_id = create_res.json()["id"]

        # 2. Mailbox zuweisen
        assign_res = await client.post(
            f"/api/v1/mailboxes/{mailbox_id}/assign",
            json={
                "user_id": "user-456",
                "valid_from": "2026-01-10T00:00:00Z"
            }
        )
        assert assign_res.status_code == 201

        # 3. Verifizieren
        get_res = await client.get(f"/api/v1/mailboxes/{mailbox_id}")
        assert get_res.status_code == 200
        assert get_res.json()["current_assignee"]["id"] == "user-456"

11. Deployment Checklist

11.1 Vor dem Deployment

  • PostgreSQL Schema migriert
  • Stalwart API Key generiert und in .env
  • MAIL_ENCRYPTION_KEY generiert (32 Bytes, Base64)
  • DNS Records konfiguriert (MX, SPF, DKIM, DMARC)
  • SSL-Zertifikate für Mail-Domain
  • Firewall-Regeln für Ports 25, 143, 465, 993

11.2 Nach dem Deployment

  • Health-Checks für alle Services grün
  • Test-E-Mail senden/empfangen
  • Admin-UI erreichbar
  • Audit-Logging funktioniert
  • Backup konfiguriert

12. Roadmap

Phase 1: MVP (4-6 Wochen)

  • Architektur-Dokumentation
  • Datenbank-Schema
  • RBAC-Mail-Bridge Backend
  • Stalwart Integration
  • Admin-UI (Basis)

Phase 2: Groupware (2-3 Wochen)

  • SOGo Integration
  • CalDAV/CardDAV
  • Jitsi-Meeting aus Kalender

Phase 3: Anonymisierung (2-3 Wochen)

  • Anonymisierungs-Service
  • E-Mail-Header-Anonymisierung
  • DSGVO-Export

Phase 4: Polish (1-2 Wochen)

  • Admin-UI vollständig
  • Dokumentation
  • Produktions-Hardening

Anhang A: Glossar

Begriff Beschreibung
Funktionale Mailbox Rollengebundene E-Mail-Adresse (z.B. klassenlehrer.5a@...)
Personenbezogene Mailbox An eine Person gebundene E-Mail-Adresse
Anonymisierung Unwiderrufliche Entfernung personenbezogener Daten
Pseudonymisierung Ersetzung durch Pseudonym (reversibel mit Schlüssel)
Audit-Trail Lückenlose Protokollierung aller Aktionen
RBAC Role-Based Access Control
CalDAV Calendar Distributed Authoring and Versioning

Anhang B: Referenzen