# Consent-Gap-Plan — Art. 9 DSGVO Mail-Auto-Delete

Stand: 2026-05-13
Autor: rebreak-backend-agent
Status: Implementiert (Backend), TODOs für mo + rebreak-native-ui

---

## Was wurde implementiert

### Schema (Migration 20260513_art9_consent_log)

Neue Spalten in `mail_connections`:
- `consent_at TIMESTAMPTZ NULL` — wann eingewilligt, NULL = "Re-Consent pending"
- `consent_version TEXT NULL` — z.B. "art9-mail-v1-2026-05-13"
- `consent_ip_address TEXT NULL` — IP zum Zeitpunkt der Einwilligung

Neue Tabelle `consent_logs`:
- Append-only Audit-Trail für alle Einwilligungen und Widerrufe
- Wird NIEMALS gelöscht (Beweispflicht Art. 7 Abs. 1 DSGVO)

### Backend-Dateien

| Datei | Zweck |
|---|---|
| `server/utils/consent-texts.ts` | Versionierte Consent-Texte (DE + EN) |
| `server/db/consent.ts` | DB-Layer: writeConsentGrant, writeConsentRevoke, getConsentLogsByUser, setMailConnectionConsent |
| `server/api/mail-connections/consent.post.ts` | POST /api/mail-connections/consent |
| `server/api/mail-connections/[id].post.ts` | POST /api/mail-connections/:id (mit Consent-Gate 412) |
| `server/api/mail-connections/[id].delete.ts` | DELETE /api/mail-connections/:id (mit Widerruf-Log) |
| `server/api/user/delete.delete.ts` | Erweitert: schreibt Widerruf für alle Connections bei Account-Löschung |

### Aktuelle Consent-Version

`"art9-mail-v1-2026-05-13"` — definiert in `server/utils/consent-texts.ts`.

---

## TODO #1 — mo (Mail-Stack / Daemon)

**Daemon pausiert Verarbeitung wenn consent_at = NULL**

Kontext: Alle Bestandsrows nach Migration haben `consent_at = NULL`. Das bedeutet
"Re-Consent pending". Der Daemon darf für diese Connections KEIN Auto-Delete
ausführen bis der User explizit eingewilligt hat.

Implementierung in `imap-idle/index.mjs` (oder wherever der Scan-Loop läuft):

```js
// Beim Laden einer Connection für den Scan-Loop:
if (!connection.consent_at) {
  log(`[consent] Skipping ${connection.email}: Re-Consent pending (consent_at = NULL)`);
  // Verbindung aus dem aktiven Scan-Pool auslassen — KEIN Fehler, kein Error-State.
  // isActive bleibt true. Wenn User Re-Consent gibt, wird consent_at gesetzt
  // und die Connection beim nächsten Loop-Cycle wieder aufgenommen.
  continue;
}
```

Checklist:
- [ ] mo — `imap-idle/index.mjs`: consent_at-Check im Connection-Load
- [ ] mo — `scan.post.ts` / `scan-internal.post.ts`: gleiches Check (On-Demand-Scans)

---

## TODO #2 — mo (OAuth Token-Revoke bei Disconnect)

**Wenn OAuth-Connections getrennt werden: Token bei Microsoft revoken**

Kontext: Wenn `authMethod === 'oauth2_microsoft'` (Outlook-OAuth, noch nicht live),
muss beim Disconnect der Refresh-Token bei Microsoft widerrufen werden.

Placeholder-Comments existieren bereits in:
- `server/api/mail-connections/[id].delete.ts` (User-Disconnect)
- `server/api/user/delete.delete.ts` (Account-Löschung)

Implementierung wenn OAuth-Phase startet:

```ts
if (conn.authMethod === 'oauth2_microsoft') {
  const refreshToken = decrypt(conn.oauthRefreshToken);
  let revoked = false;
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          grant_type: 'revoke',
          token: refreshToken,
          client_id: process.env.MS_OAUTH_CLIENT_ID,
          client_secret: process.env.MS_OAUTH_CLIENT_SECRET,
        }),
      });
      revoked = true;
      break;
    } catch (e) {
      // Retry
    }
  }
  // Audit-Log: Token-Revoke success/failure
  // Dann trotzdem DB-Row löschen (DSB-Memo Abschnitt 5.1)
}
```

---

## TODO #3 — Datenexport (Art. 15 DSGVO)

**consent_logs für den User in den Datenexport aufnehmen**

Es gibt aktuell keinen `/api/data-export`-Endpoint im Backend. Wenn er gebaut
wird (separater Sprint), muss er enthalten:
- `consent_logs` für den User (aus `getConsentLogsByUser(userId)` in `server/db/consent.ts`)
- Für jede MailConnection: Provider, verbundene E-Mail-Adresse, Verbindungs-Zeitpunkt,
  erteilte OAuth-Scopes (lesbar, kein Token-Inhalt), Token-Ablaufdatum

DSB-Memo Abschnitt 5.2 + Hans-Müller-To-Do-Liste Item #6.

---

## Frontend-Spec für rebreak-native-ui (UI-Agent)

### Re-Consent-Modal (für Bestandsuser)

Trigger: App-Open + Auth-User eingeloggt + mindestens eine MailConnection mit
`consent_at = NULL` (Backend: GET /api/mail/status liefert connections mit
`consentAt`-Feld — Null-Check im Frontend).

Alternativ: neuer Endpoint `GET /api/mail-connections/pending-consent` der nur
die IDs der Connections ohne Consent zurückgibt.

Modal-Inhalt:
- Titel: aus `getConsentText("art9-mail-v1-2026-05-13").de` (oder .en je nach Locale)
- Zwei Buttons: "Einwilligen" → POST /api/mail-connections/consent, "Verbindung trennen" → DELETE /api/mail-connections/:id

Nach "Einwilligen":
- Pro Connection ein POST /api/mail-connections/consent senden
- Body: `{ mailConnectionId: "<uuid>", consentVersion: "art9-mail-v1-2026-05-13" }`
- Bei 200: Modal schließen, Toast "Einwilligung erteilt"
- Bei 409 (version_mismatch): sollte nicht passieren wenn Frontend aktuelle Version nutzt

### ConnectMailSheet — Consent-Gate

Neuer Flow:
1. User gibt Email + Passwort ein
2. Vor dem Abschicken: Consent-Text anzeigen (Checkbox oder expliziter Button)
3. POST /api/mail-connections/:id mit `consentVersion` im Body
4. Bei 412 (`consent_required`): Consent-Modal anzeigen (sollte nicht vorkommen
   wenn Schritt 2 korrekt implementiert ist)
5. Bei 200: Connection verbunden

Der `consentVersion`-Wert muss das Frontend kennen. Empfehlung: Backend liefert
ihn via `GET /api/mail/status` oder einem neuen `GET /api/mail-connections/consent-version`-
Endpoint. Alternativ: hardcoded im Frontend, aber dann muss er bei jedem Text-Bump
synchronisiert werden.

### Empfehlung: `GET /api/mail-connections/consent-version`

Einfacher Endpoint ohne Auth:
```
GET /api/mail-connections/consent-version
→ { version: "art9-mail-v1-2026-05-13", texts: { de: "...", en: "..." } }
```

Damit muss das Frontend die Version nie hardcoden. Noch nicht implementiert —
als TODO für rebreak-backend falls UI-Agent es braucht.

---

## Consent-Text-Bump-Workflow (für künftige DSB-Updates)

1. Hans-Müller gibt neuen Text frei
2. Neuen Eintrag in `server/utils/consent-texts.ts` hinzufügen
3. `CURRENT_ART9_MAIL_VERSION` auf neue Version setzen
4. Alle bestehenden Connections mit alter Version bekommen automatisch Re-Consent-Modal
   (Daemon-Check: `connection.consent_version !== CURRENT_ART9_MAIL_VERSION` → pausieren)
5. Deploy via GitHub Actions Pipeline

---

## Nicht gemacht (explizit aus Scope ausgeschlossen)

- Migration lokal ausgeführt — nein, Pipeline deployt
- Frontend-Änderungen — UI-Agent-Task
- Daemon-Logik angefasst — mo's Domain
- git push — kein User-GO erteilt
