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
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
package imap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// handleCapability ist in jedem Zustand erlaubt (RFC 3501 §6.1.1).
|
|
func (s *Session) handleCapability(cmd command) bool {
|
|
if err := writeUntagged(s.writer, "CAPABILITY IMAP4rev1"); err != nil {
|
|
return false
|
|
}
|
|
return s.writeErr(cmd.Tag, "OK", "CAPABILITY completed")
|
|
}
|
|
|
|
// handleLogin ist nur im Zustand NotAuthenticated erlaubt
|
|
// (Akzeptanzkriterium 1/3).
|
|
func (s *Session) handleLogin(ctx context.Context, cmd command) bool {
|
|
if s.state != NotAuthenticated {
|
|
return s.writeErr(cmd.Tag, "BAD", "LOGIN not allowed in "+s.state.String()+" state")
|
|
}
|
|
if len(cmd.Args) != 2 {
|
|
return s.writeErr(cmd.Tag, "BAD", "LOGIN requires username and password")
|
|
}
|
|
if s.auth == nil {
|
|
return s.writeErr(cmd.Tag, "NO", "LOGIN not available")
|
|
}
|
|
|
|
ok, err := s.auth.Authenticate(ctx, cmd.Args[0], cmd.Args[1])
|
|
if err != nil {
|
|
return s.writeErr(cmd.Tag, "NO", "LOGIN failed")
|
|
}
|
|
if !ok {
|
|
return s.writeErr(cmd.Tag, "NO", "LOGIN failed")
|
|
}
|
|
s.state = Authenticated
|
|
return s.writeErr(cmd.Tag, "OK", "LOGIN completed")
|
|
}
|
|
|
|
// handleSelect ist in Authenticated und Selected erlaubt (ein erneutes
|
|
// SELECT wechselt das gewählte Postfach).
|
|
func (s *Session) handleSelect(ctx context.Context, cmd command) bool {
|
|
if s.state == NotAuthenticated {
|
|
return s.writeErr(cmd.Tag, "BAD", "SELECT not allowed in "+s.state.String()+" state")
|
|
}
|
|
if len(cmd.Args) != 1 {
|
|
return s.writeErr(cmd.Tag, "BAD", "SELECT requires a mailbox name")
|
|
}
|
|
if s.store == nil {
|
|
return s.writeErr(cmd.Tag, "NO", "SELECT not available")
|
|
}
|
|
|
|
mailboxName := cmd.Args[0]
|
|
exists, ok, err := s.store.Select(ctx, mailboxName)
|
|
if err != nil || !ok {
|
|
// Fehlgeschlagenes SELECT lässt den Zustand laut RFC 3501 §6.3.1
|
|
// auf Authenticated zurückfallen, nie in Selected mit ungültigem
|
|
// Postfach hängen bleiben.
|
|
s.state = Authenticated
|
|
return s.writeErr(cmd.Tag, "NO", "SELECT failed: no such mailbox")
|
|
}
|
|
|
|
if err := writeUntagged(s.writer, fmt.Sprintf("%d EXISTS", exists)); err != nil {
|
|
return false
|
|
}
|
|
s.state = Selected
|
|
s.mailbox = mailboxName
|
|
s.mailboxSize = uint32(exists)
|
|
return s.writeErr(cmd.Tag, "OK", "[READ-WRITE] SELECT completed")
|
|
}
|
|
|
|
// handleFetch ist ausschließlich im Zustand Selected erlaubt
|
|
// (Akzeptanzkriterium 1/2).
|
|
func (s *Session) handleFetch(ctx context.Context, cmd command) bool {
|
|
if s.state != Selected {
|
|
return s.writeErr(cmd.Tag, "BAD", "FETCH not allowed in "+s.state.String()+" state")
|
|
}
|
|
if len(cmd.Args) < 1 {
|
|
return s.writeErr(cmd.Tag, "BAD", "FETCH requires a sequence set")
|
|
}
|
|
|
|
seqNumbers, err := parseSequenceSet(cmd.Args[0], s.mailboxSize)
|
|
if err != nil {
|
|
return s.writeErr(cmd.Tag, "BAD", "FETCH: invalid sequence set")
|
|
}
|
|
|
|
messages, err := s.store.Fetch(ctx, s.mailbox, seqNumbers)
|
|
if err != nil {
|
|
return s.writeErr(cmd.Tag, "NO", "FETCH failed")
|
|
}
|
|
for _, m := range messages {
|
|
text := fmt.Sprintf("%d FETCH (FLAGS (%s))", m.SequenceNumber, strings.Join(m.Flags, " "))
|
|
if err := writeUntagged(s.writer, text); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return s.writeErr(cmd.Tag, "OK", "FETCH completed")
|
|
}
|
|
|
|
// handleLogout ist in jedem Zustand erlaubt und beendet die Sitzung.
|
|
func (s *Session) handleLogout(cmd command) bool {
|
|
if err := writeUntagged(s.writer, "BYE IMAP4rev1 Server logging out"); err != nil {
|
|
return false
|
|
}
|
|
_ = s.writeErr(cmd.Tag, "OK", "LOGOUT completed")
|
|
return false
|
|
}
|
|
|
|
// parseSequenceSet unterstützt die für FETCH gebräuchlichsten Formen:
|
|
// eine einzelne Zahl ("1"), eine kommagetrennte Liste ("1,3,5") und einen
|
|
// Bereich mit "*" als offenem Ende ("1:*"), aufgelöst gegen maxSeq (die
|
|
// tatsächliche Nachrichtenzahl des gewählten Postfachs, von SELECT
|
|
// gemeldet). Volle RFC-3501-Sequenzsatz-Grammatik (verschachtelte
|
|
// Bereiche etc.) ist bewusst nicht Bestandteil dieser kleinsten Lösung.
|
|
func parseSequenceSet(raw string, maxSeq uint32) ([]uint32, error) {
|
|
var result []uint32
|
|
for _, part := range strings.Split(raw, ",") {
|
|
if strings.Contains(part, ":") {
|
|
bounds := strings.SplitN(part, ":", 2)
|
|
if len(bounds) != 2 {
|
|
return nil, fmt.Errorf("imap: ungültiger bereich %q", part)
|
|
}
|
|
from, err := strconv.ParseUint(bounds[0], 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
to := uint64(maxSeq)
|
|
if bounds[1] != "*" {
|
|
to, err = strconv.ParseUint(bounds[1], 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for i := from; i <= to; i++ {
|
|
result = append(result, uint32(i))
|
|
}
|
|
continue
|
|
}
|
|
n, err := strconv.ParseUint(part, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, uint32(n))
|
|
}
|
|
return result, nil
|
|
}
|