Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,15 +495,15 @@ 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 {
return nil
}
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]
}
}
Expand Down
43 changes: 43 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -2707,6 +2749,7 @@ func fetchFoldersCmd(cfg *config.Config) tea.Cmd {
return tui.FoldersFetchedMsg{
FoldersByAccount: foldersByAccount,
MergedFolders: merged,
Errors: errsByAccount,
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions tui/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading