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,56 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// defaultMaxMessageBytes ist die Standard-Höchstgröße einer
|
||||
// angenommenen Nachricht (Akzeptanzkriterium 1).
|
||||
const defaultMaxMessageBytes = 25 * 1024 * 1024 // 25 MiB
|
||||
|
||||
// Server nimmt SMTP-Verbindungen an und bedient jede in einer eigenen
|
||||
// Goroutine — gleiches Muster wie mail/internal/imap.Server und
|
||||
// mail/internal/pop3.Server. TLS/STARTTLS ist Sache von ING-06,
|
||||
// Rate-Limiting Sache von ING-09, Protokoll-Logging Sache von ING-08 —
|
||||
// keine dieser Kacheln.
|
||||
type Server struct {
|
||||
sink MessageSink
|
||||
maxMessageBytes int64
|
||||
}
|
||||
|
||||
func NewServer(sink MessageSink) *Server {
|
||||
return NewServerWithMaxMessageBytes(sink, defaultMaxMessageBytes)
|
||||
}
|
||||
|
||||
// NewServerWithMaxMessageBytes erlaubt eine abweichende
|
||||
// Nachrichten-Höchstgröße, z. B. für Tests.
|
||||
func NewServerWithMaxMessageBytes(sink MessageSink, maxMessageBytes int64) *Server {
|
||||
return &Server{sink: sink, maxMessageBytes: maxMessageBytes}
|
||||
}
|
||||
|
||||
// Serve nimmt Verbindungen auf listener an, bis ctx beendet wird.
|
||||
func (srv *Server) Serve(ctx context.Context, listener net.Listener) error {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = listener.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("smtp: verbindung annehmen: %w", err)
|
||||
}
|
||||
session := newSession(conn, srv.sink, srv.maxMessageBytes)
|
||||
go session.Serve(ctx)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user