From d14a4cb9da23c7b8b7fa8c54310e3090584f1666 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Thu, 4 Jun 2026 08:28:31 +0400 Subject: [PATCH] fix: gate status request (#1427) ## What? Gate the `LIST ... RETURN (STATUS (UNSEEN))` request in `FetchFolders` on the server advertising the LIST-STATUS capability (RFC 5819). When the server doesn't support it, send a plain `LIST "" "*"` and populate unread counts with a per-mailbox `STATUS (UNSEEN)` fallback instead. ## Why? Fixes #1426 Signed-off-by: drew --- fetcher/fetcher.go | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index edce6ed216a577da603631b0e68beb5b240e3f5b..6c0f27fdc3f21dcfac1425ca7a5a40c011a0df3b 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -1919,11 +1919,18 @@ func FetchFolders(account *config.Account) ([]Folder, error) { } defer c.Close() //nolint:errcheck - listCmd := c.List("", "*", &imap.ListOptions{ - ReturnStatus: &imap.StatusOptions{ - NumUnseen: true, - }, - }) + // Only request the unseen count inline via LIST ... RETURN (STATUS (UNSEEN)) + // when the server advertises LIST-STATUS (RFC 5819). Servers without it + // (e.g. Proton Mail Bridge, Exchange Online) either reject the RETURN + // argument outright or send a reply go-imap can't parse (#1408). For those + // we fall back to a per-mailbox STATUS below. + hasListStatus := c.Caps().Has(imap.CapListStatus) + listOpts := &imap.ListOptions{} + if hasListStatus { + listOpts.ReturnStatus = &imap.StatusOptions{NumUnseen: true} + } + + listCmd := c.List("", "*", listOpts) defer listCmd.Close() //nolint:errcheck var folders []Folder @@ -1938,7 +1945,7 @@ func FetchFolders(account *config.Account) ([]Folder, error) { } var unread uint32 - if data.Status != nil { + if data.Status != nil && data.Status.NumUnseen != nil { unread = *data.Status.NumUnseen } @@ -1958,6 +1965,27 @@ func FetchFolders(account *config.Account) ([]Folder, error) { return nil, err } + // Without LIST-STATUS the LIST reply carries no unseen counts, so issue a + // STATUS per mailbox to populate them. Skip \Noselect folders, which can't + // be queried with STATUS. + if !hasListStatus { + for i := range folders { + if slices.Contains(folders[i].Attributes, string(imap.MailboxAttrNoSelect)) { + continue + } + status, err := c.Status(folders[i].Name, &imap.StatusOptions{NumUnseen: true}).Wait() + if err != nil { + // A single failing mailbox shouldn't abort the whole listing; + // leave its unread count at zero. + loglevel.Debugf("STATUS UNSEEN failed for %q: %v", folders[i].Name, err) + continue + } + if status.NumUnseen != nil { + folders[i].Unread = *status.NumUnseen + } + } + } + return folders, nil }