1package fetcher
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "mime"
8 "strings"
9 "time"
10
11 "github.com/andrinoff/email-cli/config"
12 "github.com/emersion/go-imap"
13 "github.com/emersion/go-imap/client"
14 "github.com/emersion/go-message/mail"
15 "golang.org/x/text/encoding/ianaindex"
16 "golang.org/x/text/transform"
17)
18
19// Attachment holds data for an email attachment.
20type Attachment struct {
21 Filename string
22 PartID string // Keep PartID to fetch on demand
23 Data []byte
24}
25
26type Email struct {
27 UID uint32
28 From string
29 To []string
30 Subject string
31 Body string
32 Date time.Time
33 MessageID string
34 References []string
35 Attachments []Attachment
36}
37
38func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
39 mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
40 if err != nil {
41 body, _ := ioutil.ReadAll(reader)
42 return string(body), nil
43 }
44
45 charset := "utf-8"
46 if params["charset"] != "" {
47 charset = strings.ToLower(params["charset"])
48 }
49
50 encoding, err := ianaindex.IANA.Encoding(charset)
51 if err != nil || encoding == nil {
52 encoding, _ = ianaindex.IANA.Encoding("utf-8")
53 }
54
55 transformReader := transform.NewReader(reader, encoding.NewDecoder())
56 decodedBody, err := ioutil.ReadAll(transformReader)
57 if err != nil {
58 return "", err
59 }
60
61 if strings.HasPrefix(mediaType, "multipart/") {
62 return "[This is a multipart message]", nil
63 }
64
65 return string(decodedBody), nil
66}
67
68func decodeHeader(header string) string {
69 dec := new(mime.WordDecoder)
70 dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
71 encoding, err := ianaindex.IANA.Encoding(charset)
72 if err != nil {
73 return nil, err
74 }
75 return transform.NewReader(input, encoding.NewDecoder()), nil
76 }
77 decoded, err := dec.DecodeHeader(header)
78 if err != nil {
79 return header
80 }
81 return decoded
82}
83
84func connect(cfg *config.Config) (*client.Client, error) {
85 var imapServer string
86 var imapPort int
87
88 switch cfg.ServiceProvider {
89 case "gmail":
90 imapServer = "imap.gmail.com"
91 imapPort = 993
92 case "icloud":
93 imapServer = "imap.mail.me.com"
94 imapPort = 993
95 default:
96 return nil, fmt.Errorf("unsupported service_provider: %s", cfg.ServiceProvider)
97 }
98
99 addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
100 c, err := client.DialTLS(addr, nil)
101 if err != nil {
102 return nil, err
103 }
104
105 if err := c.Login(cfg.Email, cfg.Password); err != nil {
106 return nil, err
107 }
108
109 return c, nil
110}
111
112func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
113 c, err := connect(cfg)
114 if err != nil {
115 return nil, err
116 }
117 defer c.Logout()
118
119 mbox, err := c.Select("INBOX", false)
120 if err != nil {
121 return nil, err
122 }
123
124 if mbox.Messages == 0 {
125 return []Email{}, nil
126 }
127
128 to := mbox.Messages - offset
129 from := uint32(1)
130 if to > limit {
131 from = to - limit + 1
132 }
133
134 if to < 1 {
135 return []Email{}, nil
136 }
137
138 seqset := new(imap.SeqSet)
139 seqset.AddRange(from, to)
140
141 messages := make(chan *imap.Message, limit)
142 done := make(chan error, 1)
143 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
144 go func() {
145 done <- c.Fetch(seqset, fetchItems, messages)
146 }()
147
148 var msgs []*imap.Message
149 for msg := range messages {
150 msgs = append(msgs, msg)
151 }
152
153 if err := <-done; err != nil {
154 return nil, err
155 }
156
157 var emails []Email
158 for _, msg := range msgs {
159 if msg == nil || msg.Envelope == nil {
160 continue
161 }
162
163 var fromAddr string
164 if len(msg.Envelope.From) > 0 {
165 fromAddr = msg.Envelope.From[0].Address()
166 }
167
168 var toAddrList []string
169 for _, addr := range msg.Envelope.To {
170 toAddrList = append(toAddrList, addr.Address())
171 }
172
173 emails = append(emails, Email{
174 UID: msg.Uid,
175 From: fromAddr,
176 To: toAddrList,
177 Subject: decodeHeader(msg.Envelope.Subject),
178 Date: msg.Envelope.Date,
179 })
180 }
181
182 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
183 emails[i], emails[j] = emails[j], emails[i]
184 }
185
186 return emails, nil
187}
188
189func FetchEmailBody(cfg *config.Config, uid uint32) (string, []Attachment, error) {
190 c, err := connect(cfg)
191 if err != nil {
192 return "", nil, err
193 }
194 defer c.Logout()
195
196 if _, err := c.Select("INBOX", false); err != nil {
197 return "", nil, err
198 }
199
200 seqset := new(imap.SeqSet)
201 seqset.AddNum(uid)
202
203 messages := make(chan *imap.Message, 1)
204 done := make(chan error, 1)
205 fetchItems := []imap.FetchItem{imap.FetchBodyStructure}
206 go func() {
207 done <- c.UidFetch(seqset, fetchItems, messages)
208 }()
209
210 if err := <-done; err != nil {
211 return "", nil, err
212 }
213
214 msg := <-messages
215 if msg == nil || msg.BodyStructure == nil {
216 return "", nil, fmt.Errorf("no message or body structure found with UID %d", uid)
217 }
218
219 var textPartID string
220 var attachments []Attachment
221 var findParts func(*imap.BodyStructure, string)
222 findParts = func(bs *imap.BodyStructure, prefix string) {
223 for i, part := range bs.Parts {
224 partID := fmt.Sprintf("%d", i+1)
225 if prefix != "" {
226 partID = fmt.Sprintf("%s.%d", prefix, i+1)
227 }
228
229 if part.MIMEType == "text" && (part.MIMESubType == "plain" || part.MIMESubType == "html") && textPartID == "" {
230 textPartID = partID
231 }
232 if part.Disposition == "attachment" || part.Disposition == "inline" {
233 if filename, ok := part.Params["filename"]; ok {
234 attachments = append(attachments, Attachment{Filename: filename, PartID: partID})
235 }
236 }
237 if len(part.Parts) > 0 {
238 findParts(part, partID)
239 }
240 }
241 }
242 findParts(msg.BodyStructure, "")
243
244 var body string
245 if textPartID != "" {
246 partMessages := make(chan *imap.Message, 1)
247 partDone := make(chan error, 1)
248
249 // Correctly create the fetch item as a string for your library version
250 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", textPartID))
251 section, err := imap.ParseBodySectionName(fetchItem)
252 if err != nil {
253 return "", nil, err
254 }
255
256 go func() {
257 // Pass the string-based fetch item
258 partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
259 }()
260
261 if err := <-partDone; err != nil {
262 return "", nil, err
263 }
264
265 partMsg := <-partMessages
266 if partMsg != nil {
267 // Use the parsed section to get the body from the response
268 literal := partMsg.GetBody(section)
269 if literal != nil {
270 bodyBytes, _ := ioutil.ReadAll(literal)
271 body = string(bodyBytes)
272 }
273 }
274 }
275
276 return body, attachments, nil
277}
278
279func FetchAttachment(cfg *config.Config, uid uint32, partID string) ([]byte, error) {
280 c, err := connect(cfg)
281 if err != nil {
282 return nil, err
283 }
284 defer c.Logout()
285
286 if _, err := c.Select("INBOX", false); err != nil {
287 return nil, err
288 }
289
290 seqset := new(imap.SeqSet)
291 seqset.AddNum(uid)
292
293 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
294 section, err := imap.ParseBodySectionName(fetchItem)
295 if err != nil {
296 return nil, err
297 }
298
299 messages := make(chan *imap.Message, 1)
300 done := make(chan error, 1)
301 go func() {
302 done <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, messages)
303 }()
304
305 if err := <-done; err != nil {
306 return nil, err
307 }
308
309 msg := <-messages
310 if msg == nil {
311 return nil, fmt.Errorf("could not fetch attachment")
312 }
313
314 literal := msg.GetBody(section)
315 if literal == nil {
316 return nil, fmt.Errorf("could not get attachment body")
317 }
318
319 return ioutil.ReadAll(literal)
320}
321
322func moveEmail(cfg *config.Config, uid uint32, destMailbox string) error {
323 c, err := connect(cfg)
324 if err != nil {
325 return err
326 }
327 defer c.Logout()
328
329 if _, err := c.Select("INBOX", false); err != nil {
330 return err
331 }
332
333 seqSet := new(imap.SeqSet)
334 seqSet.AddNum(uid)
335
336 return c.UidMove(seqSet, destMailbox)
337}
338
339func DeleteEmail(cfg *config.Config, uid uint32) error {
340 c, err := connect(cfg)
341 if err != nil {
342 return err
343 }
344 defer c.Logout()
345
346 if _, err := c.Select("INBOX", false); err != nil {
347 return err
348 }
349
350 seqSet := new(imap.SeqSet)
351 seqSet.AddNum(uid)
352
353 item := imap.FormatFlagsOp(imap.AddFlags, true)
354 flags := []interface{}{imap.DeletedFlag}
355
356 if err := c.UidStore(seqSet, item, flags, nil); err != nil {
357 return err
358 }
359
360 return c.Expunge(nil)
361}
362
363func ArchiveEmail(cfg *config.Config, uid uint32) error {
364 var archiveMailbox string
365 switch cfg.ServiceProvider {
366 case "gmail":
367 archiveMailbox = "[Gmail]/All Mail"
368 default:
369 archiveMailbox = "Archive"
370 }
371 return moveEmail(cfg, uid, archiveMailbox)
372}