feat: return the message count from the sync SELECT and skip FETCH on empty mailboxes - a 1:* sequence set against zero messages is a server error, so every fresh mailbox failed its first sync pass before any mail arrived

This commit is contained in:
Matthew Meszaros
2026-07-11 19:47:14 +02:00
parent 1403ed4e80
commit bb646709fa
2 changed files with 24 additions and 14 deletions
+14 -7
View File
@@ -26,12 +26,16 @@ func (w *WMail) Sync(ctx context.Context) *errx.MailError {
}
// FETCH requires a selected mailbox; the select also arms
// CONDSTORE for the ChangedSince filtering.
if err := w.SmtpImapData.ImapClient.SelectForSync(box.Name); err != nil {
// CONDSTORE for the ChangedSince filtering. An empty mailbox is
// skipped: 1:* on zero messages is a server error.
count, err := w.SmtpImapData.ImapClient.SelectForSync(box.Name)
if err != nil {
return err
}
if err := w.SmtpImapData.ImapClient.FetchChanges(ctx, 0); err != nil {
return err
if count > 0 {
if err := w.SmtpImapData.ImapClient.FetchChanges(ctx, 0); err != nil {
return err
}
}
w.SmtpImapData.Mailboxes = append(w.SmtpImapData.Mailboxes, &box)
@@ -40,11 +44,14 @@ func (w *WMail) Sync(ctx context.Context) *errx.MailError {
if befBox.HighestModSeq != box.HighestModSeq {
w.SmtpImapData.mailbox = box.UIDValidity
if err := w.SmtpImapData.ImapClient.SelectForSync(box.Name); err != nil {
count, err := w.SmtpImapData.ImapClient.SelectForSync(box.Name)
if err != nil {
return err
}
if err := w.SmtpImapData.ImapClient.FetchChanges(ctx, befBox.HighestModSeq); err != nil {
return err
if count > 0 {
if err := w.SmtpImapData.ImapClient.FetchChanges(ctx, befBox.HighestModSeq); err != nil {
return err
}
}
}
+10 -7
View File
@@ -178,14 +178,17 @@ func (c *Client) Mailbox(mailbox string, uidvali, opts *imap.SelectOptions) erro
return nil
}
// SelectForSync opens a mailbox read-only with CONDSTORE enabled. FETCH is
// only valid against a selected mailbox, so the sync loop must call this
// before FetchChanges; CONDSTORE on the SELECT is what arms ChangedSince.
func (c *Client) SelectForSync(mailbox string) *errx.MailError {
if _, err := c.client.Select(mailbox, &imap.SelectOptions{ReadOnly: true, CondStore: true}).Wait(); err != nil {
return c.handleError(err)
// SelectForSync opens a mailbox read-only with CONDSTORE enabled and returns
// its message count. FETCH is only valid against a selected mailbox, so the
// sync loop must call this before FetchChanges; CONDSTORE on the SELECT is
// what arms ChangedSince. The count lets the caller skip the fetch entirely
// for an empty mailbox, where a 1:* set is a server error.
func (c *Client) SelectForSync(mailbox string) (uint32, *errx.MailError) {
data, err := c.client.Select(mailbox, &imap.SelectOptions{ReadOnly: true, CondStore: true}).Wait()
if err != nil {
return 0, c.handleError(err)
}
return nil
return data.NumMessages, nil
}
func (c *Client) FetchChanges(ctx context.Context, lastModSeq uint64) *errx.MailError {