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.
47 lines
1.5 KiB
Go
47 lines
1.5 KiB
Go
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
|
|
}
|