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	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}