package smtp import ( "bufio" "bytes" "context" "fmt" "strings" "gitea.perlbach24.de/scripte/nexarch/mail/internal/tlscert" ) func (s *Session) handleHelo(verb, arg string) bool { if strings.TrimSpace(arg) == "" { return s.reply(501, verb+" requires a domain/address") == nil } // HELO/EHLO setzt den Envelope zurück, falls bereits einer im // Aufbau war (RFC 5321 §4.1.1.1). s.from = "" s.to = nil s.state = Ready if verb == "EHLO" { lines := []string{"nexarch-mail greets " + arg, "8BITMIME"} if s.tlsConfig != nil && !s.tlsActive { lines = append(lines, "STARTTLS") } return s.replyMultiline(250, lines) == nil } return s.reply(250, "nexarch-mail greets "+arg) == nil } // handleStartTLS implementiert STARTTLS (RFC 3207): TLS-Aufwertung der // bestehenden Klartextverbindung. func (s *Session) handleStartTLS(ctx context.Context) bool { if s.tlsConfig == nil { return s.reply(454, "TLS not available") == nil } if s.tlsActive { return s.reply(503, "already using TLS") == nil } if err := s.reply(220, "Ready to start TLS"); err != nil { return false } tlsConn, err := tlscert.UpgradeServer(ctx, s.conn, s.tlsConfig) if err != nil { // Handshake fehlgeschlagen -> Verbindung beenden, kein Rückfall // auf Klartext. return false } s.conn = tlsConn // Reader/Writer NEU aufsetzen: verhindert, dass vor dem Handshake // gepufferte Klartextdaten nach dem Wechsel noch als Kommandos // verarbeitet werden (Command-Injection-Schutz). s.reader = bufio.NewReaderSize(tlsConn, maxCommandLineBytes) s.writer = bufio.NewWriter(tlsConn) s.tlsActive = true // Envelope nach STARTTLS zurücksetzen (RFC 3207 §4.2). s.from = "" s.to = nil s.state = Ready return true } // handleMailFrom ist Teil des Envelope-Aufbaus (Akzeptanzkriterium 1): // die Absenderadresse wird vor der Annahme validiert. func (s *Session) handleMailFrom(arg string) bool { if s.state == Greeting { return s.reply(503, "send HELO/EHLO first") == nil } addr, err := parseMailAddressArg(arg, "FROM") if err != nil { return s.reply(501, "invalid MAIL FROM syntax") == nil } if err := validateAddress(addr); err != nil { // Akzeptanzkriterium 3: ungültige Absenderdaten -> saubere // SMTP-Fehlermeldung statt Absturz oder Verbindungsabbruch. return s.reply(553, "invalid sender address") == nil } if s.acceptLimiter != nil { if ok, retryAfter := s.acceptLimiter.Allow(s.tenantID, addr+"|"+s.sourceAddr()); !ok { // Akzeptanzkriterium 1/3 (ING-09): definierte, temporäre // Ablehnung (4xx = "try again later") MIT Wartezeit-Hinweis // statt Verbindungsabbruch ohne Erklärung. return s.reply(451, fmt.Sprintf("rate limit exceeded for sender, retry in %.1fs", retryAfter.Seconds())) == nil } } s.from = addr s.to = nil s.state = MailFromSet return s.reply(250, "OK") == nil } // handleRcptTo ist Teil des Envelope-Aufbaus (Akzeptanzkriterium 1): // jede Empfängeradresse wird vor der Annahme validiert; mehrere RCPT TO // sind erlaubt. func (s *Session) handleRcptTo(arg string) bool { if s.state != MailFromSet && s.state != RcptToSet { return s.reply(503, "send MAIL FROM first") == nil } addr, err := parseMailAddressArg(arg, "TO") if err != nil { return s.reply(501, "invalid RCPT TO syntax") == nil } if err := validateAddress(addr); err != nil { // Akzeptanzkriterium 3: ungültige Empfängerdaten -> saubere // SMTP-Fehlermeldung statt Absturz oder Verbindungsabbruch. return s.reply(553, "invalid recipient address") == nil } s.to = append(s.to, addr) s.state = RcptToSet return s.reply(250, "OK") == nil } func (s *Session) handleRset() bool { s.from = "" s.to = nil if s.state != Greeting { s.state = Ready } return s.reply(250, "OK") == nil } // handleData verlangt einen vollständig aufgebauten und validierten // Envelope (Akzeptanzkriterium 1: Envelope UND Nachrichtengröße werden // vor der Annahme geprüft) und liest die dot-gestuffte Nachricht bis zur // Abschlusszeile ".". func (s *Session) handleData(ctx context.Context) bool { if s.state != RcptToSet { return s.reply(503, "send MAIL FROM/RCPT TO first") == nil } if err := s.reply(354, "Start mail input; end with ."); err != nil { return false } var buf bytes.Buffer for { line, err := s.readLine() if err != nil { return false } if line == "." { break } // Byte-Stuffing rückgängig machen (RFC 5321 §4.5.2): eine Zeile, // die mit "." beginnt, verliert genau diesen ersten Punkt. line = strings.TrimPrefix(line, ".") buf.WriteString(line) buf.WriteString("\r\n") if int64(buf.Len()) > s.maxMessageBytes { // Akzeptanzkriterium 1: Nachrichtengröße wird VOR der // endgültigen Annahme geprüft — sauberer Fehlercode statt // unbegrenztem Pufferwachstum. _ = s.drainUntilDot() s.from = "" s.to = nil s.state = Ready return s.reply(552, "message size exceeds fixed maximum message size") == nil } } envelope := Envelope{From: s.from, To: s.to} raw := buf.Bytes() s.from = "" s.to = nil s.state = Ready if s.sink != nil { if err := s.sink.Accept(ctx, envelope, raw); err != nil { return s.reply(451, "unable to accept message, try again later") == nil } } return s.reply(250, "OK: message accepted") == nil }