# MAIL_DAEMON_DEPLOYMENT — Backyard Handoff

**Erstellt von:** Mo (Mail-Architektur-Agent)
**Datum:** 2026-05-09
**Status:** Bereit für Deployment — wartet auf Backyard-GO vom User

## Kontext

Der `rebreak-imap-idle` Daemon ist ein eigenständiger Node.js-Prozess.
Er hält pro aktivem MailConnection-DB-Eintrag eine persistente IMAP-IDLE-Session
und triggert bei neuer Mail sofort `/api/mail/scan-internal` statt auf den 30min-Cron zu warten.

Der Daemon liegt unter `backend/imap-idle/index.mjs` und hat seine eigene `package.json`.
Er ist KEIN Teil des Nitro-Builds — er wird direkt via `node` gestartet.

## Was Backyard tun muss (in dieser Reihenfolge)

### Schritt 1 — GH-Actions: imap-idle ins Artifact einschließen

In `.github/workflows/deploy-backend.yml` (oder analog) muss das `backend/imap-idle/`-Verzeichnis
ins deploy-Artifact aufgenommen werden.

Das Artifact enthält aktuell wahrscheinlich nur `backend/.output-staging/` und `backend/prisma/`.
`backend/imap-idle/` muss ebenfalls mit kopiert werden.

Konkretes Beispiel (je nach Artifact-Aufbau anpassen):

```yaml
# In der scp/rsync-Step des deploy-workflows:
- name: Copy imap-idle to server
  run: |
    scp -r backend/imap-idle/ rebreak-server:/srv/rebreak/backend/imap-idle/
```

### Schritt 2 — npm install auf Server

Nach dem Artifact-Copy muss auf dem Server `npm install` in `backend/imap-idle/` laufen.
Das installiert `imapflow` und `pg` lokal für den Daemon.

```bash
cd /srv/rebreak/backend/imap-idle && npm install --production
```

Diesen Schritt als deploy-Step in GH-Actions oder in `deploy-from-artifact.sh` ergänzen.

### Schritt 3 — Zombie-Prozesse aufräumen

Vor dem ersten Start der neuen pm2-Einträge alte Stale-Einträge entfernen
(falls `rebreak-imap-staging` oder `rebreak-idle-staging` aus altem Setup noch existieren):

```bash
pm2 delete rebreak-idle-staging rebreak-imap-staging 2>/dev/null || true
```

### Schritt 4 — ecosystem.config.js erweitern

Die folgenden Einträge in `/srv/rebreak/ecosystem.config.js` ergänzen
(unterhalb des bestehenden `rebreak-staging`-Eintrags einfügen):

```js
// ─── IMAP IDLE Daemon Staging ───────────────────────────────────────────────
{
  name: "rebreak-idle-staging",
  script: "/srv/rebreak/backend/imap-idle/index.mjs",
  interpreter: "/root/.nvm/versions/node/v24.11.1/bin/node",
  cwd: "/srv/rebreak/backend/imap-idle",
  instances: 1,
  autorestart: true,
  watch: false,
  max_memory_restart: "256M",
  env: {
    NODE_ENV: "production",
    // ACHTUNG: Keine Secrets hier hinterlegen.
    // Infisical-Wrapper via start-idle-staging.sh (Schritt 5).
  },
},

// ─── IMAP IDLE Daemon Prod (auskommentiert bis Prod-Cutover) ───────────────
// {
//   name: "rebreak-idle-prod",
//   script: "/srv/rebreak/backend/imap-idle/index.mjs",
//   interpreter: "/root/.nvm/versions/node/v24.11.1/bin/node",
//   cwd: "/srv/rebreak/backend/imap-idle",
//   instances: 1,
//   autorestart: true,
//   watch: false,
//   max_memory_restart: "256M",
//   env: { NODE_ENV: "production" },
// },
```

### Schritt 5 — start-idle-staging.sh erstellen

Der Daemon braucht die gleichen Infisical-Secrets wie das Backend.
Eine eigene Start-Shell analog zu `backend/start-staging.sh` erstellen:

Pfad: `/srv/rebreak/backend/imap-idle/start-idle-staging.sh`

```bash
#!/bin/bash
# rebreak-imap-idle Staging — Infisical-Secret-Injection

set -euo pipefail
source /etc/environment

if [[ -z "${INFISICAL_CLIENT_ID:-}" || -z "${INFISICAL_CLIENT_SECRET:-}" ]]; then
  echo "[idle] FEHLER: INFISICAL_CLIENT_ID / SECRET nicht gesetzt" >&2
  exit 1
fi

INFISICAL_TOKEN=$(infisical login \
  --method=universal-auth \
  --client-id="${INFISICAL_CLIENT_ID}" \
  --client-secret="${INFISICAL_CLIENT_SECRET}" \
  --silent --plain 2>/dev/null)

[[ -z "$INFISICAL_TOKEN" ]] && { echo "[idle] Infisical login fehlgeschlagen" >&2; exit 1; }

NODE_BIN="/root/.nvm/versions/node/v24.11.1/bin/node"
DAEMON="/srv/rebreak/backend/imap-idle/index.mjs"

exec infisical run \
  --projectId="${INFISICAL_PROJECT_ID:-14b11b35-ef59-4b8a-a16b-398f0cc3ad93}" \
  --env=staging \
  --token="$INFISICAL_TOKEN" \
  -- bash -c '
    set -e
    export DATABASE_URL="${DATABASE_URL:-${NUXT_DATABASE_URL:-}}"
    export ADMIN_SECRET="${ADMIN_SECRET:-${NUXT_ADMIN_SECRET:-}}"
    export ENCRYPTION_KEY="${ENCRYPTION_KEY:-${NUXT_ENCRYPTION_KEY:-}}"
    export BACKEND_URL="http://127.0.0.1:3016"
    exec '"$NODE_BIN"' '"$DAEMON"'
  '
```

Dann `chmod +x start-idle-staging.sh` und in ecosystem.config.js den `script`-Key
auf `start-idle-staging.sh` zeigen lassen (mit `interpreter: "bash"`),
analog zum Pattern von `rebreak-staging`.

### Schritt 6 — pm2 starten

```bash
pm2 startOrReload /srv/rebreak/ecosystem.config.js
```

## Verifikations-Schritte nach Deployment

```bash
# 1. pm2-Status prüfen
pm2 list
# Erwartung: rebreak-idle-staging → status=online, restart=0

# 2. Logs der ersten 60 Sekunden ansehen
pm2 logs rebreak-idle-staging --lines 100
# Erwartung: "[idle/<email>] connected (...)" für alle aktiven Mailboxen
# "[idle/db] refreshed — N active connections, N sessions"

# 3. Test: Mail an eine verbundene Mailbox schicken (Betreff: "casino bonus")
# Innerhalb von 5 Sekunden sollte im Log erscheinen:
# "[idle/<email>] exists-event received (new mail)"
# "[idle/<email>] scan-triggered → scanned=X blocked=1"

# 4. Memory-Check nach 10 Minuten
pm2 monit
# Erwartung: < 100MB bei <20 Connections, < 200MB bei 100 Connections
```

## Rollback-Plan

Falls der Daemon crashed oder instabil ist:

```bash
pm2 stop rebreak-idle-staging
# NICHT delete — damit Logs erhalten bleiben
```

Auswirkung: Mail-Scanning fällt auf den bestehenden 30min-Cron zurück.
Kein Komplett-Ausfall der Mail-Schutz-Funktion. Gambling-Mails werden
weiter gelöscht, nur mit bis zu 30min Verzögerung statt Echtzeit.

## Bekannte Provider-Quirks

| Provider    | IMAP-Host                   | Port | TLS      | Bekanntes Problem                          |
|-------------|-----------------------------|------|----------|--------------------------------------------|
| Gmail       | imap.gmail.com              | 993  | Implicit | App-Password erforderlich (kein OAuth2)    |
| iCloud      | imap.mail.me.com            | 993  | Implicit | App-Specific-Password in Apple-Settings    |
| Outlook     | outlook.office365.com       | 993  | Implicit | IDLE-Drop nach ~20min — disableCompression |
| GMX         | imap.gmx.net                | 993  | Implicit | Stabil, kein besonderer Quirk              |
| Web.de      | imap.web.de                 | 993  | Implicit | Stabil                                     |
| T-Online    | secureimap.t-online.de      | 993  | Implicit | Stabil                                     |
| Posteo      | posteo.de                   | 993  | Implicit | Stabil                                     |

Outlook-spezifisch: Der Daemon setzt `disableCompression: true` wenn der Host
`office365` enthält — verhindert partial-read-Fehler nach IDLE-Drop.

## Datei-Übersicht

```
backend/imap-idle/
  index.mjs        — Daemon-Hauptdatei (ESM, standalone)
  package.json     — Eigene Dependencies (imapflow, pg)
  README.md        — Kurz-Doku (lokal starten, env-vars, log-format)
```

## Abhaengigkeiten

- `imapflow ^1.2.18` — IMAP-Client-Library (bereits in backend/package.json)
- `pg ^8.16.3` — Direkter Postgres-Zugriff (kein Prisma im Daemon-Kontext)
- Node.js >= 20 (ESM, top-level await via main())
- Infisical-CLI auf dem Server (bereits installiert fuer rebreak-staging)
