fetcher.go

  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}