From 00e127d77241972863f505d92943565562813460 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 29 May 2025 21:29:11 +0200 Subject: [PATCH 1/2] imapmemserver: add support for the recent flag --- imapserver/imapmemserver/mailbox.go | 19 ++++++++++++++----- imapserver/imapmemserver/session.go | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/imapserver/imapmemserver/mailbox.go b/imapserver/imapmemserver/mailbox.go index dee9a67b..1b70b782 100644 --- a/imapserver/imapmemserver/mailbox.go +++ b/imapserver/imapmemserver/mailbox.go @@ -76,6 +76,10 @@ func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusD num := uint32(len(mbox.l)) data.NumMessages = &num } + if options.NumRecent { + num := mbox.countByFlagLocked("\\Recent") + data.NumRecent = &num + } if options.UIDNext { data.UIDNext = mbox.uidNext } @@ -94,10 +98,6 @@ func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusD size := mbox.sizeLocked() data.Size = &size } - if options.NumRecent { - num := uint32(0) - data.NumRecent = &num - } return &data } @@ -146,6 +146,7 @@ func (mbox *Mailbox) appendBytes(buf []byte, options *imap.AppendOptions) *imap. msg.t = options.Time } + msg.flags[canonicalFlag("\\Recent")] = struct{}{} for _, flag := range options.Flags { msg.flags[canonicalFlag(flag)] = struct{}{} } @@ -188,12 +189,14 @@ func (mbox *Mailbox) selectDataLocked() *imap.SelectData { // TODO: skip if IMAP4rev1 is disabled by the server, or IMAP4rev2 is // enabled by the client firstUnseenSeqNum := mbox.firstUnseenSeqNumLocked() + numRecent := mbox.countByFlagLocked("\\Recent") return &imap.SelectData{ Flags: flags, PermanentFlags: permanentFlags, NumMessages: uint32(len(mbox.l)), FirstUnseenSeqNum: firstUnseenSeqNum, + NumRecent: numRecent, UIDNext: mbox.uidNext, UIDValidity: mbox.uidValidity, } @@ -283,10 +286,11 @@ func (mbox *Mailbox) expungeLocked(expunged map[*message]struct{}) (seqNums []ui // NewView creates a new view into this mailbox. // // Callers must call MailboxView.Close once they are done with the mailbox view. -func (mbox *Mailbox) NewView() *MailboxView { +func (mbox *Mailbox) NewView(options *imap.SelectOptions) *MailboxView { return &MailboxView{ Mailbox: mbox, tracker: mbox.tracker.NewSession(), + options: *options, } } @@ -300,6 +304,7 @@ func (mbox *Mailbox) NewView() *MailboxView { // selected state. type MailboxView struct { *Mailbox + options imap.SelectOptions // immutable tracker *imapserver.SessionTracker searchRes imap.UIDSet } @@ -331,6 +336,10 @@ func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, op respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum)) err = msg.fetch(respWriter, options) + + if !mbox.options.ReadOnly { + delete(msg.flags, canonicalFlag("\\Recent")) + } }) return err } diff --git a/imapserver/imapmemserver/session.go b/imapserver/imapmemserver/session.go index 70e9d2f8..861fe801 100644 --- a/imapserver/imapmemserver/session.go +++ b/imapserver/imapmemserver/session.go @@ -40,7 +40,7 @@ func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap } mbox.mutex.Lock() defer mbox.mutex.Unlock() - sess.mailbox = mbox.NewView() + sess.mailbox = mbox.NewView(options) return mbox.selectDataLocked(), nil } From 99a888019e3ee6066002a806d514258a92e04214 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 11 Jun 2025 20:49:28 +0200 Subject: [PATCH 2/2] wip --- imapserver/imapmemserver/mailbox.go | 51 +++++++++++++++++++++-------- imapserver/imapmemserver/message.go | 25 ++++++++++---- imapserver/imapmemserver/session.go | 24 +++++++++++++- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/imapserver/imapmemserver/mailbox.go b/imapserver/imapmemserver/mailbox.go index 1b70b782..259fedb9 100644 --- a/imapserver/imapmemserver/mailbox.go +++ b/imapserver/imapmemserver/mailbox.go @@ -288,9 +288,10 @@ func (mbox *Mailbox) expungeLocked(expunged map[*message]struct{}) (seqNums []ui // Callers must call MailboxView.Close once they are done with the mailbox view. func (mbox *Mailbox) NewView(options *imap.SelectOptions) *MailboxView { return &MailboxView{ - Mailbox: mbox, - tracker: mbox.tracker.NewSession(), - options: *options, + Mailbox: mbox, + tracker: mbox.tracker.NewSession(), + readOnly: options.ReadOnly, + recent: make(map[imap.UID]struct{}), } } @@ -304,9 +305,11 @@ func (mbox *Mailbox) NewView(options *imap.SelectOptions) *MailboxView { // selected state. type MailboxView struct { *Mailbox - options imap.SelectOptions // immutable - tracker *imapserver.SessionTracker - searchRes imap.UIDSet + readOnly bool // immutable + tracker *imapserver.SessionTracker + searchRes imap.UIDSet + recent map[imap.UID]struct{} + prevNumRecent uint32 } // Close releases the resources allocated for the mailbox view. @@ -335,11 +338,8 @@ func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, op } respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum)) - err = msg.fetch(respWriter, options) - - if !mbox.options.ReadOnly { - delete(msg.flags, canonicalFlag("\\Recent")) - } + _, isRecent := mbox.recent[msg.uid] + err = msg.fetch(respWriter, options, isRecent) }) return err } @@ -358,7 +358,8 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc for i, msg := range mbox.l { seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1) - if !msg.search(seqNum, criteria) { + _, isRecent := mbox.recent[msg.uid] + if !msg.search(seqNum, criteria, isRecent) { continue } @@ -438,7 +439,19 @@ func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, fl } func (mbox *MailboxView) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error { - return mbox.tracker.Poll(w, allowExpunge) + if err := mbox.tracker.Poll(w, allowExpunge); err != nil { + return err + } + mbox.mutex.Lock() + mbox.pollRecentLocked() + numRecent := uint32(len(mbox.recent)) + sendNumRecent := numRecent != mbox.prevNumRecent + mbox.prevNumRecent = numRecent + mbox.mutex.Unlock() + if sendNumRecent { + w.WriteNumRecent(numRecent) + } + return nil } func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error { @@ -518,3 +531,15 @@ func staticNumRange(start, stop *uint32, max uint32) { *start, *stop = *stop, *start } } + +func (mbox *MailboxView) pollRecentLocked() { + if mbox.readOnly { + return + } + for _, msg := range mbox.l { + if _, ok := msg.flags[canonicalFlag("\\Recent")]; ok { + mbox.recent[msg.uid] = struct{}{} + delete(msg.flags, canonicalFlag("\\Recent")) + } + } +} diff --git a/imapserver/imapmemserver/message.go b/imapserver/imapmemserver/message.go index d5580459..8e2dd28a 100644 --- a/imapserver/imapmemserver/message.go +++ b/imapserver/imapmemserver/message.go @@ -25,11 +25,15 @@ type message struct { flags map[imap.Flag]struct{} } -func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error { +func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions, isRecent bool) error { w.WriteUID(msg.uid) if options.Flags { - w.WriteFlags(msg.flagList()) + flags := msg.flagList() + if isRecent { + flags = append(flags, "\\Recent") + } + w.WriteFlags(flags) } if options.InternalDate { w.WriteInternalDate(msg.t) @@ -121,7 +125,7 @@ func (msg *message) reader() *gomessage.Entity { return r } -func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool { +func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria, isRecent bool) bool { for _, seqSet := range criteria.SeqNum { if seqNum == 0 || !seqSet.Contains(seqNum) { return false @@ -136,13 +140,20 @@ func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool { return false } + hasFlag := func(flag imap.Flag) bool { + if isRecent && canonicalFlag(flag) == canonicalFlag("\\Recent") { + return true + } + _, ok := msg.flags[canonicalFlag(flag)] + return ok + } for _, flag := range criteria.Flag { - if _, ok := msg.flags[canonicalFlag(flag)]; !ok { + if !hasFlag(flag) { return false } } for _, flag := range criteria.NotFlag { - if _, ok := msg.flags[canonicalFlag(flag)]; ok { + if hasFlag(flag) { return false } } @@ -183,12 +194,12 @@ func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool { } for _, not := range criteria.Not { - if msg.search(seqNum, ¬) { + if msg.search(seqNum, ¬, isRecent) { return false } } for _, or := range criteria.Or { - if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) { + if !msg.search(seqNum, &or[0], isRecent) && !msg.search(seqNum, &or[1], isRecent) { return false } } diff --git a/imapserver/imapmemserver/session.go b/imapserver/imapmemserver/session.go index 861fe801..a00afa8a 100644 --- a/imapserver/imapmemserver/session.go +++ b/imapserver/imapmemserver/session.go @@ -41,7 +41,12 @@ func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap mbox.mutex.Lock() defer mbox.mutex.Unlock() sess.mailbox = mbox.NewView(options) - return mbox.selectDataLocked(), nil + data := mbox.selectDataLocked() + if !options.ReadOnly && data.NumRecent > 0 { + sess.mailbox.pollRecentLocked() + sess.mailbox.prevNumRecent = data.NumRecent + } + return data, nil } func (sess *UserSession) Unselect() error { @@ -138,3 +143,20 @@ func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) } return sess.mailbox.Idle(w, stop) } + +func (sess *UserSession) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) { + data, err := sess.user.Status(name, options) + if err != nil { + return nil, err + } + + if mbox := sess.mailbox; mbox != nil && data.NumRecent != nil { + mbox.mutex.Lock() + if mbox.name == name { + *data.NumRecent += uint32(len(mbox.recent)) + } + mbox.mutex.Unlock() + } + + return data, nil +}