CluPilotCloud/docs/superpowers/specs/2026-07-27-mailboxes-design.md

11 KiB

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::REGISTRYmail.passwordmail.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 MaintenanceCancelledMailmaintenance, CloudReadyprovisioning.
  • 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