Detailed changes
@@ -3,13 +3,18 @@ package fetcher
import (
"fmt"
"io"
+ "io/ioutil"
"log"
+ "mime"
+ "strings"
"time"
"github.com/andrinoff/email-cli/config"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
+ "golang.org/x/text/encoding/ianaindex"
+ "golang.org/x/text/transform"
)
type Email struct {
@@ -20,11 +25,58 @@ type Email struct {
Date time.Time
}
-func FetchEmails(cfg *config.Config) ([]Email, error) {
+// ... (decodePart and decodeHeader functions remain the same)
+func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
+ mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
+ if err != nil {
+ body, _ := ioutil.ReadAll(reader)
+ return string(body), nil
+ }
+
+ charset := "utf-8" // Default charset
+ if params["charset"] != "" {
+ charset = strings.ToLower(params["charset"])
+ }
+
+ encoding, err := ianaindex.IANA.Encoding(charset)
+ if err != nil || encoding == nil {
+ encoding, _ = ianaindex.IANA.Encoding("utf-8")
+ }
+
+ transformReader := transform.NewReader(reader, encoding.NewDecoder())
+ decodedBody, err := ioutil.ReadAll(transformReader)
+ if err != nil {
+ return "", err
+ }
+
+ if strings.HasPrefix(mediaType, "multipart/") {
+ return "[This is a multipart message]", nil
+ }
+
+ return string(decodedBody), nil
+}
+
+func decodeHeader(header string) string {
+ dec := new(mime.WordDecoder)
+ dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
+ encoding, err := ianaindex.IANA.Encoding(charset)
+ if err != nil {
+ return nil, err
+ }
+ return transform.NewReader(input, encoding.NewDecoder()), nil
+ }
+ decoded, err := dec.DecodeHeader(header)
+ if err != nil {
+ return header
+ }
+ return decoded
+}
+
+// FetchEmails now supports pagination with a limit and offset.
+func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
var imapServer string
var imapPort int
- // Determine the IMAP server based on the service provider in the config.
switch cfg.ServiceProvider {
case "gmail":
imapServer = "imap.gmail.com"
@@ -36,7 +88,6 @@ func FetchEmails(cfg *config.Config) ([]Email, error) {
return nil, fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
}
- // Connect to the server
addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
c, err := client.DialTLS(addr, nil)
if err != nil {
@@ -45,49 +96,62 @@ func FetchEmails(cfg *config.Config) ([]Email, error) {
log.Println("Connected")
defer c.Logout()
- // Login
if err := c.Login(cfg.Email, cfg.Password); err != nil {
return nil, err
}
log.Println("Logged in")
- // Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
return nil, err
}
- // Get the last 10 messages
+ // Handle pagination logic
+ if mbox.Messages == 0 {
+ return []Email{}, nil
+ }
+
+ to := mbox.Messages - offset
from := uint32(1)
- to := mbox.Messages
- if mbox.Messages > 10 {
- from = mbox.Messages - 9
+ if to > limit {
+ from = to - limit + 1
+ }
+
+ if to < 1 {
+ return []Email{}, nil // No more messages to fetch
}
+
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
- messages := make(chan *imap.Message, 10)
+ messages := make(chan *imap.Message, limit)
done := make(chan error, 1)
go func() {
- done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchItem("BODY[]")}, messages)
+ done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchItem("BODY[]")}, messages)
}()
var emails []Email
for msg := range messages {
- r := msg.GetBody(&imap.BodySectionName{})
- if r == nil {
- log.Fatal("Server didn't return message body")
+ if msg == nil {
+ continue
}
- mr, err := mail.CreateReader(r)
+ bodyLiteral := msg.GetBody(&imap.BodySectionName{})
+ if bodyLiteral == nil {
+ log.Println("Could not get message body")
+ continue
+ }
+
+ mr, err := mail.CreateReader(bodyLiteral)
if err != nil {
- log.Fatal(err)
+ log.Printf("Error creating mail reader: %v", err)
+ continue
}
header := mr.Header
fromAddrs, _ := header.AddressList("From")
toAddrs, _ := header.AddressList("To")
- subject, _ := header.Subject()
+ subject := decodeHeader(header.Get("Subject"))
date, _ := header.Date()
var fromAddr string
@@ -100,18 +164,31 @@ func FetchEmails(cfg *config.Config) ([]Email, error) {
toAddrList = append(toAddrList, addr.Address)
}
- p, err := mr.NextPart()
- if err != nil && err != io.EOF {
- log.Fatal(err)
+ var body string
+ for {
+ p, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ log.Printf("Error getting next part: %v", err)
+ break
+ }
+
+ mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
+ if mediaType == "text/plain" || mediaType == "text/html" {
+ decodedPart, decodeErr := decodePart(p.Body, p.Header)
+ if decodeErr == nil {
+ body = decodedPart
+ break
+ }
+ }
}
- body, _ := io.ReadAll(p.Body)
-
emails = append(emails, Email{
From: fromAddr,
To: toAddrList,
Subject: subject,
- Body: string(body),
+ Body: body,
Date: date,
})
}
@@ -120,7 +197,6 @@ func FetchEmails(cfg *config.Config) ([]Email, error) {
return nil, err
}
- // Reverse the order of emails to be from newest to oldest.
for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
emails[i], emails[j] = emails[j], emails[i]
}
@@ -23,7 +23,7 @@ func TestFetchEmails(t *testing.T) {
t.Skip("Skipping TestFetchEmails: placeholder or empty password found in config.")
}
- emails, err := FetchEmails(cfg)
+ emails, err := FetchEmails(cfg, 10, 10)
if err != nil {
t.Fatalf("FetchEmails() failed with error: %v", err)
}
@@ -21,6 +21,11 @@ import (
"github.com/yuin/goldmark/renderer/html"
)
+const (
+ initialEmailLimit = 20
+ paginationLimit = 20
+)
+
// mainModel holds the state for the entire application.
type mainModel struct {
current tea.Model
@@ -79,7 +84,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
- // --- Custom Messages for Switching Views ---
+ // --- Custom Messages for Switching Views and Pagination ---
case tui.Credentials:
cfg := &config.Config{
ServiceProvider: msg.Provider,
@@ -97,7 +102,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.GoToInboxMsg:
m.current = tui.NewStatus("Fetching emails...")
- return m, tea.Batch(m.current.Init(), fetchEmails(m.config))
+ return m, tea.Batch(m.current.Init(), fetchEmails(m.config, initialEmailLimit, 0))
case tui.EmailsFetchedMsg:
m.emails = msg.Emails
@@ -107,6 +112,18 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
cmds = append(cmds, m.current.Init())
+ case tui.FetchMoreEmailsMsg:
+ cmds = append(cmds, func() tea.Msg { return tui.FetchingMoreEmailsMsg{} })
+ cmds = append(cmds, fetchEmails(m.config, paginationLimit, msg.Offset))
+ return m, tea.Batch(cmds...)
+
+ case tui.EmailsAppendedMsg:
+ m.emails = append(m.emails, msg.Emails...)
+ // Pass the new emails to the inbox to be appended.
+ m.current, cmd = m.current.Update(msg)
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+
case tui.GoToSendMsg:
m.current = tui.NewComposer(m.config.Email)
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
@@ -193,14 +210,19 @@ func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
}
}
-// fetchEmails retrieves emails in the background.
-func fetchEmails(cfg *config.Config) tea.Cmd {
+// fetchEmails retrieves emails in the background and dispatches the correct message.
+func fetchEmails(cfg *config.Config, limit, offset uint32) tea.Cmd {
return func() tea.Msg {
- emails, err := fetcher.FetchEmails(cfg)
+ emails, err := fetcher.FetchEmails(cfg, limit, offset)
if err != nil {
return tui.FetchErr(err)
}
- return tui.EmailsFetchedMsg{Emails: emails}
+ if offset == 0 {
+ // This is the initial fetch.
+ return tui.EmailsFetchedMsg{Emails: emails}
+ }
+ // This is a subsequent fetch for pagination.
+ return tui.EmailsAppendedMsg{Emails: emails}
}
}
@@ -208,6 +230,7 @@ func main() {
cfg, err := config.LoadConfig()
var initialModel *mainModel
if err != nil {
+ // If config doesn't exist, guide the user to create one via the login view.
initialModel = newInitialModel(nil)
} else {
initialModel = newInitialModel(cfg)
@@ -219,4 +242,4 @@ func main() {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
-}
+}
@@ -46,8 +46,11 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
fmt.Fprint(w, fn(str))
}
+// Inbox is now stateful to handle pagination.
type Inbox struct {
- list list.Model
+ list list.Model
+ isFetching bool
+ emailsCount int
}
func NewInbox(emails []fetcher.Email) *Inbox {
@@ -61,13 +64,18 @@ func NewInbox(emails []fetcher.Email) *Inbox {
l := list.New(items, itemDelegate{}, 20, 14)
l.Title = "Inbox"
- l.SetShowStatusBar(false)
+ l.SetShowStatusBar(true)
l.SetFilteringEnabled(true)
l.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = inboxHelpStyle
+ l.SetStatusBarItemName("email", "emails")
- return &Inbox{list: l}
+ return &Inbox{
+ list: l,
+ isFetching: false,
+ emailsCount: len(emails),
+ }
}
func (m *Inbox) Init() tea.Cmd {
@@ -75,6 +83,8 @@ func (m *Inbox) Init() tea.Cmd {
}
func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "enter" {
@@ -89,13 +99,44 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil
+
+ case FetchingMoreEmailsMsg:
+ m.isFetching = true
+ m.list.Title = "Fetching more emails..."
+ return m, nil
+
+ case EmailsAppendedMsg:
+ m.isFetching = false
+ m.list.Title = "Inbox"
+ newItems := make([]list.Item, len(msg.Emails))
+ for i, email := range msg.Emails {
+ newItems[i] = item{
+ title: email.Subject,
+ desc: email.From,
+ }
+ }
+ // Correctly append new items to the list.
+ currentItems := m.list.Items()
+ allItems := append(currentItems, newItems...)
+ cmd := m.list.SetItems(allItems)
+ m.emailsCount += len(msg.Emails)
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+ }
+
+ // Infinite scroll logic
+ if !m.isFetching && m.list.Index() >= len(m.list.Items())-5 {
+ cmds = append(cmds, func() tea.Msg {
+ return FetchMoreEmailsMsg{Offset: uint32(m.emailsCount)}
+ })
}
var cmd tea.Cmd
var newModel list.Model
newModel, cmd = m.list.Update(msg)
m.list = newModel
- return m, cmd
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
}
func (m *Inbox) View() string {
@@ -51,4 +51,17 @@ type GoToInboxMsg struct{}
type GoToSendMsg struct{}
// A message to navigate to the settings view.
-type GoToSettingsMsg struct{}
+type GoToSettingsMsg struct{}
+
+// A message to fetch more emails with a given offset.
+type FetchMoreEmailsMsg struct {
+ Offset uint32
+}
+
+// A message to indicate that the app is fetching more emails.
+type FetchingMoreEmailsMsg struct{}
+
+// A message to indicate that new emails have been fetched and should be appended.
+type EmailsAppendedMsg struct {
+ Emails []fetcher.Email
+}