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.
60 lines
1.7 KiB
Go
60 lines
1.7 KiB
Go
package smtp
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// parseCommand zerlegt eine Kommandozeile in Verb (großgeschrieben) und
|
|
// restliches Argument.
|
|
func parseCommand(line string) (verb, arg string) {
|
|
parts := strings.SplitN(strings.TrimSpace(line), " ", 2)
|
|
verb = strings.ToUpper(parts[0])
|
|
if len(parts) == 2 {
|
|
arg = strings.TrimSpace(parts[1])
|
|
}
|
|
return verb, arg
|
|
}
|
|
|
|
// parseMailAddressArg extrahiert die Adresse aus "FROM:<addr>" bzw.
|
|
// "TO:<addr>" (RFC 5321 §4.1.1.2/4.1.1.3). SMTP-Parameter wie SIZE=...
|
|
// werden für diese kleinste Lösung ignoriert.
|
|
func parseMailAddressArg(arg, keyword string) (string, error) {
|
|
trimmed := strings.TrimSpace(arg)
|
|
upper := strings.ToUpper(trimmed)
|
|
prefix := keyword + ":"
|
|
if !strings.HasPrefix(upper, prefix) {
|
|
return "", fmt.Errorf("smtp: erwartete %q am anfang von %q", prefix, arg)
|
|
}
|
|
rest := strings.TrimSpace(trimmed[len(prefix):])
|
|
if sp := strings.IndexByte(rest, ' '); sp >= 0 {
|
|
rest = rest[:sp]
|
|
}
|
|
rest = strings.TrimPrefix(rest, "<")
|
|
rest = strings.TrimSuffix(rest, ">")
|
|
if rest == "" {
|
|
return "", fmt.Errorf("smtp: leere adresse")
|
|
}
|
|
return rest, nil
|
|
}
|
|
|
|
// validateAddress prüft eine E-Mail-Adresse defensiv gegen
|
|
// Steuerzeichen und offensichtlich falsche Form (Akzeptanzkriterium 3:
|
|
// ungültige Empfänger-/Absenderdaten führen zu sauberer Fehlermeldung
|
|
// statt Absturz).
|
|
func validateAddress(addr string) error {
|
|
for _, r := range addr {
|
|
if r < 0x20 || r == 0x7f {
|
|
return fmt.Errorf("smtp: steuerzeichen in adresse")
|
|
}
|
|
}
|
|
at := strings.IndexByte(addr, '@')
|
|
if at <= 0 || at == len(addr)-1 {
|
|
return fmt.Errorf("smtp: ungültige adresse %q", addr)
|
|
}
|
|
if strings.IndexByte(addr[at+1:], '@') >= 0 {
|
|
return fmt.Errorf("smtp: ungültige adresse %q", addr)
|
|
}
|
|
return nil
|
|
}
|