search.go

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