diff --git a/internal/app/worker/wmail/sync_imap.go b/internal/app/worker/wmail/sync_imap.go index be0c78d5..31ae71af 100644 --- a/internal/app/worker/wmail/sync_imap.go +++ b/internal/app/worker/wmail/sync_imap.go @@ -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 + } } } diff --git a/internal/client/smtpimap/imap/client.go b/internal/client/smtpimap/imap/client.go index 61559ce7..411e4fa0 100644 --- a/internal/client/smtpimap/imap/client.go +++ b/internal/client/smtpimap/imap/client.go @@ -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 {