# 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 |