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:
- Kuratiert, nie beliebig. Die Liste der Zwecke ist fest; die Postfächer sind Daten, aber ihre Form ist es nicht.
- Eigener Schlüssel. Postfach-Passwörter werden mit
SECRETS_KEYverschlüsselt, nicht mitAPP_KEY. Ein APP_KEY-Wechsel ist gewöhnliche Wartung und darf den Mailversand nicht unbrauchbar machen. - 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.envist 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.
systemist der Rückfall, und zwar per Definition statt per Schalter: ein Zweck ohne belegtes Postfach benutzt das Postfach vonsystem. 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.systemselbst darf nicht leer bleiben: die Konsole verweigert das Speichern mit leeremsystem-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\MailboxResolverlö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:
MaintenanceAnnouncementMailundMaintenanceCancelledMail→maintenance,CloudReady→provisioning. - Auf dev wird weiterhin nur protokolliert.
MAIL_MAILER=logbleibt 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=logvorbei. 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 inapp/Support/Navigation.php— der einen Quelle. - Neue Berechtigung
mail.manage, nichtsecrets.manage: wer den Support-Absender pflegt, braucht keinen Zugriff auf Stripe- und DNS-Schlüssel. Rollen:Owner,Adminbekommen sie;Supportebenfalls. - 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
- Die fünf Postfächer werden als Zeilen angelegt — leer, aber sichtbar, damit die Seite zeigt, was erwartet wird.
- Die vorhandenen
MAIL_*aus.envfüllen die Server-Karte und dasno-reply-Postfach. Es bricht nichts. mail.passwordverlässt die Registry. Ein gespeicherter Wert wandert vorher insno-reply-Postfach. Die Karte verschwindet, weil sie ersetzt ist — nicht, weil sie versteckt wird.MAIL_FROM_ADDRESS/MAIL_FROM_NAMEverlieren 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_KEYverschlüsselt, nicht mitAPP_KEY; ein APP_KEY-Wechsel lässt den Versand unberührt. - Ein gespeichertes Passwort wird nie an die Oberfläche zurückgegeben.
systemlässt sich nicht leer speichern; jeder andere Zweck darf leer bleiben und fällt dann aufsystemzurück.Reply-Togesetzt fürsupport, fehlt beino_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=logauf dev. - Nichts wird beim Booten in
config('mail')geschrieben (Regel 3). mail.manageschützt die Seite;secrets.manageallein 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 |