Files
nexarch/mail/docs/ING-03-PRUEFPROTOKOLL.md
T
sysops 6631bcbbdd feat(mail): ING-03 SMTP-Server & Mailer (RFC 5321)
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.
2026-09-01 00:59:56 +02:00

5.6 KiB

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_InvalidSenderCleanError553 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.