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}