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 UID uint32 // Added UID to uniquely identify emails
22 From string
23 To []string
24 Subject string
25 Body string
26 Date time.Time
27 MessageID string
28 References []string
29}
30
31func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
32 mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
33 if err != nil {
34 body, _ := ioutil.ReadAll(reader)
35 return string(body), nil
36 }
37
38 charset := "utf-8"
39 if params["charset"] != "" {
40 charset = strings.ToLower(params["charset"])
41 }
42
43 encoding, err := ianaindex.IANA.Encoding(charset)
44 if err != nil || encoding == nil {
45 encoding, _ = ianaindex.IANA.Encoding("utf-8")
46 }
47
48 transformReader := transform.NewReader(reader, encoding.NewDecoder())
49 decodedBody, err := ioutil.ReadAll(transformReader)
50 if err != nil {
51 return "", err
52 }
53
54 if strings.HasPrefix(mediaType, "multipart/") {
55 return "[This is a multipart message]", nil
56 }
57
58 return string(decodedBody), nil
59}
60
61func decodeHeader(header string) string {
62 dec := new(mime.WordDecoder)
63 dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
64 encoding, err := ianaindex.IANA.Encoding(charset)
65 if err != nil {
66 return nil, err
67 }
68 return transform.NewReader(input, encoding.NewDecoder()), nil
69 }
70 decoded, err := dec.DecodeHeader(header)
71 if err != nil {
72 return header
73 }
74 return decoded
75}
76
77// connect initializes a connection to the IMAP server.
78func connect(cfg *config.Config) (*client.Client, error) {
79 var imapServer string
80 var imapPort int
81
82 switch cfg.ServiceProvider {
83 case "gmail":
84 imapServer = "imap.gmail.com"
85 imapPort = 993
86 case "icloud":
87 imapServer = "imap.mail.me.com"
88 imapPort = 993
89 default:
90 return nil, fmt.Errorf("unsupported service_provider: %s", cfg.ServiceProvider)
91 }
92
93 addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
94 c, err := client.DialTLS(addr, nil)
95 if err != nil {
96 return nil, err
97 }
98
99 if err := c.Login(cfg.Email, cfg.Password); err != nil {
100 return nil, err
101 }
102
103 return c, nil
104}
105
106func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
107 c, err := connect(cfg)
108 if err != nil {
109 return nil, err
110 }
111 defer c.Logout()
112
113 mbox, err := c.Select("INBOX", false)
114 if err != nil {
115 return nil, err
116 }
117
118 if mbox.Messages == 0 {
119 return []Email{}, nil
120 }
121
122 to := mbox.Messages - offset
123 from := uint32(1)
124 if to > limit {
125 from = to - limit + 1
126 }
127
128 if to < 1 {
129 return []Email{}, nil
130 }
131
132 seqset := new(imap.SeqSet)
133 seqset.AddRange(from, to)
134
135 messages := make(chan *imap.Message, limit)
136 done := make(chan error, 1)
137 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchItem("BODY[]")}
138 go func() {
139 done <- c.Fetch(seqset, fetchItems, messages)
140 }()
141
142 var emails []Email
143 for msg := range messages {
144 if msg == nil {
145 continue
146 }
147
148 bodyLiteral := msg.GetBody(&imap.BodySectionName{})
149 if bodyLiteral == nil {
150 log.Println("Could not get message body")
151 continue
152 }
153
154 mr, err := mail.CreateReader(bodyLiteral)
155 if err != nil {
156 log.Printf("Error creating mail reader: %v", err)
157 continue
158 }
159
160 header := mr.Header
161 fromAddrs, _ := header.AddressList("From")
162 toAddrs, _ := header.AddressList("To")
163 subject := decodeHeader(header.Get("Subject"))
164 date, _ := header.Date()
165 messageID := header.Get("Message-ID")
166 references := header.Get("References")
167
168 var fromAddr string
169 if len(fromAddrs) > 0 {
170 fromAddr = fromAddrs[0].Address
171 }
172
173 var toAddrList []string
174 for _, addr := range toAddrs {
175 toAddrList = append(toAddrList, addr.Address)
176 }
177
178 var body string
179 for {
180 p, err := mr.NextPart()
181 if err == io.EOF {
182 break
183 } else if err != nil {
184 log.Printf("Error getting next part: %v", err)
185 break
186 }
187
188 mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
189 if mediaType == "text/plain" || mediaType == "text/html" {
190 decodedPart, decodeErr := decodePart(p.Body, p.Header)
191 if decodeErr == nil {
192 body = decodedPart
193 break
194 }
195 }
196 }
197
198 emails = append(emails, Email{
199 UID: msg.Uid,
200 From: fromAddr,
201 To: toAddrList,
202 Subject: subject,
203 Body: body,
204 Date: date,
205 MessageID: messageID,
206 References: strings.Fields(references),
207 })
208 }
209
210 if err := <-done; err != nil {
211 return nil, err
212 }
213
214 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
215 emails[i], emails[j] = emails[j], emails[i]
216 }
217
218 return emails, nil
219}
220
221// moveEmail moves an email to a specified destination mailbox.
222func moveEmail(cfg *config.Config, uid uint32, destMailbox string) error {
223 c, err := connect(cfg)
224 if err != nil {
225 return err
226 }
227 defer c.Logout()
228
229 if _, err := c.Select("INBOX", false); err != nil {
230 return err
231 }
232
233 seqSet := new(imap.SeqSet)
234 seqSet.AddNum(uid)
235
236 return c.UidMove(seqSet, destMailbox)
237}
238
239// DeleteEmail moves an email to the trash folder.
240func DeleteEmail(cfg *config.Config, uid uint32) error {
241 var trashMailbox string
242 switch cfg.ServiceProvider {
243 case "gmail":
244 trashMailbox = "[Gmail]/Trash"
245 default:
246 trashMailbox = "Trash"
247 }
248 return moveEmail(cfg, uid, trashMailbox)
249}
250
251// ArchiveEmail moves an email to the archive folder.
252func ArchiveEmail(cfg *config.Config, uid uint32) error {
253 var archiveMailbox string
254 switch cfg.ServiceProvider {
255 case "gmail":
256 archiveMailbox = "[Gmail]/All Mail"
257 default:
258 archiveMailbox = "Archive"
259 }
260 return moveEmail(cfg, uid, archiveMailbox)
261}