fetcher.go

  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}