1package fetcher
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "log"
8 "mime"
9 "strings"
10 "time"
11
12 "github.com/andrinoff/email-cli/config"
13 "github.com/emersion/go-imap"
14 "github.com/emersion/go-imap/client"
15 "github.com/emersion/go-message/mail"
16 "golang.org/x/text/encoding/ianaindex"
17 "golang.org/x/text/transform"
18)
19
20type Email struct {
21 From string
22 To []string
23 Subject string
24 Body string
25 Date time.Time
26 MessageID string
27 References []string
28}
29
30func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
31 mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
32 if err != nil {
33 body, _ := ioutil.ReadAll(reader)
34 return string(body), nil
35 }
36
37 charset := "utf-8" // Default charset
38 if params["charset"] != "" {
39 charset = strings.ToLower(params["charset"])
40 }
41
42 encoding, err := ianaindex.IANA.Encoding(charset)
43 if err != nil || encoding == nil {
44 encoding, _ = ianaindex.IANA.Encoding("utf-8")
45 }
46
47 transformReader := transform.NewReader(reader, encoding.NewDecoder())
48 decodedBody, err := ioutil.ReadAll(transformReader)
49 if err != nil {
50 return "", err
51 }
52
53 if strings.HasPrefix(mediaType, "multipart/") {
54 return "[This is a multipart message]", nil
55 }
56
57 return string(decodedBody), nil
58}
59
60func decodeHeader(header string) string {
61 dec := new(mime.WordDecoder)
62 dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
63 encoding, err := ianaindex.IANA.Encoding(charset)
64 if err != nil {
65 return nil, err
66 }
67 return transform.NewReader(input, encoding.NewDecoder()), nil
68 }
69 decoded, err := dec.DecodeHeader(header)
70 if err != nil {
71 return header
72 }
73 return decoded
74}
75
76// FetchEmails now supports pagination with a limit and offset.
77func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
78 var imapServer string
79 var imapPort int
80
81 switch cfg.ServiceProvider {
82 case "gmail":
83 imapServer = "imap.gmail.com"
84 imapPort = 993
85 case "icloud":
86 imapServer = "imap.mail.me.com"
87 imapPort = 993
88 default:
89 return nil, fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
90 }
91
92 addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
93 c, err := client.DialTLS(addr, nil)
94 if err != nil {
95 return nil, err
96 }
97 log.Println("Connected")
98 defer c.Logout()
99
100 if err := c.Login(cfg.Email, cfg.Password); err != nil {
101 return nil, err
102 }
103 log.Println("Logged in")
104
105 mbox, err := c.Select("INBOX", false)
106 if err != nil {
107 return nil, err
108 }
109
110 // Handle pagination logic
111 if mbox.Messages == 0 {
112 return []Email{}, nil
113 }
114
115 to := mbox.Messages - offset
116 from := uint32(1)
117 if to > limit {
118 from = to - limit + 1
119 }
120
121 if to < 1 {
122 return []Email{}, nil // No more messages to fetch
123 }
124
125 seqset := new(imap.SeqSet)
126 seqset.AddRange(from, to)
127
128 messages := make(chan *imap.Message, limit)
129 done := make(chan error, 1)
130 go func() {
131 done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchItem("BODY[]")}, messages)
132 }()
133
134 var emails []Email
135 for msg := range messages {
136 if msg == nil {
137 continue
138 }
139
140 bodyLiteral := msg.GetBody(&imap.BodySectionName{})
141 if bodyLiteral == nil {
142 log.Println("Could not get message body")
143 continue
144 }
145
146 mr, err := mail.CreateReader(bodyLiteral)
147 if err != nil {
148 log.Printf("Error creating mail reader: %v", err)
149 continue
150 }
151
152 header := mr.Header
153 fromAddrs, _ := header.AddressList("From")
154 toAddrs, _ := header.AddressList("To")
155 subject := decodeHeader(header.Get("Subject"))
156 date, _ := header.Date()
157 messageID := header.Get("Message-ID")
158 references := header.Get("References")
159
160 var fromAddr string
161 if len(fromAddrs) > 0 {
162 fromAddr = fromAddrs[0].Address
163 }
164
165 var toAddrList []string
166 for _, addr := range toAddrs {
167 toAddrList = append(toAddrList, addr.Address)
168 }
169
170 var body string
171 for {
172 p, err := mr.NextPart()
173 if err == io.EOF {
174 break
175 } else if err != nil {
176 log.Printf("Error getting next part: %v", err)
177 break
178 }
179
180 mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
181 if mediaType == "text/plain" || mediaType == "text/html" {
182 decodedPart, decodeErr := decodePart(p.Body, p.Header)
183 if decodeErr == nil {
184 body = decodedPart
185 break
186 }
187 }
188 }
189
190 emails = append(emails, Email{
191 From: fromAddr,
192 To: toAddrList,
193 Subject: subject,
194 Body: body,
195 Date: date,
196 MessageID: messageID,
197 References: strings.Fields(references),
198 })
199 }
200
201 if err := <-done; err != nil {
202 return nil, err
203 }
204
205 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
206 emails[i], emails[j] = emails[j], emails[i]
207 }
208
209 return emails, nil
210}