@@ -179,101 +179,131 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
return []Email{}, nil
}
- to := mbox.Messages - offset
- from := uint32(1)
- if to > limit {
- from = to - limit + 1
- }
+ var allEmails []Email
- if to < 1 {
+ // Start from the top minus offset
+ cursor := uint32(0)
+ if mbox.Messages > offset {
+ cursor = mbox.Messages - offset
+ } else {
return []Email{}, nil
}
- seqset := new(imap.SeqSet)
- seqset.AddRange(from, to)
-
- messages := make(chan *imap.Message, limit)
- done := make(chan error, 1)
- fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
- go func() {
- done <- c.Fetch(seqset, fetchItems, messages)
- }()
-
- var msgs []*imap.Message
- for msg := range messages {
- msgs = append(msgs, msg)
- }
-
- if err := <-done; err != nil {
- return nil, err
+ // Determine if we should filter
+ fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
+ if fetchEmail == "" {
+ fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
}
+ isSentMailbox := mailbox == getSentMailbox(account)
- var emails []Email
- for _, msg := range msgs {
- if msg == nil || msg.Envelope == nil {
- continue
+ // Loop until we have enough emails or run out of messages
+ for len(allEmails) < int(limit) && cursor > 0 {
+ // Determine chunk size
+ // Fetch at least 'limit' or 50 messages to reduce round trips
+ chunkSize := limit
+ if chunkSize < 50 {
+ chunkSize = 50
}
- var fromAddr string
- if len(msg.Envelope.From) > 0 {
- fromAddr = msg.Envelope.From[0].Address()
+ from := uint32(1)
+ if cursor > uint32(chunkSize) {
+ from = cursor - uint32(chunkSize) + 1
}
- var toAddrList []string
- // Build recipient list from To and Cc for matching and display
- for _, addr := range msg.Envelope.To {
- toAddrList = append(toAddrList, addr.Address())
- }
- for _, addr := range msg.Envelope.Cc {
- toAddrList = append(toAddrList, addr.Address())
+ seqset := new(imap.SeqSet)
+ seqset.AddRange(from, cursor)
+
+ messages := make(chan *imap.Message, chunkSize)
+ done := make(chan error, 1)
+ fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
+
+ go func() {
+ done <- c.Fetch(seqset, fetchItems, messages)
+ }()
+
+ var batchMsgs []*imap.Message
+ for msg := range messages {
+ batchMsgs = append(batchMsgs, msg)
}
- // Determine which email to filter on: prefer Account.FetchEmail, fallback to Account.Email
- fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
- if fetchEmail == "" {
- fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
+ if err := <-done; err != nil {
+ return nil, err
}
- // Determine if this is a sent mailbox
- isSentMailbox := mailbox == getSentMailbox(account)
+ // Filter messages in this batch
+ var batchEmails []Email
+ for _, msg := range batchMsgs {
+ if msg == nil || msg.Envelope == nil {
+ continue
+ }
- // Apply different filtering logic based on mailbox type
- matched := false
- if isSentMailbox {
- // For sent mailbox, check if the sender matches the fetchEmail
- if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
- matched = true
+ var fromAddr string
+ if len(msg.Envelope.From) > 0 {
+ fromAddr = msg.Envelope.From[0].Address()
}
- } else {
- // For inbox and other mailboxes, check if any recipient matches the fetchEmail
- for _, r := range toAddrList {
- if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
+
+ var toAddrList []string
+ for _, addr := range msg.Envelope.To {
+ toAddrList = append(toAddrList, addr.Address())
+ }
+ for _, addr := range msg.Envelope.Cc {
+ toAddrList = append(toAddrList, addr.Address())
+ }
+
+ matched := false
+ if isSentMailbox {
+ if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
matched = true
- break
+ }
+ } else {
+ for _, r := range toAddrList {
+ if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
+ matched = true
+ break
+ }
}
}
- }
- if !matched {
- // Skip messages not matching the filter criteria
- continue
+ if !matched {
+ continue
+ }
+
+ batchEmails = append(batchEmails, Email{
+ UID: msg.Uid,
+ From: fromAddr,
+ To: toAddrList,
+ Subject: decodeHeader(msg.Envelope.Subject),
+ Date: msg.Envelope.Date,
+ AccountID: account.ID,
+ })
+ }
+
+ // Sort batch Newest -> Oldest (since IMAP usually returns Oldest->Newest or arbitrary)
+ // Assuming seqset order or standard behavior, we want to ensure we append Newest emails first
+ // so that the final list is correct.
+ // Actually, let's just sort the batch by UID desc (Newest first)
+ // Simple bubble sort for small batch
+ for i := 0; i < len(batchEmails); i++ {
+ for j := i + 1; j < len(batchEmails); j++ {
+ if batchEmails[j].UID > batchEmails[i].UID {
+ batchEmails[i], batchEmails[j] = batchEmails[j], batchEmails[i]
+ }
+ }
}
- emails = append(emails, Email{
- UID: msg.Uid,
- From: fromAddr,
- To: toAddrList,
- Subject: decodeHeader(msg.Envelope.Subject),
- Date: msg.Envelope.Date,
- AccountID: account.ID,
- })
+ // Append to allEmails
+ allEmails = append(allEmails, batchEmails...)
+
+ // Update cursor for next iteration
+ cursor = from - 1
}
- for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
- emails[i], emails[j] = emails[j], emails[i]
+ // Trim if we have too many
+ if len(allEmails) > int(limit) {
+ allEmails = allEmails[:limit]
}
- return emails, nil
+ return allEmails, nil
}
func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, []Attachment, error) {
@@ -32,6 +32,7 @@ import (
const (
initialEmailLimit = 20
paginationLimit = 20
+ maxCacheEmails = 100
)
// Version variables are injected by the build (GoReleaser ldflags).
@@ -280,17 +281,22 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.current = m.inbox
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ counts := make(map[string]int)
+ for k, v := range emailsByAcct {
+ counts[k] = len(v)
+ }
+
// Start background refresh
return m, tea.Batch(
m.current.Init(),
func() tea.Msg { return tui.RefreshingEmailsMsg{Mailbox: tui.MailboxInbox} },
- refreshEmails(m.config, tui.MailboxInbox),
+ refreshEmails(m.config, tui.MailboxInbox, counts),
)
case tui.RequestRefreshMsg:
return m, tea.Batch(
func() tea.Msg { return tui.RefreshingEmailsMsg{Mailbox: msg.Mailbox} },
- refreshEmails(m.config, msg.Mailbox),
+ refreshEmails(m.config, msg.Mailbox, msg.Counts),
)
case tui.EmailsRefreshedMsg:
@@ -424,32 +430,40 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.sentByAcct == nil {
m.sentByAcct = make(map[string][]fetcher.Email)
}
- m.sentByAcct[msg.AccountID] = append(m.sentByAcct[msg.AccountID], msg.Emails...)
- m.sentEmails = append(m.sentEmails, msg.Emails...)
+ unique := filterUnique(m.sentByAcct[msg.AccountID], msg.Emails)
+ m.sentByAcct[msg.AccountID] = append(m.sentByAcct[msg.AccountID], unique...)
+ m.sentEmails = append(m.sentEmails, unique...)
return m, nil
}
if msg.Mailbox == tui.MailboxTrash {
if m.trashByAcct == nil {
m.trashByAcct = make(map[string][]fetcher.Email)
}
- m.trashByAcct[msg.AccountID] = append(m.trashByAcct[msg.AccountID], msg.Emails...)
- m.trashEmails = append(m.trashEmails, msg.Emails...)
+ unique := filterUnique(m.trashByAcct[msg.AccountID], msg.Emails)
+ m.trashByAcct[msg.AccountID] = append(m.trashByAcct[msg.AccountID], unique...)
+ m.trashEmails = append(m.trashEmails, unique...)
return m, nil
}
if msg.Mailbox == tui.MailboxArchive {
if m.archiveByAcct == nil {
m.archiveByAcct = make(map[string][]fetcher.Email)
}
- m.archiveByAcct[msg.AccountID] = append(m.archiveByAcct[msg.AccountID], msg.Emails...)
- m.archiveEmails = append(m.archiveEmails, msg.Emails...)
+ unique := filterUnique(m.archiveByAcct[msg.AccountID], msg.Emails)
+ m.archiveByAcct[msg.AccountID] = append(m.archiveByAcct[msg.AccountID], unique...)
+ m.archiveEmails = append(m.archiveEmails, unique...)
return m, nil
}
// Inbox
if m.emailsByAcct == nil {
m.emailsByAcct = make(map[string][]fetcher.Email)
}
- m.emailsByAcct[msg.AccountID] = append(m.emailsByAcct[msg.AccountID], msg.Emails...)
- m.emails = append(m.emails, msg.Emails...)
+ unique := filterUnique(m.emailsByAcct[msg.AccountID], msg.Emails)
+ m.emailsByAcct[msg.AccountID] = append(m.emailsByAcct[msg.AccountID], unique...)
+ m.emails = append(m.emails, unique...)
+
+ // Save to cache
+ go saveEmailsToCache(m.emails)
+
return m, nil
case tui.GoToSendMsg:
@@ -1179,7 +1193,7 @@ func loadCachedEmails() tea.Cmd {
}
}
-func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
+func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind, counts map[string]int) tea.Cmd {
return func() tea.Msg {
emailsByAccount := make(map[string][]fetcher.Email)
var mu sync.Mutex
@@ -1191,10 +1205,18 @@ func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
defer wg.Done()
var emails []fetcher.Email
var err error
+
+ limit := uint32(initialEmailLimit)
+ if counts != nil {
+ if c, ok := counts[acc.ID]; ok && c > 0 {
+ limit = uint32(c)
+ }
+ }
+
if mailbox == tui.MailboxSent {
- emails, err = fetcher.FetchSentEmails(&acc, initialEmailLimit, 0)
+ emails, err = fetcher.FetchSentEmails(&acc, limit, 0)
} else {
- emails, err = fetcher.FetchEmails(&acc, initialEmailLimit, 0)
+ emails, err = fetcher.FetchEmails(&acc, limit, 0)
}
if err != nil {
log.Printf("Error fetching from %s: %v", acc.Email, err)
@@ -1212,6 +1234,9 @@ func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
}
func saveEmailsToCache(emails []fetcher.Email) {
+ if len(emails) > maxCacheEmails {
+ emails = emails[:maxCacheEmails]
+ }
var cachedEmails []config.CachedEmail
for _, email := range emails {
cachedEmails = append(cachedEmails, config.CachedEmail{
@@ -1792,6 +1817,20 @@ func runUpdateCLI() error {
return nil
}
+func filterUnique(existing, incoming []fetcher.Email) []fetcher.Email {
+ seen := make(map[uint32]struct{})
+ for _, e := range existing {
+ seen[e.UID] = struct{}{}
+ }
+ var unique []fetcher.Email
+ for _, e := range incoming {
+ if _, ok := seen[e.UID]; !ok {
+ unique = append(unique, e)
+ }
+ }
+ return unique
+}
+
func main() {
// If invoked as CLI update command, run updater and exit.
if len(os.Args) > 1 && os.Args[1] == "update" {
@@ -163,6 +163,9 @@ func NewInboxSingleAccount(emails []fetcher.Email) *Inbox {
}
func (m *Inbox) updateList() {
+ // Capture current index to restore later
+ currentIndex := m.list.Index()
+
var displayEmails []fetcher.Email
var showAccountLabel bool
@@ -236,6 +239,16 @@ func (m *Inbox) updateList() {
l.SetHeight(m.height / 2)
}
+ // Restore index
+ // If index is out of bounds (e.g. list shrank), clamp it.
+ if currentIndex >= len(items) {
+ currentIndex = len(items) - 1
+ }
+ if currentIndex < 0 {
+ currentIndex = 0
+ }
+ l.Select(currentIndex)
+
m.list = l
}
@@ -326,8 +339,13 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
case "r":
+ // Copy counts to avoid race conditions if used elsewhere (though here it's just passing data)
+ counts := make(map[string]int)
+ for k, v := range m.emailCountByAcct {
+ counts[k] = v
+ }
return m, func() tea.Msg {
- return RequestRefreshMsg{Mailbox: m.mailbox}
+ return RequestRefreshMsg{Mailbox: m.mailbox, Counts: counts}
}
case "enter":
selectedItem, ok := m.list.SelectedItem().(item)