From afdeb9622ee1aad9dc5e58f20813f8a1da7946a5 Mon Sep 17 00:00:00 2001 From: joao-crm Date: Sun, 23 Aug 2026 19:42:15 +0000 Subject: [PATCH] feat: release the selected IMAP mailbox before the LIST-STATUS poll so Dovecot servers stop reporting a frozen HIGHESTMODSEQ and live sync keeps detecting new mail after the first fetch --- internal/app/worker/wmail/sync_imap.go | 3 +++ internal/client/smtpimap/imap/client.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/internal/app/worker/wmail/sync_imap.go b/internal/app/worker/wmail/sync_imap.go index 5de4b9f2..b7fecf3b 100644 --- a/internal/app/worker/wmail/sync_imap.go +++ b/internal/app/worker/wmail/sync_imap.go @@ -33,6 +33,9 @@ func (w *WMail) Sync(ctx context.Context) *errx.MailError { stats := &tickStats{} client := w.SmtpImapData.ImapClient + // A mailbox left selected by the previous pass freezes LIST-STATUS on this + // connection, so release it before asking what changed. + client.ReleaseMailbox() folders, err := client.Folders() if err != nil { return err diff --git a/internal/client/smtpimap/imap/client.go b/internal/client/smtpimap/imap/client.go index ca3725d7..479e6897 100644 --- a/internal/client/smtpimap/imap/client.go +++ b/internal/client/smtpimap/imap/client.go @@ -193,6 +193,17 @@ func (c *Client) SelectForSync(mailbox string) (uint32, *errx.MailError) { return data.NumMessages, nil } +// ReleaseMailbox drops the selected mailbox. Dovecot answers LIST-STATUS for +// the selected mailbox with the values it held at SELECT, so a loop that keeps +// INBOX selected never sees another change land. Servers without UNSELECT keep +// the previous behaviour. +func (c *Client) ReleaseMailbox() { + if c.client == nil || !c.client.Caps().Has(imap.CapUnselect) { + return + } + _ = c.client.Unselect().Wait() +} + // Fetched is one message's envelope as read by FetchEnvelopes, plus what // FetchBody needs to read its text parts later. Bodies are deliberately a // second step: the sync loop decides per message whether it is new and