diff --git a/internal/client/smtpimap/imap/client.go b/internal/client/smtpimap/imap/client.go index 81daa477..8f1a22f1 100644 --- a/internal/client/smtpimap/imap/client.go +++ b/internal/client/smtpimap/imap/client.go @@ -66,12 +66,7 @@ func (c *Client) Connect() *errx.MailError { return errx.ErrMailServerUnreachable } - client := imapclient.New(conn, nil) - - caps := client.Caps() - if !caps.Has(imap.CapCondStore) { - return errx.ErrMailCondStoreNotSupported - } + c.client = imapclient.New(conn, nil) var xerr *errx.MailError @@ -81,8 +76,18 @@ func (c *Client) Connect() *errx.MailError { case models.AuthOAuth2: xerr = c.oauth2Auth() } + if xerr != nil { + return xerr + } - return xerr + // CONDSTORE backs the ChangedSince incremental sync. Servers (Gmail, + // Dovecot, ...) typically advertise it only after authentication, so the + // check must run post-auth. + if !c.client.Caps().Has(imap.CapCondStore) { + return errx.ErrMailCondStoreNotSupported + } + + return nil } func (c *Client) Close() error { @@ -126,7 +131,14 @@ func (c *Client) oauth2Auth() *errx.MailError { func (c *Client) Folders() ([]models.Mailbox, *errx.MailError) { var resp []models.Mailbox - cmd := c.client.List("", "%", nil) + // LIST-STATUS: without requesting these, f.Status is nil for every + // folder and the sync loop sees an empty account. + cmd := c.client.List("", "%", &imap.ListOptions{ + ReturnStatus: &imap.StatusOptions{ + UIDValidity: true, + HighestModSeq: true, + }, + }) for f := cmd.Next(); f != nil; f = cmd.Next() { if len(resp) >= config.MaxEmailFolders { @@ -167,7 +179,11 @@ func (c *Client) Mailbox(mailbox string, uidvali, opts *imap.SelectOptions) erro } func (c *Client) FetchChanges(ctx context.Context, lastModSeq uint64) *errx.MailError { - cmd := c.client.Fetch(&imap.SeqSet{}, &imap.FetchOptions{ + // 1:* — an empty SeqSet has no encodable ranges and panics inside + // go-imap; ChangedSince narrows the result server-side. + var allMessages imap.SeqSet + allMessages.AddRange(1, 0) + cmd := c.client.Fetch(allMessages, &imap.FetchOptions{ UID: true, Envelope: true, BodyStructure: &imap.FetchItemBodyStructure{