package imapimport import ( "bufio" "context" "fmt" "net" "strconv" "strings" ) // RealClient spricht echtes IMAP4rev1 (RFC 3501) über TCP — genutzt für // den realistischen Testpostfach-Nachweis (Pflichtprüfung 3) gegen den // echten ING-01-Server, und produktiv gegen jeden RFC-3501-konformen // IMAP-Server. Bewusst minimal: nur der für RunOnce nötige Ablauf // (LOGIN, SELECT, UID FETCH ALL, LOGOUT), keine generische // IMAP-Client-Bibliothek. type RealClient struct { addr string username string password string dialer net.Dialer } func NewRealClient(addr, username, password string) *RealClient { return &RealClient{addr: addr, username: username, password: password} } func (c *RealClient) Sync(ctx context.Context, mailbox string) (uint64, []RemoteMessage, error) { conn, err := c.dialer.DialContext(ctx, "tcp", c.addr) if err != nil { return 0, nil, fmt.Errorf("imapimport: verbindung aufbauen: %w", err) } defer func() { _ = conn.Close() }() if deadline, ok := ctx.Deadline(); ok { _ = conn.SetDeadline(deadline) } reader := bufio.NewReader(conn) // Begrüßung. if _, err := readLine(reader); err != nil { return 0, nil, fmt.Errorf("imapimport: begrüßung lesen: %w", err) } if _, err := sendCommand(conn, reader, 1, "LOGIN "+c.username+" "+c.password); err != nil { return 0, nil, fmt.Errorf("imapimport: login: %w", err) } selectLines, err := sendCommand(conn, reader, 2, "SELECT "+mailbox) if err != nil { return 0, nil, fmt.Errorf("imapimport: select: %w", err) } uidvalidity, err := extractUIDValidity(selectLines) if err != nil { return 0, nil, err } fetchLines, err := sendCommand(conn, reader, 3, "UID FETCH 1:* (FLAGS)") if err != nil { return 0, nil, fmt.Errorf("imapimport: uid fetch: %w", err) } messages := parseFetchLines(fetchLines) _, _ = sendCommand(conn, reader, 4, "LOGOUT") return uidvalidity, messages, nil } func readLine(reader *bufio.Reader) (string, error) { line, err := reader.ReadString('\n') if err != nil { return "", err } return strings.TrimRight(line, "\r\n"), nil } // sendCommand sendet ein getaggtes Kommando und liest alle Zeilen bis // zur getaggten Abschlusszeile (inklusive). Liefert einen Fehler, wenn // die Abschlusszeile nicht "OK" meldet. func sendCommand(conn net.Conn, reader *bufio.Reader, tagN int, command string) ([]string, error) { tag := "C" + strconv.Itoa(tagN) if _, err := conn.Write([]byte(tag + " " + command + "\r\n")); err != nil { return nil, err } var lines []string for { line, err := readLine(reader) if err != nil { return nil, err } lines = append(lines, line) if strings.HasPrefix(line, tag+" ") { if !strings.HasPrefix(line, tag+" OK") { return lines, fmt.Errorf("server meldete: %s", line) } return lines, nil } } } func extractUIDValidity(lines []string) (uint64, error) { for _, line := range lines { idx := strings.Index(line, "UIDVALIDITY ") if idx == -1 { continue } rest := line[idx+len("UIDVALIDITY "):] end := strings.IndexAny(rest, "] ") if end == -1 { end = len(rest) } v, err := strconv.ParseUint(rest[:end], 10, 64) if err != nil { return 0, fmt.Errorf("imapimport: uidvalidity parsen: %w", err) } return v, nil } return 0, fmt.Errorf("imapimport: keine UIDVALIDITY in SELECT-Antwort gefunden") } // parseFetchLines parst Zeilen der Form // "* FETCH (UID FLAGS ())" (siehe mail/internal/imap // writeFetchResults). func parseFetchLines(lines []string) []RemoteMessage { var messages []RemoteMessage for _, line := range lines { if !strings.Contains(line, "FETCH (UID ") { continue } uidIdx := strings.Index(line, "UID ") + len("UID ") rest := line[uidIdx:] spaceIdx := strings.IndexByte(rest, ' ') if spaceIdx == -1 { continue } uid, err := strconv.ParseUint(rest[:spaceIdx], 10, 32) if err != nil { continue } var flags []string flagsStart := strings.Index(line, "FLAGS (") flagsEnd := strings.LastIndex(line, ")") if flagsStart != -1 && flagsEnd > flagsStart { inner := line[flagsStart+len("FLAGS (") : flagsEnd] if inner != "" { flags = strings.Split(inner, " ") } } messages = append(messages, RemoteMessage{UID: uint32(uid), Flags: flags}) } return messages }