package imap import ( "bufio" "context" "errors" "io" "net" "strings" ) // maxCommandLineBytes begrenzt eine einzelne Kommandozeile (Defensive // Fehlerbehandlung bei nicht-konformen Gegenstellen statt optimistischem // Parsing, siehe Ticket-Vorbild Dovecot) — verhindert unbegrenztes // Pufferwachstum durch eine Gegenstelle, die niemals CRLF sendet. const maxCommandLineBytes = 8192 // Session ist eine einzelne IMAP-Verbindung mit eigener // Zustandsmaschine (Akzeptanzkriterium 1). type Session struct { conn net.Conn reader *bufio.Reader writer *bufio.Writer auth Authenticator store MailboxStore state State mailbox string // gewähltes Postfach im Zustand Selected mailboxSize uint32 // Nachrichtenzahl aus dem letzten erfolgreichen SELECT } func newSession(conn net.Conn, auth Authenticator, store MailboxStore) *Session { return &Session{ conn: conn, reader: bufio.NewReaderSize(conn, maxCommandLineBytes), writer: bufio.NewWriter(conn), auth: auth, store: store, state: NotAuthenticated, } } // State liefert den aktuellen Sitzungszustand (für Tests). func (s *Session) State() State { return s.state } // Serve führt die Sitzung bis LOGOUT oder Verbindungsende aus. func (s *Session) Serve(ctx context.Context) { defer func() { _ = s.conn.Close() }() if err := writeUntagged(s.writer, "OK IMAP4rev1 Service Ready"); err != nil { return } for { line, err := s.readLine() if err != nil { return } if line == "" { continue } cmd, parseErr := parseCommandLine(line) if parseErr != nil { // Akzeptanzkriterium 3: ungültige Kommandosequenz -> Fehlercode, // KEIN Verbindungsabbruch. if err := writeUntagged(s.writer, "BAD Error in IMAP command received by server."); err != nil { return } continue } if !s.dispatch(ctx, cmd) { return // LOGOUT oder nicht behebbarer Schreibfehler } } } // readLine liest eine CRLF- (oder LF-)terminierte Zeile ohne // Zeilenumbruch. Überlange Zeilen (siehe maxCommandLineBytes) werden als // Fehler behandelt statt unbegrenzt zu puffern. func (s *Session) readLine() (string, error) { line, err := s.reader.ReadString('\n') if err != nil { if errors.Is(err, io.EOF) && line != "" { // Letzte Zeile ohne abschließendes LF — als vollständige Zeile // behandeln, danach ohnehin Verbindungsende. return strings.TrimRight(line, "\r"), nil } return "", err } return strings.TrimRight(line, "\r\n"), nil } // dispatch verarbeitet EIN geparstes Kommando. Rückgabewert false // bedeutet: Sitzung beenden (LOGOUT abgeschlossen oder Schreibfehler). func (s *Session) dispatch(ctx context.Context, cmd command) bool { switch cmd.Name { case "CAPABILITY": return s.handleCapability(cmd) case "LOGIN": return s.handleLogin(ctx, cmd) case "SELECT": return s.handleSelect(ctx, cmd) case "FETCH": return s.handleFetch(ctx, cmd) case "LOGOUT": return s.handleLogout(cmd) default: return s.writeErr(cmd.Tag, "BAD", "Unknown command") } } func (s *Session) writeErr(tag, status, text string) bool { return writeTagged(s.writer, tag, status, text) == nil }