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