# 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 ```sql -- ============================================================ -- 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 ```yaml # ============================================================ # 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) ```python # 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 ```python # 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 ```python # 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 ```python # 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 ```tsx // 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('mailboxes') const [mailboxes, setMailboxes] = useState([]) 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 ( {/* Tabs */}
{/* Mailboxes Tab */} {activeTab === 'mailboxes' && (
{/* Stats */}
{mailboxes.length}
Funktionale Mailboxen
{mailboxes.filter(m => m.current_assignee).length}
Zugewiesen
{mailboxes.filter(m => !m.current_assignee).length}
Nicht zugewiesen
{new Set(mailboxes.map(m => m.role_key)).size}
Verschiedene Rollen
{/* Mailbox Liste */}

Funktionale Mailboxen

{mailboxes.map((mailbox) => ( ))}
E-Mail-Adresse Rolle Aktuell zugewiesen Status Aktionen
{mailbox.email_address} {mailbox.role_key} {mailbox.current_assignee ? (
{mailbox.current_assignee.name}
{mailbox.current_assignee.email}
) : ( Nicht zugewiesen )}
{mailbox.is_active ? 'Aktiv' : 'Inaktiv'}
)} {/* Anonymization Tab */} {activeTab === 'anonymization' && (
{/* Warning Banner */}
⚠️

Anonymisierung ist irreversibel

Die Anonymisierung kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass alle Aufbewahrungsfristen eingehalten werden.

{/* Anonymization Form */}

Mitarbeiter anonymisieren

)}
) } ``` --- ## 8. Docker-Konfiguration ### 8.1 Docker Compose Erweiterung ```yaml # 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 ```toml # 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 ```python # 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 ```python # 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 ```python # 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 ```python # 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) - [x] 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 - [Stalwart Mail Server Docs](https://stalw.art/docs/) - [SOGo Installation Guide](https://www.sogo.nu/support/faq.html) - [DSGVO Art. 17 - Recht auf Löschung](https://dsgvo-gesetz.de/art-17-dsgvo/) - [RFC 5545 - iCalendar](https://tools.ietf.org/html/rfc5545) - [RFC 4791 - CalDAV](https://tools.ietf.org/html/rfc4791)