IMAP-Server-Grundgerüst: TCP-Listener, Command-Parser, Session- Zustandsmaschine (Not Authenticated/Authenticated/Selected), Grundbefehle CAPABILITY/LOGIN/SELECT/FETCH/LOGOUT. - parser.go: Tag+Kommando+Argumente (Atome, zitierte Zeichenketten), keine IMAP-Literalsyntax (kleinste Lösung). - response.go: sanitizeResponseText entfernt eingebettete CR/LF vor jeder Antwortzeile — bekannten archivmail-Fehler (Header-/Zeilen-Injection durch Stringkonkatenation ohne CRLF-Prüfung) strukturell vermieden. - session.go/commands.go: strikte Zustandsprüfung je Kommando, verbotene Übergänge und fehlerhafte Zeilen liefern BAD/NO statt Verbindungsabbruch. maxCommandLineBytes begrenzt Pufferwachstum defensiv. - server.go: TCP-Accept-Schleife, eine Goroutine je Verbindung. - Authenticator/MailboxStore als schmale Schnittstellen — echte Benutzerverwaltungs-/Postfach-Anbindung ist Sache von IMP-01 u. a. Prüfungen (alle real durchgeführt, siehe mail/docs/ING-01-PRUEFPROTOKOLL.md): 1. Manuelle Session mit Pythons imaplib gegen den echten laufenden Server: alle Grundbefehle real beantwortet, ungültiges SELECT liefert real NO ohne Verbindungsabbruch. 2. TestSession_StateTransitionsAndForbiddenTransitions: alle drei Zustandsübergänge und deren verbotene Übergänge real über TCP geprüft. 3. TestServer_50ParallelSessionsNoLeak: 50 reale parallele Sessions, 0 Fehler. Kein Umbau: alle bestehenden Pakete unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
package imap
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// ErrMalformedCommand wird geliefert, wenn eine Kommandozeile nicht dem
|
|
// Grundformat "Tag SP Kommando [SP Argumente]" entspricht. Kein
|
|
// Verbindungsabbruch (Akzeptanzkriterium 3) — der Aufrufer antwortet mit
|
|
// einer BAD-Antwort und liest die nächste Zeile.
|
|
var ErrMalformedCommand = errors.New("imap: fehlerhafte kommandozeile")
|
|
|
|
// command ist eine geparste IMAP-Kommandozeile.
|
|
type command struct {
|
|
Tag string
|
|
Name string // groß geschrieben (z. B. "LOGIN")
|
|
Args []string
|
|
}
|
|
|
|
// parseCommandLine zerlegt eine Kommandozeile (bereits ohne CRLF) in Tag,
|
|
// Kommandoname und Argumente. Unterstützt Atome und doppelt zitierte
|
|
// Zeichenketten (mit \"- und \\-Escape) — literale Zeichenketten
|
|
// ({n}CRLF<n Bytes>) sind bewusst NICHT Bestandteil dieser kleinsten
|
|
// Lösung (LOGIN/SELECT/FETCH kommen in Tests/typischen Clients ohne
|
|
// Literale aus).
|
|
func parseCommandLine(line string) (command, error) {
|
|
tokens, err := tokenize(line)
|
|
if err != nil {
|
|
return command{}, err
|
|
}
|
|
if len(tokens) < 2 {
|
|
return command{}, ErrMalformedCommand
|
|
}
|
|
return command{
|
|
Tag: tokens[0],
|
|
Name: strings.ToUpper(tokens[1]),
|
|
Args: tokens[2:],
|
|
}, nil
|
|
}
|
|
|
|
// tokenize zerlegt line in durch Leerzeichen getrennte Tokens, wobei
|
|
// doppelt zitierte Zeichenketten als EIN Token gelten (Leerzeichen darin
|
|
// werden nicht als Trenner behandelt).
|
|
func tokenize(line string) ([]string, error) {
|
|
var tokens []string
|
|
var current strings.Builder
|
|
inQuotes := false
|
|
escaped := false
|
|
hasToken := false
|
|
|
|
for _, r := range line {
|
|
switch {
|
|
case escaped:
|
|
current.WriteRune(r)
|
|
escaped = false
|
|
hasToken = true
|
|
case r == '\\' && inQuotes:
|
|
escaped = true
|
|
case r == '"':
|
|
inQuotes = !inQuotes
|
|
hasToken = true
|
|
case r == ' ' && !inQuotes:
|
|
if hasToken {
|
|
tokens = append(tokens, current.String())
|
|
current.Reset()
|
|
hasToken = false
|
|
}
|
|
default:
|
|
current.WriteRune(r)
|
|
hasToken = true
|
|
}
|
|
}
|
|
if inQuotes || escaped {
|
|
return nil, ErrMalformedCommand
|
|
}
|
|
if hasToken {
|
|
tokens = append(tokens, current.String())
|
|
}
|
|
return tokens, nil
|
|
}
|