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, uidvalidity, 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 } // RFC 3501 §2.3.1.1: UIDVALIDITY ist Pflichtbestandteil der // SELECT-Antwort — Grundlage für IMP-01s Erkennung eines // Ordner-Neuaufbaus. if err := writeUntagged(s.writer, fmt.Sprintf("OK [UIDVALIDITY %d] UIDs valid", uidvalidity)); 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") } return s.writeFetchResults(cmd.Tag, "FETCH", messages) } // handleUIDFetch implementiert "UID FETCH" (RFC 3501 §6.4.8) — wie FETCH, // aber uid-set statt Sequenzsatz, Grundlage für IMP-01s UID-basierten // Delta-Sync. func (s *Session) handleUIDFetch(ctx context.Context, cmd command) bool { if s.state != Selected { return s.writeErr(cmd.Tag, "BAD", "UID FETCH not allowed in "+s.state.String()+" state") } if len(cmd.Args) < 2 { return s.writeErr(cmd.Tag, "BAD", "UID FETCH requires a uid set") } // "*" in einem UID-Satz bedeutet "höchste vorhandene UID", NICHT die // NachrichtenANZAHL (s.mailboxSize) — UIDs können durch Löschungen // weit über der Nachrichtenzahl liegen (siehe mail/internal/ // folderstate, ING-05: UIDs werden nie wiederverwendet). Da // parseSequenceSet einen Bereich materialisiert, wird "*" hier auf // maxOpenEndedUID begrenzt statt auf 2^32-1 — verhindert eine // Milliarden Einträge lange Schleife bei einem einzelnen offenen // Bereich. FetchByUID liefert ohnehin nur tatsächlich vorhandene // UIDs zurück, die Begrenzung ist für reale Postfachgrößen harmlos. uidSet, err := parseSequenceSet(cmd.Args[1], maxOpenEndedUID) if err != nil { return s.writeErr(cmd.Tag, "BAD", "UID FETCH: invalid uid set") } messages, err := s.store.FetchByUID(ctx, s.mailbox, uidSet) if err != nil { return s.writeErr(cmd.Tag, "NO", "UID FETCH failed") } return s.writeFetchResults(cmd.Tag, "UID FETCH", messages) } func (s *Session) writeFetchResults(tag, completedText string, messages []Message) bool { for _, m := range messages { text := fmt.Sprintf("%d FETCH (UID %d FLAGS (%s))", m.SequenceNumber, m.UID, strings.Join(m.Flags, " ")) if err := writeUntagged(s.writer, text); err != nil { return false } } return s.writeErr(tag, "OK", completedText+" 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. // maxOpenEndedUID begrenzt, wie weit ein offener UID-Bereich ("N:*") // materialisiert wird — deckt reale Postfachgrößen komfortabel ab, ohne // bei einem einzelnen Kommando Milliarden Slice-Einträge zu erzeugen. const maxOpenEndedUID = 1_000_000 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 }