hello.karameht.com — Devlog #2: Wohin mit den Mails?
Form-Submit landet zuerst in SQLite, Cronjob sendet via SMTP, Retry mit Backoff. Warum nicht MySQL, warum nicht synchron — und warum die Datenbank die Wahrheit ist und das Mail-Programm nur eine View. Teil 2 von 4.
In Teil 1 habe ich die Idee und den Stack umrissen: PHP, vanilla, kein Composer, eigener Mailversand via PHPMailer, eigene Subdomain auf all-inkl. Heute geht’s eine Schicht tiefer — das Unsichtbare zwischen „Submit-Button” und „Mail im Posteingang”.
Klingt simpel. Ist es nicht. Hier entscheidet sich, ob Nachrichten verloren gehen, ob ich verlässlich antworten kann, ob das System unter Last umfällt.
Wohin mit den Mails?
Form-Submit landet erstmal in einer eigenen Datenbank — nicht direkt im SMTP-Versand. Klingt nach Overhead, ist aber das Einzige, was verhindert, dass Nachrichten einfach weg sind.
DB-Wahl: SQLite, kein MySQL. Eine File-Datei, kein DB-Server, keine zweiten Credentials, Backup über die Storage Box von Hetzner, verschlüsselt — fertig. Für ein paar Nachrichten am Tag ist MySQL Overengineering. Wenn das Ding mal hundert Mails pro Stunde sieht, reden wir nochmal — vorher nicht.
Schema-Skizze, noch nicht final:
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT,
body TEXT NOT NULL,
ip_hash TEXT,
ua TEXT,
created_at INTEGER NOT NULL,
sent_at INTEGER,
status TEXT DEFAULT 'pending',
retry_count INTEGER DEFAULT 0
);
status läuft durch pending → sending → sent oder landet bei failed. ip_hash statt Roh-IP — bcrypt oder HMAC mit Server-Secret, dann kann ich Rate-Limit machen ohne Klartext-IP zu lagern. DSGVO ist nicht nur Cookie-Banner, das fängt im Schema an.
Retention: 90 Tage, dann automatisch weg. Speicherbegrenzung ist Pflicht und ich habe keinen Grund, Nachrichten ewig zu hamstern. Cronjob räumt auf. Das Gleiche auch in meinem Postfach: ich räume wöchentlich auf und lösche rigoros — das Ganze sicher mit Proton Mail.
PDO mit Prepared Statements, kein String-Konkat in SQL. Wer SQL-Konkatenation in 2026 noch verteidigt, hat den letzten Pentest-Bericht nicht gelesen.
Was, wenn die Mail nicht ankommt?
Klassischer Fehler: User klickt „Senden”, Server versucht synchron via SMTP, Timeout 30 Sekunden, User wartet, gibt auf, weiß nicht ob’s geklappt hat. Oder schlimmer: fire-and-forget per mail(), und keiner weiß je, ob die Nachricht angekommen ist. Beides ist Mist.
Lösung als Queue gedacht: Form-Submit schreibt sofort in die DB (Status pending) → 200 OK an User → Cronjob alle paar Minuten holt pending-Messages → SMTP-Versuch → Status auf sent oder bei Fehler retry_count++.
Retry-Backoff, noch nicht festgenagelt: nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden. Danach failed-Status und ich kriege eine Notfall-Mail (über einen zweiten Pfad, der nicht dieselbe Queue ist — sonst staut sich der Notfall im Stau, den er melden soll). Klassischer Fallstrick: dein Alerting hängt am selben System, das ausgefallen ist.
Cronjob ist all-inkl-Standard, kein extra Service. Ein PHP-Skript, getriggert per php /pfad/queue.php. Simpel.
Die DB ist die Wahrheit
Side-Effect dieser Architektur: ich habe alle Nachrichten auch dann noch, wenn der SMTP-Anbieter spinnt, ich versehentlich eine Mail lösche oder mein Mail-Client ein Update kaputtmacht. Die Wahrheit liegt in der DB, das Mail-Programm ist nur eine View.
Das klingt nach Detail. Ist aber der entscheidende Architektur-Schwenk gegenüber dem klassischen „form → mail() → vergessen”-Pattern.
Implikationen:
- Antworten zurückverfolgen: jede Eingangsnachricht hat eine
id. Antwort von mir per Mail enthält dieidals Reference-Header. Threading bleibt erhalten, auch wenn der User seine Mail-Adresse ändert. - DSGVO-Auskunft: Wer fragt „was habt ihr von mir gespeichert?”, kriegt ein
SELECT * FROM messages WHERE email = ?als Antwort. Eine Query, kein eigenes Tool. Vielleicht später. - Spam-Forensik: Wenn ich einen Pattern-Spam sehe, kann ich nachschauen, welche IP-Hashes, welche User-Agents, welche Zeitfenster. Logs reichen nicht — die werden rotiert.
- Statistik ohne Tracking: Wie viele Nachrichten kommen pro Woche? Wie oft sind sie technisch (Bug-Report) vs inhaltlich (Feedback)? Eine SQL-Query, keine Analytics-Plattform.
Was ich nicht mache: NLP-Analyse auf Body, Auto-Tagging, Sentiment-Score. YAGNI gilt auch hier. Das Wichtigste: ich erhalte eure Mails.
Wer empfängt die Mail eigentlich?
Die SMTP-Versand-Schicht ist all-inkl. Die Empfangs-Schicht ist Proton Mail — meine MX-Records zeigen dorthin. Heißt: jede Nachricht, die mein Skript an die in .env hinterlegte Empfänger-Adresse schickt, landet am Ende in einem Postfach mit E2E-Encryption, 2FA, Schweizer Datenschutz und ohne Werbe-Pixel-Tracker im HTML-Body.
Privacy-Pipeline schließt damit den Kreis. Ohne Proton hätte ich am Ende der Kette einen tracking-affinen Mail-Anbieter — dann wäre alles vorgelagerte Aufräumen für die Tonne. So aber: Form → DB → Cron → SMTP → E2E-Postfach. Keine Stelle gibt mehr preis als nötig.
Public-DNS-Info, kein Geheimnis. Wer wissen will, wohin Mail für karameht-Domains geht: dig MX karameht.com. Verstecken kann ich das nicht. Aber: Proton als Endstation ist eher Abschreckung als Angriffsfläche — Phishing-Angriffe sind dort schwerer als bei den üblichen Verdächtigen.
Was, wenn jemand das auch baut?
Wenn ihr das nachbauen wollt, hier die Minimal-Architektur für diese Schicht:
- SQLite-Datei
data/hello.sqliteaußerhalb vonpublic/(kein direkter HTTP-Zugriff) - Schema oben, plus eine
rate_limits-Tabelle (kommt in Teil 4 genauer) - PDO-Wrapper mit prepared statements — eine Datei
src/Db.phpreicht src/Queue.phpmit zwei Funktionen:enqueue(message)unddispatch()- Cronjob alle 2 Minuten:
*/2 * * * * php /pfad/queue.php dispatch - Cronjob täglich:
0 3 * * * php /pfad/queue.php cleanupfür Retention - Notfall-Pfad: zweite Mail-Adresse, anderer Anbieter, hardcoded Notifier wenn
failed-Rate > Schwellwert
Mehr nicht. Wirklich. Wer hier schon einen Message-Broker reinsetzt, hat das Problem nicht verstanden.
wahrheit ∈ db ; mail ∈ view
In #3 reden wir über das, was als nächstes auftaucht, sobald Nachrichten in einer DB liegen: wer liest sie eigentlich? Admin-Dashboard, Auth, und warum ich Auth-Code lieber nicht selber schreibe.
Wer Daten ernst nimmt, schreibt sie zuerst in seine eigene DB. Alles andere ist Glaube an SaaS-Verfügbarkeit. Und Glaube ist keine Backup-Strategie.
Euer Mehmet