Neues Paket mail/internal/smtp: SMTP-Server für eingehende Mails, von Grund auf implementiert, analog zu mail/internal/imap und mail/internal/pop3 — TCP-Listener mit einer Goroutine pro Verbindung, Session-Zustandsmaschine (Greeting/Ready/MailFromSet/RcptToSet), Kommandos HELO/EHLO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT. Envelope wird schrittweise aufgebaut und validiert (503 bei übersprungenen Schritten, 553 bei ungültiger Absender-/Empfängeradresse), Nachrichtengröße wird während DATA laufend gegen eine konfigurierbare Höchstgröße geprüft (552 bei Überschreitung, Sink bekommt die Nachricht nicht). Dot-Stuffing beim Empfang korrekt rückgängig gemacht. Neues Paket mail/internal/mailer: Mailer-Komponente für ausgehende Nachrichten. headerWriter ist die einzige Stelle, an der Header geschrieben werden — jeder Feldwert wird hart gegen CR/LF/Steuerzeichen geprüft, bevor er in die Nachricht geschrieben wird. Behebt den bekannten archivmail-Fehler (Header-Injection durch Stringkonkatenation ohne CRLF-Prüfung, siehe known-issues-archivmail.md #1). Sender.Send überträgt per echtem net/smtp-Client (Standardbibliothek) — keine Zugangsdaten im Code, Zieladresse kommt vom Aufrufer. Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt: CRLF-/Steuerzeichen-Injection in Betreff und Anzeigenamen schlägt fehl (vier Testfälle), Ende-zu-Ende-Header-Integritätstest über echten SMTP-Dialog (Mailpit/MailHog nicht installierbar auf diesem Rechner — Ersatz durch den in dieser Kachel gebauten echten SMTP-Server, kein Mock, im Prüfprotokoll begründet), Lasttest mit 50 gleichzeitigen Verbindungen ohne Goroutine-/Verbindungsleck. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~26 Pakete) regressionsfrei getestet.
117 lines
5.6 KiB
Markdown
117 lines
5.6 KiB
Markdown
# ING-03 — SMTP-Server & Mailer: Prüfprotokoll
|
|
|
|
Datum: 2026-09-01
|
|
Host: 192.168.1.131 (Build/Test/Lint), rsync + ssh
|
|
Pakete: `mail/internal/smtp` (SMTP-Server, neu), `mail/internal/mailer` (Mailer-Komponente, neu)
|
|
|
|
## Umsetzung
|
|
|
|
**`mail/internal/smtp`** — SMTP-Server (RFC 5321) für eingehende Mails,
|
|
von Grund auf implementiert, architektonisch analog zu
|
|
`mail/internal/imap`/`pop3`: TCP-Listener mit einer Goroutine pro
|
|
Verbindung, Session-Zustandsmaschine (Greeting → Ready → MailFromSet →
|
|
RcptToSet), Kommandos HELO/EHLO, MAIL FROM, RCPT TO, DATA, RSET, NOOP,
|
|
QUIT. Envelope-Aufbau ist strikt schrittweise: MAIL FROM ohne HELO,
|
|
RCPT TO ohne MAIL FROM und DATA ohne mindestens ein gültiges RCPT TO
|
|
werden jeweils mit `503` zurückgewiesen. Absender-/Empfängeradressen
|
|
werden vor Annahme validiert (`503`/`553` bei ungültiger Syntax bzw.
|
|
Steuerzeichen). Die Nachrichtengröße wird während des DATA-Empfangs
|
|
laufend geprüft; eine Überschreitung führt zu `552` und verworfener
|
|
Nachricht, ohne den Sink zu erreichen. Dot-(Byte-)Stuffing wird beim
|
|
Empfang korrekt rückgängig gemacht (RFC 5321 §4.5.2).
|
|
|
|
**`mail/internal/mailer`** — Mailer-Komponente für ausgehende
|
|
Nachrichten. `headerWriter` (`header.go`) ist die EINZIGE Stelle, an der
|
|
Header geschrieben werden: jeder Feldwert wird vor dem Schreiben hart
|
|
gegen CR/LF/Steuerzeichen geprüft, `Message.Build()` nutzt
|
|
ausschließlich diese API — keine freie Stringkonkatenation von
|
|
From/To/Subject (behebt den bekannten archivmail-Fehler #1,
|
|
Header-Injection durch ungeprüfte Konkatenation). `Sender.Send`
|
|
überträgt die gebaute Nachricht per echtem `net/smtp`-Client
|
|
(Standardbibliothek, reale TCP-Verbindung) über HELO/MAIL FROM/RCPT
|
|
TO/DATA. Keine Zugangsdaten im Code — die Zieladresse wird als
|
|
Parameter/Umgebungsvariable vom Aufrufer bereitgestellt.
|
|
|
|
## Pflichtprüfung 1: Steuerzeichen/CRLF in Betreff und Anzeigenamen — kein Header-Bruch möglich
|
|
|
|
`TestHeaderWriter_RejectsControlCharsAndCRLFInSubjectAndDisplayName`
|
|
(`mailer/mailer_test.go`), vier Fälle: CRLF im Betreff (versuchte
|
|
Bcc-Injection), CRLF im Anzeigenamen des Absenders, nackter LF ohne CR,
|
|
Steuerzeichen NUL im Betreff — `Message.Build()` liefert in allen vier
|
|
Fällen einen Fehler, KEINE gebaute Nachricht. Ergänzend
|
|
`TestHeaderWriter_AcceptsCleanValues`: normale Werte (inkl. Umlaute)
|
|
werden nicht fälschlich abgelehnt.
|
|
|
|
Ergebnis: **BESTANDEN**.
|
|
|
|
## Pflichtprüfung 2: automatisierter Test sendet Testmail über Mailpit/MailHog, prüft Header-Integrität
|
|
|
|
**Abweichung von der wörtlichen Ticketvorgabe, dokumentiert:** Mailpit
|
|
und MailHog sind auf diesem Rechner NICHT installiert — Projektregel
|
|
verbietet das Nachinstallieren zusätzlicher Toolchains/Dienste
|
|
(kein Docker verfügbar, keine Systempaketinstallation). Als echter
|
|
Ersatz — kein Mock, kein fabriziertes Transkript, dieselbe Konvention
|
|
wie die manuellen Client-Tests aus ING-01/ING-02 — läuft
|
|
`TestSender_SendRealMessageOverSMTP_HeaderIntegrity`
|
|
(`mailer/mailer_test.go`) gegen den in dieser Kachel gebauten, echten
|
|
`mail/internal/smtp`-Server: realer TCP-Listener, echter
|
|
`net/smtp`-Standardbibliotheks-Client, reale HELO/MAIL FROM/RCPT
|
|
TO/DATA-Sequenz über das Netzwerk. Geprüft wird:
|
|
|
|
- Envelope (`From`/`To`) kommt beim Server unverändert an.
|
|
- From-, To-, Subject- und ein zusätzlicher Header (`X-NEXARCH-Test`)
|
|
kommen byte-identisch als eigene Headerzeilen an.
|
|
- Genau eine Leerzeile trennt Header von Body (`\r\n\r\n`), Body-Text
|
|
vollständig und unverändert.
|
|
|
|
Ergebnis: **BESTANDEN** — Header-Integrität über einen echten
|
|
Ende-zu-Ende-SMTP-Dialog bestätigt.
|
|
|
|
## Pflichtprüfung 3: Lasttest mit gleichzeitigen Verbindungen ohne Verbindungsleck
|
|
|
|
`TestServer_ConcurrentConnectionsNoLeak` (`smtp/smtp_test.go`): 50
|
|
parallele reale TCP-Verbindungen, jede vollständige
|
|
EHLO/MAIL/RCPT/DATA/QUIT-Sequenz. Alle 50 Nachrichten kommen beim Sink
|
|
an. `runtime.NumGoroutine()` vor und nach dem Lasttest verglichen (mit
|
|
Toleranz für Laufzeit-Jitter und Aufräumzeit).
|
|
|
|
Ergebnis: **BESTANDEN** — Goroutinezahl kehrt auf den Ausgangswert
|
|
zurück, kein Verbindungs-/Ressourcenleck.
|
|
|
|
## Akzeptanzkriterien
|
|
|
|
1. **SMTP-Annahme validiert Envelope und Nachrichtengröße vor der
|
|
Annahme**: `TestSession_EnvelopeMustBeBuiltBeforeData` (schrittweise
|
|
Envelope-Prüfung, `503` bei übersprungenen Schritten) und
|
|
`TestData_MessageSizeCheckedBeforeAcceptance` (Überschreitung der
|
|
konfigurierten Höchstgröße führt zu `552`, Sink bekommt die
|
|
Nachricht NICHT, Session danach weiter funktionsfähig).
|
|
2. **Mailer erzeugt Header ausschließlich über strukturierte
|
|
Writer-API, keine freie Stringkonkatenation**: `header.go`
|
|
(`headerWriter.WriteField`) ist der einzige Ort, an dem
|
|
`Message.Build()` Header schreibt; durch Pflichtprüfung 1 belegt.
|
|
3. **Ungültige Empfängerdaten führen zu sauberer SMTP-Fehlermeldung
|
|
statt Absturz**: `TestRcptTo_InvalidRecipientCleanError` und
|
|
`TestMailFrom_InvalidSenderCleanError` — `553` bei ungültiger
|
|
Adresse, Verbindung bleibt danach nutzbar.
|
|
|
|
## Build/Vet/Lint/Test — Gesamtmodul
|
|
|
|
```
|
|
go build ./... → OK
|
|
go vet ./... → OK
|
|
golangci-lint run ./... → 0 issues
|
|
go test ./... -p 1 (TEST_TENANT_DSN, TEST_MANTICORE_URL gesetzt) → alle Pakete ok, inkl. neuen internal/smtp und internal/mailer
|
|
```
|
|
|
|
Keine Regression in den bestehenden ~26 Paketen.
|
|
|
|
## Ergebnis
|
|
|
|
ING-03 erfüllt alle Akzeptanzkriterien mit echten, ausgeführten
|
|
Nachweisen. Pflichtprüfung 2 wurde mangels installierbarem
|
|
Mailpit/MailHog gegen den eigenen, in dieser Kachel gebauten
|
|
SMTP-Server durchgeführt (funktional gleichwertig: echter SMTP-Dialog,
|
|
kein Mock) — siehe Abschnitt oben. Freigeschaltet: ING-06, ING-08,
|
|
ING-09, ING-10, QA-04, QA-07.
|