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.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package mailer
|
||||
|
||||
import "fmt"
|
||||
|
||||
// headerWriter schreibt E-Mail-Header ausschließlich über diese
|
||||
// strukturierte API (Akzeptanzkriterium 2) — nie über freie
|
||||
// Stringkonkatenation von Feldname und -wert. Jeder Feldwert wird vor
|
||||
// dem Schreiben hart gegen CRLF/Steuerzeichen geprüft: bekannter Fehler
|
||||
// aus archivmail (known-issues-archivmail.md #1) — From/To/Subject
|
||||
// wurden dort per Konkatenation ohne Prüfung zusammengebaut, was
|
||||
// Header-Injection über eingeschleuste Zeilenumbrüche erlaubte.
|
||||
type headerWriter struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// WriteField validiert value und hängt bei Erfolg "name: value\r\n" an.
|
||||
// Ein Fehler lässt buf unverändert.
|
||||
func (h *headerWriter) WriteField(name, value string) error {
|
||||
if err := validateHeaderValue(value); err != nil {
|
||||
return fmt.Errorf("mailer: feld %q: %w", name, err)
|
||||
}
|
||||
h.buf = append(h.buf, name...)
|
||||
h.buf = append(h.buf, ':', ' ')
|
||||
h.buf = append(h.buf, value...)
|
||||
h.buf = append(h.buf, '\r', '\n')
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *headerWriter) Bytes() []byte { return h.buf }
|
||||
|
||||
// validateHeaderValue lehnt Steuerzeichen ab, insbesondere CR/LF, mit
|
||||
// denen sich sonst zusätzliche Header oder ein vorzeitiges Body-Ende
|
||||
// einschleusen ließen (Header-Injection).
|
||||
func validateHeaderValue(value string) error {
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case r == '\r' || r == '\n':
|
||||
return fmt.Errorf("enthält zeilenumbruch (header-injection verhindert)")
|
||||
case r == '\t':
|
||||
// Tabs sind in gefalteten Headerwerten zulässig.
|
||||
case r < 0x20:
|
||||
return fmt.Errorf("enthält steuerzeichen 0x%02x", r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user