# Spec — Postfächer und Absender

**Datum:** 2026-07-27
**Status:** entworfen, noch nicht umgesetzt
**Umfang:** **nur senden.** Empfangen (IMAP, Zuordnung eingehender Antworten) ist
ausdrücklich nicht Teil davon — siehe §10.
**Verwandt:** `docs/superpowers/specs/2026-07-27-operator-identity-design.md`
(eigenes Vorhaben, keine Abhängigkeit in beide Richtungen)

---

## 1. Ziel

CluPilot verschickt aus mehreren Adressen — `no-reply`, `office`, `support`,
`info`, `billing`. Heute gibt es dafür **eine** Stelle: das Geheimnis
`mail.password` in der Zugangsdaten-Registry, das auf
`mail.mailers.smtp.password` zeigt. Ein einziges Passwort für einen einzigen
Absender.

Das Vorhaben gibt jedem Postfach einen eigenen Datensatz, macht in der Konsole
einstellbar, welche Mail aus welchem Postfach geht, und stellt sicher, dass eine
Antwort des Kunden dort ankommt, wo sie jemand liest.

### Warum das nicht in die Registry passt

`SecretVault::REGISTRY` bildet **einen Schlüssel auf einen Config-Pfad** ab. Ein
Postfach ist Adresse, Anzeigename, Benutzer und Passwort. Fünf Postfächer als
Registry-Einträge wären zwanzig Einzelgeheimnisse mit Namen wie
`mail.support.username` — die Seite wäre voll und trotzdem unbedienbar, und die
Zuordnung „welche Mail aus welchem Postfach" hätte immer noch keinen Ort.

### Geerbte Regeln

`SecretVault` hat drei ausformulierte Regeln. Sie gelten hier unverändert:

1. **Kuratiert, nie beliebig.** Die Liste der *Zwecke* ist fest; die Postfächer
   sind Daten, aber ihre Form ist es nicht.
2. **Eigener Schlüssel.** Postfach-Passwörter werden mit `SECRETS_KEY`
   verschlüsselt, **nicht** mit `APP_KEY`. Ein APP_KEY-Wechsel ist gewöhnliche
   Wartung und darf den Mailversand nicht unbrauchbar machen.
3. **Beim Benutzen lesen, nie beim Booten überlagern.** Ein Overlay kostet auf
   jeder Anfrage eine Abfrage und lässt langlaufende Queue-Worker für immer das
   halten, was beim Start galt.

---

## 2. Ausgangsbefund

| Befund | Beleg |
|---|---|
| Genau **ein** Mail-Geheimnis | `SecretVault::REGISTRY` → `mail.password` → `mail.mailers.smtp.password` |
| Ein Server, ein Postfach | `.env`: `MAIL_HOST=mail.thinkidoo.com`, `MAIL_PORT=587`, `MAIL_SCHEME=tls`, `MAIL_USERNAME=no-reply@clupilot.com` |
| Es wird derzeit **nicht** verschickt | `MAIL_MAILER=log` |
| Absender-Vorgabe zeigt ins Leere | `MAIL_FROM_ADDRESS=hello@clupilot.local` — diese Adresse existiert nicht |
| Drei Mails vorhanden, keine setzt `from` | `MaintenanceAnnouncementMail`, `MaintenanceCancelledMail`, `CloudReady` — alle nehmen den globalen Absender |
| Alle Postfächer auf einem Server | entschieden 2026-07-27 |

> **Nebenbefund:** Das SMTP-Passwort steht im Klartext in `.env`. Das ist bei
> Laravel normal und `.env` ist gitignored — aber sobald es im Vault liegt,
> gehört es dort weg, und ein Wechsel bei der Gelegenheit ist billig.

---

## 3. Datenform

### Server — einmal, in den Einstellungen

`mail.host`, `mail.port`, `mail.encryption` über `App\Support\Settings`. Ein
Server, eine Stelle. Wenn sich der Anbieter ändert, ändert sich **eine** Karte,
nicht fünf Datensätze.

### Tabelle `mailboxes`

| Spalte | Zweck |
|---|---|
| `id`, `uuid` | Schlüssel; `uuid` für URLs |
| `key` (unique) | `no-reply`, `office`, `support`, `info`, `billing` — der Name, unter dem ein Zweck darauf zeigt |
| `address` | die Absenderadresse |
| `display_name` | „CluPilot Support" |
| `username` | SMTP-Benutzer; leer = `address` |
| `password` | verschlüsselt mit `SECRETS_KEY` |
| `no_reply` (bool) | unterdrückt `Reply-To`, siehe §5 |
| `active` (bool) | ein Postfach abschalten, ohne es zu löschen |
| `last_verified_at` | wann der Testversand zuletzt geklappt hat |
| `timestamps` | |

`key` ist frei wählbar — die fünf sind Startwerte, keine Schranke. Was **nicht**
frei ist, ist die Liste der Zwecke.

---

## 4. Zuordnung: welche Mail aus welchem Postfach

Eine kuratierte Liste von Zwecken, je Zweck ein Postfach:

| Zweck | Was daraus geht | Vorgabe |
|---|---|---|
| `maintenance` | Wartung angekündigt / abgesagt | `no-reply` |
| `provisioning` | Cloud bereit, Bestellbestätigung | `no-reply` |
| `support` | Antworten auf Support-Anfragen | `support` |
| `billing` | Rechnungen, Zahlungserinnerungen | `billing` |
| `system` | alles ohne eigenen Zweck | `no-reply` |

- Einstellbar in der Konsole, ein Auswahlfeld je Zeile.
- **`system` ist der Rückfall**, und zwar per Definition statt per Schalter: ein
  Zweck ohne belegtes Postfach benutzt das Postfach von `system`. Damit gibt es
  kein zweites Feld „welches ist das Standardpostfach", das der Zuordnung
  widersprechen könnte — der Rückfall *ist* eine Zeile der Zuordnungstabelle und
  wird dort gepflegt wie jede andere.
- `system` selbst darf nicht leer bleiben: die Konsole verweigert das Speichern
  mit leerem `system`-Postfach, denn das ist der eine Fall, für den es keinen
  Rückfall mehr gibt.
- **Die Zweckliste ist Code, nicht Daten.** Ein Zweck ohne Absender wäre sonst
  nur zur Laufzeit zu bemerken — an einer Mail, die nicht ankam.

---

## 5. `Reply-To` — das, was Nur-Senden brauchbar macht

Jede Mail bekommt `From` **und** `Reply-To` auf die Adresse ihres Postfachs.

Ohne das wäre „nur senden" eine Sackgasse: der Kunde antwortet auf eine
Support-Antwort und die Antwort verschwindet. Mit `Reply-To` landet sie im
echten `support@`-Postfach, das im gewöhnlichen Mailprogramm gelesen wird — das
ist der Grund, warum Empfangen (IMAP) hier **nicht** gebraucht wird.

**Ausnahme:** Postfächer mit `no_reply = true` bekommen **kein** `Reply-To`.
Ein „no-reply", auf das man antworten kann, ist eine Lüge im Absender.

---

## 6. Versand zur Laufzeit

- `App\Services\Mail\MailboxResolver` löst *Zweck → Postfach* auf und baut den
  Symfony-Transport **beim Versenden**. Nichts wird beim Booten in die Config
  geschrieben (Regel 3).
- Die drei vorhandenen Mailables bekommen ihren Zweck: `MaintenanceAnnouncementMail`
  und `MaintenanceCancelledMail` → `maintenance`, `CloudReady` → `provisioning`.
- **Auf dev wird weiterhin nur protokolliert.** `MAIL_MAILER=log` bleibt für
  gewöhnliche Mails wirksam, damit ein Seeder-Lauf oder ein Test mit echten
  Adressen niemandem eine Wartungsankündigung schickt. Entschieden 2026-07-27.

---

## 7. Testversand, der wirklich sendet

Pro Postfach ein Knopf: echte SMTP-Verbindung mit **diesen** Zugangsdaten, an
eine eingetippte Adresse.

- **Er geht am `MAIL_MAILER=log` vorbei.** Täte er das nicht, meldete er auf dev
  Erfolg, während er in eine Logdatei schreibt — ein Test, der die
  Implementierung nachrechnet statt sie zu prüfen. Genau der Fehler aus
  Handoff §2.2, nur an anderer Stelle.
- Die Oberfläche sagt, dass wirklich verschickt wird.
- Schlägt es fehl, steht der SMTP-Fehler **wörtlich** da — nicht „Fehler beim
  Senden". Bei falschen Zugangsdaten ist der Serversatz die ganze Diagnose.
- Erfolg setzt `last_verified_at`; die Liste zeigt „zuletzt bestätigt".

> Damit ist die Testknopf-Bedingung der Registry erfüllt: es gibt einen echten
> Test, also ist der Knopf gerechtfertigt. Ein Knopf, der still etwas anderes
> prüft, wäre schlechter als keiner.

---

## 8. Konsole

- Eigene Seite `admin.mail`, Navigationsgruppe **System** neben Zugangsdaten,
  eingetragen in `app/Support/Navigation.php` — der **einen** Quelle.
- Neue Berechtigung **`mail.manage`**, nicht `secrets.manage`: wer den
  Support-Absender pflegt, braucht keinen Zugriff auf Stripe- und DNS-Schlüssel.
  Rollen: `Owner`, `Admin` bekommen sie; `Support` ebenfalls.
- Aufbau: Server-Karte oben, Postfachliste darunter, Zuordnungstabelle zuletzt.
- **Bearbeiten im Modal (R20).** Die Zuordnungstabelle ist ein einzelnes
  Auswahlfeld je Zeile — R20s ausdrückliche Ausnahme, kein Modal.
- Passwörter werden nie wieder angezeigt, gleiche Formulierung wie bei den
  Zugangsdaten („Wird verschlüsselt gespeichert und nie wieder vollständig
  angezeigt.").
- Alle Zeiten über `->local()` (R19), Icons neben dem Text (R18).

---

## 9. Übernahme

1. Die fünf Postfächer werden als Zeilen angelegt — leer, aber sichtbar, damit
   die Seite zeigt, was erwartet wird.
2. Die vorhandenen `MAIL_*` aus `.env` füllen die Server-Karte und das
   `no-reply`-Postfach. Es bricht nichts.
3. **`mail.password` verlässt die Registry.** Ein gespeicherter Wert wandert
   vorher ins `no-reply`-Postfach. Die Karte verschwindet, weil sie **ersetzt**
   ist — nicht, weil sie versteckt wird.
4. `MAIL_FROM_ADDRESS`/`MAIL_FROM_NAME` verlieren ihre Rolle; das Standardpostfach
   übernimmt sie. Die Werte bleiben als Rückfall stehen, solange kein Postfach
   eingerichtet ist.

---

## 10. Abgrenzung

- **Kein Empfangen.** Kein IMAP, keine Zuordnung eingehender Antworten, keine
  Anhänge. `Reply-To` (§5) macht das für den Support-Fall entbehrlich.
- **Keine E-Mail-Vorlagen.** Bestellbestätigung und Störungsmeldung sind im
  Handoff §3.2 vereinbart, brauchen aber erst diesen Unterbau. Eigenes Vorhaben.
- **Keine Support-Warteschlange.** Sie wird durch dieses Vorhaben *möglich*
  (Handoff §3.1, Punkt 2), ist aber nicht darin enthalten.

---

## 11. Tests

- Zweck → Postfach, inklusive Rückfall auf das Standardpostfach.
- Passwort ist mit `SECRETS_KEY` verschlüsselt, **nicht** mit `APP_KEY`; ein
  APP_KEY-Wechsel lässt den Versand unberührt.
- Ein gespeichertes Passwort wird nie an die Oberfläche zurückgegeben.
- `system` lässt sich nicht leer speichern; jeder andere Zweck darf leer bleiben
  und fällt dann auf `system` zurück.
- `Reply-To` gesetzt für `support`, **fehlt** bei `no_reply = true`.
- Die drei vorhandenen Mailables tragen ihren Zweck und den richtigen Absender.
- Testversand benutzt den Transport des Postfachs, **nicht** den Log-Mailer —
  geprüft am Transport, nicht am Rückgabewert.
- Gewöhnlicher Versand respektiert `MAIL_MAILER=log` auf dev.
- Nichts wird beim Booten in `config('mail')` geschrieben (Regel 3).
- `mail.manage` schützt die Seite; `secrets.manage` allein genügt nicht.
- R18/R19/R20 auf der neuen Seite.

---

## 12. Reihenfolge

| Phase | Inhalt |
|---|---|
| 1 | Tabelle, Modell, Verschlüsselung, Übernahme aus `.env`, `mail.password` aus der Registry |
| 2 | `MailboxResolver`, Zwecke, `Reply-To`, die drei Mailables umstellen |
| 3 | Konsolen-Seite, `mail.manage`, Navigation, Bearbeiten im Modal |
| 4 | Testversand am Log-Mailer vorbei, `last_verified_at`, wörtliche Fehlermeldung |

---

## 13. Risiken

| Risiko | Gegenmaßnahme |
|---|---|
| Ein falsch gesetzter Absender fällt erst auf, wenn eine Mail nicht ankommt | Zweckliste ist Code; ein unbelegter Zweck fällt auf das Standardpostfach zurück statt zu scheitern; Testversand pro Postfach |
| Queue-Worker halten alte Zugangsdaten | Auflösung beim Versenden, nicht beim Booten (Regel 3) — als Test festgeschrieben |
| `SECRETS_KEY` fehlt | Der Vault behandelt das bereits: Speichern ist dann abgeschaltet und die Konsole sagt es, statt Zugangsdaten im Klartext abzulegen. Gleiches Verhalten für Postfächer. |
| Versehentlicher Echtversand auf dev | `MAIL_MAILER=log` bleibt wirksam; nur der Testknopf sendet, und er sagt es |
| Vorhandenes SMTP-Passwort liegt im Klartext in `.env` | Nach der Übernahme aus `.env` entfernen; Passwortwechsel empfohlen |
