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()
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}