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) 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 }