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