diff --git a/imapserver/imapmemserver/mailbox.go b/imapserver/imapmemserver/mailbox.go index dee9a67b..259fedb9 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,12 @@ 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(), + Mailbox: mbox, + tracker: mbox.tracker.NewSession(), + readOnly: options.ReadOnly, + recent: make(map[imap.UID]struct{}), } } @@ -300,8 +305,11 @@ func (mbox *Mailbox) NewView() *MailboxView { // selected state. type MailboxView struct { *Mailbox - 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. @@ -330,7 +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) + _, isRecent := mbox.recent[msg.uid] + err = msg.fetch(respWriter, options, isRecent) }) return err } @@ -349,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 } @@ -429,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 { @@ -509,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 70e9d2f8..a00afa8a 100644 --- a/imapserver/imapmemserver/session.go +++ b/imapserver/imapmemserver/session.go @@ -40,8 +40,13 @@ func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap } mbox.mutex.Lock() defer mbox.mutex.Unlock() - sess.mailbox = mbox.NewView() - return mbox.selectDataLocked(), nil + sess.mailbox = mbox.NewView(options) + 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 +}