fetcher.go

  1package fetcher
  2
  3import (
  4	"crypto/tls"
  5	"fmt"
  6	"io"
  7	"log"
  8	"time"
  9
 10	"github.com/andrinoff/email-cli/config"
 11	"github.com/emersion/go-imap/v2"
 12	"github.com/emersion/go-imap/v2/imapclient"
 13)
 14
 15// Email struct holds the essential information for a single email.
 16type Email struct {
 17	From    string
 18	Subject string
 19	Body    string
 20	Date    time.Time
 21}
 22
 23// FetchEmails connects to the email provider's IMAP server and retrieves the latest emails.
 24func FetchEmails(cfg *config.Config) ([]Email, error) {
 25	var imapHost, serverName string
 26	switch cfg.ServiceProvider {
 27	case "gmail":
 28		imapHost = "imap.gmail.com:993"
 29		serverName = "imap.gmail.com"
 30	case "icloud":
 31		imapHost = "imap.mail.me.com:993"
 32		serverName = "imap.mail.me.com"
 33	default:
 34		return nil, fmt.Errorf("unsupported service provider: %s", cfg.ServiceProvider)
 35	}
 36
 37	// Set up client options
 38	options := imapclient.Options{
 39		TLSConfig: &tls.Config{ServerName: serverName},
 40	}
 41	// Dial the IMAP server
 42	c, err := imapclient.DialTLS(imapHost, &options)
 43	if err != nil {
 44		return nil, fmt.Errorf("failed to dial IMAP server: %w", err)
 45	}
 46	defer c.Close()
 47
 48	// Login
 49	if err := c.Login(cfg.Email, cfg.Password).Wait(); err != nil {
 50		return nil, fmt.Errorf("failed to login: %w", err)
 51	}
 52
 53	// Select INBOX to get message count
 54	selectData, err := c.Select("INBOX", nil).Wait()
 55	if err != nil {
 56		return nil, fmt.Errorf("failed to select INBOX: %w", err)
 57	}
 58	numMessages := selectData.NumMessages
 59
 60	if numMessages == 0 {
 61		return []Email{}, nil // No messages
 62	}
 63
 64	// Determine the sequence set for the last 10 messages
 65	start := uint32(1)
 66	if numMessages > 10 {
 67		start = numMessages - 9
 68	}
 69	seqSet := imap.SeqSetNum(start, numMessages)
 70
 71	// Define what to fetch for each message
 72	fetchOptions := &imap.FetchOptions{
 73		Envelope: true,
 74		BodySection: []*imap.FetchItemBodySection{
 75			{Specifier: imap.PartSpecifierText},
 76		},
 77	}
 78
 79	// Fetch the messages
 80	cmd := c.Fetch(seqSet, fetchOptions)
 81	defer cmd.Close()
 82
 83	var emails []Email
 84	for {
 85		msg := cmd.Next()
 86		if msg == nil {
 87			break // All messages have been processed
 88		}
 89
 90		var from, subject, body string
 91		var date time.Time
 92
 93		// Get header info from the envelope
 94		if env := msg.Envelope(); env != nil {
 95			if len(env.From) > 0 {
 96				from = env.From[0].Address()
 97			}
 98			subject = env.Subject
 99			date = env.Date
100		}
101
102		// Read the body content
103		if part := msg.BodySection(&imap.FetchItemBodySection{Specifier: imap.PartSpecifierText}); part != nil {
104			bodyBytes, err := io.ReadAll(part)
105			if err != nil {
106				log.Printf("failed to read body part: %v", err)
107				continue
108			}
109			body = string(bodyBytes)
110		}
111
112		emails = append(emails, Email{
113			From:    from,
114			Subject: subject,
115			Body:    body,
116			Date:    date,
117		})
118	}
119	if err := cmd.Err(); err != nil {
120		return nil, fmt.Errorf("FETCH command failed: %w", err)
121	}
122
123	// Reverse the slice to show the newest emails first
124	for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
125		emails[i], emails[j] = emails[j], emails[i]
126	}
127
128	return emails, nil
129}