package mail import ( "fmt" "io" "sort" "time" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" ) type IMAPConfig struct { Host string Port int Security string // tls | starttls Username string Password string } type Client struct { c *client.Client } func Dial(cfg IMAPConfig) (*Client, error) { addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) var ( ic *client.Client err error ) switch cfg.Security { case "tls": ic, err = client.DialTLS(addr, nil) case "starttls": ic, err = client.Dial(addr) if err == nil { err = ic.StartTLS(nil) } default: return nil, fmt.Errorf("unknown imap security %q", cfg.Security) } if err != nil { return nil, err } if err := ic.Login(cfg.Username, cfg.Password); err != nil { ic.Logout() return nil, err } return &Client{c: ic}, nil } func (c *Client) Logout() error { return c.c.Logout() } func (c *Client) SelectFolder(folder string) (uint32, uint32, error) { mbox, err := c.c.Select(folder, true) // read-only select if err != nil { return 0, 0, err } var maxUID uint32 if mbox.Messages > 0 { // UIDNext-1 is an upper bound for the highest existing UID. if mbox.UidNext > 0 { maxUID = mbox.UidNext - 1 } } return mbox.UidValidity, maxUID, nil } func (c *Client) fetchByUIDSet(folder string, set *imap.SeqSet, full bool) ([]Message, error) { if _, err := c.c.Select(folder, true); err != nil { return nil, err } section := &imap.BodySectionName{Peek: true} // entire message, no \Seen side-effect items := []imap.FetchItem{imap.FetchUid, section.FetchItem()} msgCh := make(chan *imap.Message, 16) done := make(chan error, 1) go func() { done <- c.c.UidFetch(set, items, msgCh) }() var out []Message for m := range msgCh { r := m.GetBody(section) if r == nil { continue } raw, _ := io.ReadAll(r) parsed, err := ParseMessage(m.Uid, raw) if err != nil { return nil, err } if !full { parsed.Attachments = nil parsed.BodyText = "" } out = append(out, parsed) } if err := <-done; err != nil { return nil, err } sort.Slice(out, func(i, j int) bool { return out[i].Header.UID > out[j].Header.UID }) return out, nil } func (c *Client) FetchHeaders(folder string, uids []uint32) ([]Header, error) { set := new(imap.SeqSet) if len(uids) == 0 { set.AddRange(1, 0) // 1:* — all } else { for _, u := range uids { set.AddNum(u) } } msgs, err := c.fetchByUIDSet(folder, set, false) if err != nil { return nil, err } return headersOf(msgs), nil } func (c *Client) FetchHeadersRange(folder string, sinceUID, beforeUID uint32, limit int) ([]Header, error) { set := new(imap.SeqSet) lo := sinceUID + 1 if sinceUID == 0 { lo = 1 } hi := uint32(0) // '*' if beforeUID > 1 { hi = beforeUID - 1 } set.AddRange(lo, hi) msgs, err := c.fetchByUIDSet(folder, set, false) if err != nil { return nil, err } h := headersOf(msgs) if limit > 0 && len(h) > limit { h = h[:limit] } return h, nil } func (c *Client) FetchFull(folder string, uid uint32) (Message, error) { set := new(imap.SeqSet) set.AddNum(uid) msgs, err := c.fetchByUIDSet(folder, set, true) if err != nil { return Message{}, err } if len(msgs) == 0 { return Message{}, fmt.Errorf("uid %d not found in %s", uid, folder) } return msgs[0], nil } type SearchCriteria struct { From string SubjectContains string Text string Since time.Time Before time.Time } func (c *Client) Search(folder string, sc SearchCriteria, limit int) ([]Header, error) { if _, err := c.c.Select(folder, true); err != nil { return nil, err } crit := imap.NewSearchCriteria() if sc.From != "" { crit.Header.Add("From", sc.From) } if sc.SubjectContains != "" { crit.Header.Add("Subject", sc.SubjectContains) } if sc.Text != "" { crit.Text = []string{sc.Text} } if !sc.Since.IsZero() { crit.Since = sc.Since } if !sc.Before.IsZero() { crit.Before = sc.Before } uids, err := c.c.UidSearch(crit) if err != nil { return nil, err } if len(uids) == 0 { return nil, nil } return c.FetchHeaders(folder, uids) } func headersOf(msgs []Message) []Header { out := make([]Header, 0, len(msgs)) for _, m := range msgs { out = append(out, m.Header) } return out }