search.go

  1package fetcher
  2
  3import (
  4	"fmt"
  5	"sort"
  6
  7	"github.com/emersion/go-imap/v2"
  8	"github.com/floatpane/matcha/backend"
  9	"github.com/floatpane/matcha/config"
 10)
 11
 12// SearchMailbox searches a mailbox server-side and fetches matching envelopes.
 13func SearchMailbox(account *config.Account, folder string, query backend.SearchQuery) ([]Email, error) {
 14	c, err := connect(account)
 15	if err != nil {
 16		return nil, err
 17	}
 18	defer c.Close() //nolint:errcheck
 19
 20	if _, err := c.Select(folder, nil).Wait(); err != nil {
 21		return nil, err
 22	}
 23
 24	criteria := buildSearchCriteria(query)
 25	options := (*imap.SearchOptions)(nil)
 26	if caps := c.Caps(); caps.Has(imap.CapESearch) || caps.Has(imap.CapIMAP4rev2) {
 27		options = &imap.SearchOptions{ReturnAll: true}
 28	}
 29
 30	searchData, err := c.UIDSearch(criteria, options).Wait()
 31	if err != nil && options != nil {
 32		searchData, err = c.UIDSearch(criteria, nil).Wait()
 33	}
 34	if err != nil {
 35		return nil, fmt.Errorf("imap search: %w", err)
 36	}
 37
 38	uids := searchData.AllUIDs()
 39	if len(uids) == 0 {
 40		return []Email{}, nil
 41	}
 42
 43	sort.Slice(uids, func(i, j int) bool {
 44		return uids[i] > uids[j]
 45	})
 46	if limit := searchLimit(query); len(uids) > int(limit) {
 47		uids = uids[:limit]
 48	}
 49
 50	var uidSet imap.UIDSet
 51	for _, uid := range uids {
 52		uidSet.AddNum(uid)
 53	}
 54
 55	msgs, err := c.Fetch(uidSet, &imap.FetchOptions{
 56		Envelope: true,
 57		UID:      true,
 58		Flags:    true,
 59	}).Collect()
 60	if err != nil {
 61		return nil, fmt.Errorf("imap search fetch: %w", err)
 62	}
 63
 64	emails := make([]Email, 0, len(msgs))
 65	for _, msg := range msgs {
 66		if msg.Envelope == nil {
 67			continue
 68		}
 69		email := Email{
 70			UID:       uint32(msg.UID),
 71			Subject:   decodeHeader(msg.Envelope.Subject),
 72			Date:      msg.Envelope.Date,
 73			IsRead:    hasSeenFlag(msg.Flags),
 74			MessageID: msg.Envelope.MessageID,
 75			AccountID: account.ID,
 76		}
 77		if len(msg.Envelope.From) > 0 {
 78			email.From = formatAddress(msg.Envelope.From[0])
 79		}
 80		for _, addr := range msg.Envelope.To {
 81			email.To = append(email.To, addr.Addr())
 82		}
 83		for _, addr := range msg.Envelope.Cc {
 84			email.To = append(email.To, addr.Addr())
 85		}
 86		for _, addr := range msg.Envelope.ReplyTo {
 87			email.ReplyTo = append(email.ReplyTo, addr.Addr())
 88		}
 89		emails = append(emails, email)
 90	}
 91	sort.Slice(emails, func(i, j int) bool {
 92		return emails[i].UID > emails[j].UID
 93	})
 94
 95	return emails, nil
 96}
 97
 98func buildSearchCriteria(query backend.SearchQuery) *imap.SearchCriteria {
 99	criteria := &imap.SearchCriteria{}
100	if query.From != "" {
101		criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{Key: "From", Value: query.From})
102	}
103	if query.To != "" {
104		criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{Key: "To", Value: query.To})
105	}
106	if query.Subject != "" {
107		criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{Key: "Subject", Value: query.Subject})
108	}
109	if query.Body != "" {
110		criteria.Body = []string{query.Body}
111	}
112	if !query.Since.IsZero() {
113		criteria.Since = query.Since
114	}
115	if !query.Before.IsZero() {
116		criteria.Before = query.Before
117	}
118	if query.LargerThan > 0 {
119		criteria.Larger = int64(query.LargerThan)
120	}
121	return criteria
122}
123
124func searchLimit(query backend.SearchQuery) uint32 {
125	if query.Limit > 0 {
126		return query.Limit
127	}
128	return 100
129}