fix: gate status request (#1427)

Drew Smirnoff created

## 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 <me@andrinoff.com>

Change summary

fetcher/fetcher.go | 40 ++++++++++++++++++++++++++++++++++------
1 file changed, 34 insertions(+), 6 deletions(-)

Detailed changes

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
 }