fix: display all emails

drew created

Change summary

fetcher/fetcher.go | 141 ++++++++++++++++++++++++-----------------------
go.mod             |   1 
sender/sender.go   |  65 ++++++++++++++++-----
3 files changed, 121 insertions(+), 86 deletions(-)

Detailed changes

fetcher/fetcher.go 🔗

@@ -1,121 +1,124 @@
 package fetcher
 
 import (
-	"crypto/tls"
 	"fmt"
+	"io"
 	"log"
 	"time"
 
 	"github.com/andrinoff/email-cli/config"
-	"github.com/emersion/go-imap/v2"
-	"github.com/emersion/go-imap/v2/imapclient"
+	"github.com/emersion/go-imap"
+	"github.com/emersion/go-imap/client"
+	"github.com/emersion/go-message/mail"
 )
 
-// Email struct holds the essential information for a single email.
 type Email struct {
 	From    string
+	To      []string
 	Subject string
 	Body    string
 	Date    time.Time
 }
 
-// FetchEmails connects to the email provider's IMAP server and retrieves the latest emails.
 func FetchEmails(cfg *config.Config) ([]Email, error) {
-	var imapHost, serverName string
+	var imapServer string
+	var imapPort int
+
+	// Determine the IMAP server based on the service provider in the config.
 	switch cfg.ServiceProvider {
 	case "gmail":
-		imapHost = "imap.gmail.com:993"
-		serverName = "imap.gmail.com"
+		imapServer = "imap.gmail.com"
+		imapPort = 993
 	case "icloud":
-		imapHost = "imap.mail.me.com:993"
-		serverName = "imap.mail.me.com"
+		imapServer = "imap.mail.me.com"
+		imapPort = 993
 	default:
-		return nil, fmt.Errorf("unsupported service provider: %s", cfg.ServiceProvider)
+		return nil, fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
 	}
 
-	options := imapclient.Options{
-		TLSConfig: &tls.Config{ServerName: serverName},
-	}
-	c, err := imapclient.DialTLS(imapHost, &options)
+	// Connect to the server
+	addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
+	c, err := client.DialTLS(addr, nil)
 	if err != nil {
-		return nil, fmt.Errorf("failed to dial IMAP server: %w", err)
+		return nil, err
 	}
-	defer c.Close()
+	log.Println("Connected")
+	defer c.Logout()
 
-	if err := c.Login(cfg.Email, cfg.Password).Wait(); err != nil {
-		return nil, fmt.Errorf("failed to login: %w", err)
+	// Login
+	if err := c.Login(cfg.Email, cfg.Password); err != nil {
+		return nil, err
 	}
+	log.Println("Logged in")
 
-	selectData, err := c.Select("INBOX", nil).Wait()
+	// Select INBOX
+	mbox, err := c.Select("INBOX", false)
 	if err != nil {
-		return nil, fmt.Errorf("failed to select INBOX: %w", err)
-	}
-	numMessages := selectData.NumMessages
-
-	if numMessages == 0 {
-		return []Email{}, nil // No messages
+		return nil, err
 	}
 
-	start := uint32(1)
-	if numMessages > 10 {
-		start = numMessages - 9
+	// Get the last 10 messages
+	from := uint32(1)
+	to := mbox.Messages
+	if mbox.Messages > 10 {
+		from = mbox.Messages - 9
 	}
-	seqSet := imap.SeqSetNum(start, numMessages)
+	seqset := new(imap.SeqSet)
+	seqset.AddRange(from, to)
 
-	fetchOptions := &imap.FetchOptions{
-		Envelope: true,
-		BodySection: []*imap.FetchItemBodySection{
-			{Specifier: imap.PartSpecifierText},
-		},
-	}
-
-	cmd := c.Fetch(seqSet, fetchOptions)
+	messages := make(chan *imap.Message, 10)
+	done := make(chan error, 1)
+	go func() {
+		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchItem("BODY[]")}, messages)
+	}()
 
 	var emails []Email
-	for {
-		msg := cmd.Next()
-		if msg == nil {
-			break
+	for msg := range messages {
+		r := msg.GetBody(&imap.BodySectionName{})
+		if r == nil {
+			log.Fatal("Server didn't return message body")
 		}
 
-		buf, err := msg.Collect()
+		mr, err := mail.CreateReader(r)
 		if err != nil {
-			log.Printf("failed to collect message data: %v", err)
-			continue
+			log.Fatal(err)
 		}
-		
-		var from, subject, body string
-		var date time.Time
-
-		if buf.Envelope != nil {
-			if len(buf.Envelope.From) > 0 {
-				from = buf.Envelope.From[0].Addr()
-			}
-			subject = buf.Envelope.Subject
-			date = buf.Envelope.Date
+
+		header := mr.Header
+		fromAddrs, _ := header.AddressList("From")
+		toAddrs, _ := header.AddressList("To")
+		subject, _ := header.Subject()
+		date, _ := header.Date()
+
+		var fromAddr string
+		if len(fromAddrs) > 0 {
+			fromAddr = fromAddrs[0].Address
 		}
-		
-		// Corrected: FindBodySection returns []byte, not io.Reader
-		if bodySection := buf.FindBodySection(&imap.FetchItemBodySection{Specifier: imap.PartSpecifierText}); bodySection != nil {
-			body = string(bodySection) // Directly convert []byte to string
+
+		var toAddrList []string
+		for _, addr := range toAddrs {
+			toAddrList = append(toAddrList, addr.Address)
+		}
+
+		p, err := mr.NextPart()
+		if err != nil && err != io.EOF {
+			log.Fatal(err)
 		}
 
+		body, _ := io.ReadAll(p.Body)
+
 		emails = append(emails, Email{
-			From:    from,
+			From:    fromAddr,
+			To:      toAddrList,
 			Subject: subject,
-			Body:    body,
+			Body:    string(body),
 			Date:    date,
 		})
 	}
 
-	if err := cmd.Close(); err != nil {
-		return nil, fmt.Errorf("FETCH command failed: %w", err)
-	}
-
-	// Reverse the slice to show the newest emails first
-	for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
-		emails[i], emails[j] = emails[j], emails[i]
+	if err := <-done; err != nil {
+		return nil, err
 	}
 
 	return emails, nil
-}
+}

go.mod 🔗

@@ -15,6 +15,7 @@ require (
 	github.com/charmbracelet/x/ansi v0.9.3 // indirect
 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
 	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/emersion/go-imap v1.2.1 // indirect
 	github.com/emersion/go-imap/v2 v2.0.0-beta.5 // indirect
 	github.com/emersion/go-message v0.18.2 // indirect
 	github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect

sender/sender.go 🔗

@@ -1,34 +1,65 @@
 package sender
 
 import (
+	"crypto/rand"
 	"fmt"
 	"net/smtp"
+	"time"
 
-	"github.com/andrinoff/email-cli/config" // Import the local config package
+	"github.com/andrinoff/email-cli/config"
 )
 
-// SendEmail sends an email using the provided configuration and content.
+// generateMessageID creates a unique Message-ID header.
+// This is a crucial header for deliverability, as its absence is a spam indicator.
+func generateMessageID(from string) string {
+	// Generate a random part to ensure uniqueness
+	buf := make([]byte, 16)
+	_, err := rand.Read(buf)
+	if err != nil {
+		// Fallback to a less random but still unique value if crypto/rand fails
+		return fmt.Sprintf("<%d.%s>", time.Now().UnixNano(), from)
+	}
+	return fmt.Sprintf("<%x@%s>", buf, from)
+}
+
 func SendEmail(cfg *config.Config, to []string, subject, body string) error {
-	var smtpHost, smtpPort string
+	var smtpServer string
+	var smtpPort int
 
+	// Determine the SMTP server based on the service provider in the config.
 	switch cfg.ServiceProvider {
 	case "gmail":
-		smtpHost = "smtp.gmail.com"
-		smtpPort = "587"
+		smtpServer = "smtp.gmail.com"
+		smtpPort = 587
 	case "icloud":
-		smtpHost = "smtp.mail.me.com"
-		smtpPort = "587"
+		smtpServer = "smtp.mail.me.com"
+		smtpPort = 587
 	default:
-		return fmt.Errorf("unsupported service provider: %s", cfg.ServiceProvider)
+		return fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
+	}
+
+	// Set up authentication information.
+	auth := smtp.PlainAuth("", cfg.Email, cfg.Password, smtpServer)
+
+	// Construct the full email message with proper headers.
+	headers := map[string]string{
+		"From":         cfg.Email,
+		"To":           to[0], // Assuming one recipient for the header display
+		"Subject":      subject,
+		"Message-ID":   generateMessageID(cfg.Email),
+		"Content-Type": "text/plain; charset=UTF-8", // Explicitly set content type
+	}
+
+	var msg string
+	for k, v := range headers {
+		msg += fmt.Sprintf("%s: %s\r\n", k, v)
 	}
+	msg += "\r\n" + body
 
-	auth := smtp.PlainAuth("", cfg.Email, cfg.Password, smtpHost)
-	msg := fmt.Sprintf("From: %s <%s>\r\n", cfg.Name, cfg.Email) +
-		fmt.Sprintf("To: %s\r\n", to[0]) +
-		fmt.Sprintf("Subject: %s\r\n", subject) +
-		"\r\n" + // An empty line is required between headers and the body.
-		body
+	// SMTP server address.
+	addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
 
-	addr := smtpHost + ":" + smtpPort
-	return smtp.SendMail(addr, auth, cfg.Email, to, []byte(msg))
-}
+	// Send the email.
+	err := smtp.SendMail(addr, auth, cfg.Email, to, []byte(msg))
+	return err
+}