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.
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
- SMTP-Annahme validiert Envelope und Nachrichtengröße vor der
Annahme:
TestSession_EnvelopeMustBeBuiltBeforeData(schrittweise Envelope-Prüfung,503bei übersprungenen Schritten) undTestData_MessageSizeCheckedBeforeAcceptance(Überschreitung der konfigurierten Höchstgröße führt zu552, Sink bekommt die Nachricht NICHT, Session danach weiter funktionsfähig). - Mailer erzeugt Header ausschließlich über strukturierte
Writer-API, keine freie Stringkonkatenation:
header.go(headerWriter.WriteField) ist der einzige Ort, an demMessage.Build()Header schreibt; durch Pflichtprüfung 1 belegt. - Ungültige Empfängerdaten führen zu sauberer SMTP-Fehlermeldung
statt Absturz:
TestRcptTo_InvalidRecipientCleanErrorundTestMailFrom_InvalidSenderCleanError—553bei 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.