diff --git a/config/cache.go b/config/cache.go index 0774516..04979e2 100644 --- a/config/cache.go +++ b/config/cache.go @@ -495,6 +495,8 @@ func saveEmailBodyCache(cache *EmailBodyCache) error { } // GetCachedEmailBody returns the cached body for a specific email, or nil if not cached. +// LastAccessedAt is updated by SaveEmailBody, not here -- a read should not +// mutate cache state. func GetCachedEmailBody(folderName string, uid uint32, accountID string) *CachedEmailBody { cache, err := LoadEmailBodyCache(folderName) if err != nil { @@ -502,8 +504,6 @@ func GetCachedEmailBody(folderName string, uid uint32, accountID string) *Cached } for i, b := range cache.Bodies { if b.UID == uid && b.AccountID == accountID { - cache.Bodies[i].LastAccessedAt = time.Now() - _ = saveEmailBodyCache(cache) return &cache.Bodies[i] } } diff --git a/main.go b/main.go index 6e683d1..e9c9c23 100644 --- a/main.go +++ b/main.go @@ -524,6 +524,44 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } go config.SaveAccountFolders(accID, names) } + // Per-account fetch errors (e.g. broken IMAP login, unreachable + // server) are non-fatal: other accounts' folders are still shown. + // Surface them as a transient overlay so the user knows why an + // account's folders are missing instead of silently dropping them. + // Reuses the PluginNotifyMsg pattern (save current view, show + // status with a tea.Tick that fires RestoreViewMsg). + if len(msg.Errors) > 0 { + lookup := map[string]string{} + if m.config != nil { + for _, acc := range m.config.Accounts { + name := acc.Email + if name == "" { + name = acc.Name + } + if name == "" { + name = acc.ID + } + lookup[acc.ID] = name + } + } + parts := make([]string, 0, len(msg.Errors)) + for accID, err := range msg.Errors { + name := lookup[accID] + if name == "" { + name = accID + } + parts = append(parts, fmt.Sprintf("%s: %v", name, err)) + } + sort.Strings(parts) + m.previousModel = m.current + m.current = tui.NewStatus(fmt.Sprintf( + "Folder fetch failed for %d account(s): %s", + len(parts), strings.Join(parts, "; "), + )) + return m, tea.Tick(4*time.Second, func(t time.Time) tea.Msg { + return tui.RestoreViewMsg{} + }) + } return m, nil case tui.SwitchFolderMsg: @@ -2675,6 +2713,7 @@ func fetchFoldersCmd(cfg *config.Config) tea.Cmd { return nil } foldersByAccount := make(map[string][]fetcher.Folder) + errsByAccount := make(map[string]error) seen := make(map[string]fetcher.Folder) var mu sync.Mutex var wg sync.WaitGroup @@ -2685,6 +2724,9 @@ func fetchFoldersCmd(cfg *config.Config) tea.Cmd { defer wg.Done() folders, err := fetcher.FetchFolders(&acc) if err != nil { + mu.Lock() + errsByAccount[acc.ID] = err + mu.Unlock() return } mu.Lock() @@ -2707,6 +2749,7 @@ func fetchFoldersCmd(cfg *config.Config) tea.Cmd { return tui.FoldersFetchedMsg{ FoldersByAccount: foldersByAccount, MergedFolders: merged, + Errors: errsByAccount, } } } diff --git a/tui/messages.go b/tui/messages.go index 8a605eb..207ecc5 100644 --- a/tui/messages.go +++ b/tui/messages.go @@ -416,9 +416,16 @@ type RequestRefreshMsg struct { // --- Folder Messages --- // FoldersFetchedMsg signals that IMAP folders have been fetched for all accounts. +// +// Errors holds per-account fetch failures (e.g. broken IMAP login, network +// unreachable). Accounts that succeeded appear in FoldersByAccount; accounts +// that failed appear in Errors. The two are disjoint by construction. This +// lets the TUI surface a non-fatal warning instead of silently dropping the +// affected account's folder list. type FoldersFetchedMsg struct { FoldersByAccount map[string][]fetcher.Folder // accountID -> folders MergedFolders []fetcher.Folder // unique folders across all accounts + Errors map[string]error // accountID -> fetch error, if any } // SwitchFolderMsg signals switching to a different IMAP folder.