From d6a1250dfb60ae0a618a773cbd0c941078070a38 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 28 Jul 2025 18:17:48 +0400 Subject: [PATCH] fix: display all emails --- fetcher/fetcher.go | 141 +++++++++++++++++++++++---------------------- go.mod | 1 + sender/sender.go | 65 +++++++++++++++------ 3 files changed, 121 insertions(+), 86 deletions(-) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 030e1dccaf01d7ad5bc6cff4984e8850222b4f51..4817c88a7d3796ce9db2c8d0fba83134019e94e8 100644 --- a/fetcher/fetcher.go +++ b/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 -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 66786e6fd8df61b32105e118e1470fe69410b691..4bf4b68f9084f70c10bd4b09eebef384466dee21 100644 --- a/go.mod +++ b/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 diff --git a/sender/sender.go b/sender/sender.go index 0865eb855316c8dafcc8e384ed00e87ca1c274c2..679e549f210fafa71cd177f02e441197766a230d 100644 --- a/sender/sender.go +++ b/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)) -} \ No newline at end of file + // Send the email. + err := smtp.SendMail(addr, auth, cfg.Email, to, []byte(msg)) + return err +}