// email/client.go – IMAP-Client für Email-Abfragen package email import ( "crypto/tls" "fmt" imap "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapclient" "my-brain-importer/internal/config" ) // Message repräsentiert eine Email (ohne Body für schnelle Übersichten). type Message struct { Subject string From string Date string } // Client wraps die IMAP-Verbindung. type Client struct { c *imapclient.Client } // Connect öffnet eine IMAP-Verbindung. func Connect() (*Client, error) { cfg := config.Cfg.Email addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) var ( c *imapclient.Client err error ) switch { case cfg.TLS: tlsCfg := &tls.Config{ServerName: cfg.Host} c, err = imapclient.DialTLS(addr, &imapclient.Options{TLSConfig: tlsCfg}) case cfg.StartTLS: tlsCfg := &tls.Config{ServerName: cfg.Host} c, err = imapclient.DialStartTLS(addr, &imapclient.Options{TLSConfig: tlsCfg}) default: c, err = imapclient.DialInsecure(addr, nil) } if err != nil { return nil, fmt.Errorf("IMAP verbinden: %w", err) } if err := c.Login(cfg.User, cfg.Password).Wait(); err != nil { c.Close() return nil, fmt.Errorf("IMAP login: %w", err) } return &Client{c: c}, nil } // Close schließt die Verbindung. func (cl *Client) Close() { cl.c.Logout().Wait() cl.c.Close() } // FetchRecent holt die letzten n Emails (Envelope-Daten, kein Body). func (cl *Client) FetchRecent(n uint32) ([]Message, error) { folder := config.Cfg.Email.Folder if folder == "" { folder = "INBOX" } selectData, err := cl.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait() if err != nil { return nil, fmt.Errorf("IMAP select: %w", err) } if selectData.NumMessages == 0 { return nil, nil } start := uint32(1) if selectData.NumMessages > n { start = selectData.NumMessages - n + 1 } var seqSet imap.SeqSet seqSet.AddRange(start, selectData.NumMessages) msgs, err := cl.c.Fetch(seqSet, &imap.FetchOptions{Envelope: true}).Collect() if err != nil { return nil, fmt.Errorf("IMAP fetch: %w", err) } return parseMessages(msgs), nil } // FetchUnread holt ungelesene Emails (Envelope-Daten, kein Body). func (cl *Client) FetchUnread() ([]Message, error) { folder := config.Cfg.Email.Folder if folder == "" { folder = "INBOX" } if _, err := cl.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil { return nil, fmt.Errorf("IMAP select: %w", err) } searchData, err := cl.c.Search(&imap.SearchCriteria{ NotFlag: []imap.Flag{imap.FlagSeen}, }, nil).Wait() if err != nil { return nil, fmt.Errorf("IMAP search: %w", err) } seqNums := searchData.AllSeqNums() if len(seqNums) == 0 { return nil, nil } var seqSet imap.SeqSet seqSet.AddNum(seqNums...) msgs, err := cl.c.Fetch(seqSet, &imap.FetchOptions{Envelope: true}).Collect() if err != nil { return nil, fmt.Errorf("IMAP fetch: %w", err) } return parseMessages(msgs), nil } func parseMessages(msgs []*imapclient.FetchMessageBuffer) []Message { result := make([]Message, 0, len(msgs)) for _, msg := range msgs { if msg.Envelope == nil { continue } m := Message{ Subject: msg.Envelope.Subject, Date: msg.Envelope.Date.Format("2006-01-02 15:04"), } if len(msg.Envelope.From) > 0 { addr := msg.Envelope.From[0] if addr.Name != "" { m.From = fmt.Sprintf("%s <%s@%s>", addr.Name, addr.Mailbox, addr.Host) } else { m.From = fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host) } } result = append(result, m) } return result }