8000 imapserver: add CONDSTORE support by dejanstrbac · Pull Request #687 · emersion/go-imap · GitHub
[go: up one dir, main page]

Skip to content

imapserver: add CONDSTORE support #687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add status and search support for condstore
  • Loading branch information
mercata committed May 19, 2025
commit 1c633b478fa5c22ab8a15f641f7dc82f90107516
94 changes: 93 additions & 1 deletion imapclient/condstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,98 @@ func TestStore_UnchangedSince(t *testing.T) {
t.Errorf("Second Store() with UNCHANGEDSINCE returned %d messages, should be 0", len(messages))
}
}
func TestStatus_HighestModSeq(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer client.Close()
defer server.Close()

// Test STATUS with HIGHESTMODSEQ parameter
options := &imap.StatusOptions{
HighestModSeq: true,
}
data, err := client.Status("INBOX", options).Wait()
if err != nil {
t.Fatalf("Status() with HIGHESTMODSEQ = %v", err)
}

// Verify that HighestModSeq is returned
if data.HighestModSeq == 0 {
t.Errorf("StatusData.HighestModSeq is 0, expected non-zero value")
}
t.Logf("Mailbox HIGHESTMODSEQ from STATUS: %d", data.HighestModSeq)
}

func TestSearch_ModSeq(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()

// First, get current ModSeq for our message
seqSet := imap.SeqSetNum(1)
firstFetch, err := client.Fetch(seqSet, &imap.FetchOptions{
ModSeq: true,
}).Collect()
if err != nil {
t.Fatalf("Initial Fetch() = %v", err)
}
currentModSeq := firstFetch[0].ModSeq
t.Logf("Initial ModSeq: %d", currentModSeq)

// Now search with MODSEQ criterion using a value lower than current
// This should find the message
searchCriteria := &imap.SearchCriteria{
ModSeq: &imap.SearchCriteriaModSeq{
ModSeq: currentModSeq - 1,
},
}
searchOptions := &imap.SearchOptions{
ReturnCount: true,
}
results, err := client.Search(searchCriteria, searchOptions).Wait()
if err != nil {
t.Fatalf("Search with MODSEQ = %v", err)
}

// There should be one message that matches
if results.Count != 1 {
t.Errorf("Search with MODSEQ < current returned %d messages, want 1", results.Count)
}

// Now search with MODSEQ criterion using current value
// This should find the message (since MODSEQ criterion is >= not >)
searchCriteria = &imap.SearchCriteria{
ModSeq: &imap.SearchCriteriaModSeq{
ModSeq: currentModSeq,
},
}
results, err = client.Search(searchCriteria, searchOptions).Wait()
if err != nil {
t.Fatalf("Search with MODSEQ = %v", err)
}

// There should be one message that matches
if results.Count != 1 {
t.Errorf("Search with MODSEQ = current returned %d messages, want 1", results.Count)
}

// Now search with MODSEQ criterion using a higher value
// This should NOT find the message
searchCriteria = &imap.SearchCriteria{
ModSeq: &imap.SearchCriteriaModSeq{
ModSeq: currentModSeq + 1,
},
}
results, err = client.Search(searchCriteria, searchOptions).Wait()
if err != nil {
t.Fatalf("Search with MODSEQ = %v", err)
}

// There should be no messages that match
if results.Count != 0 {
t.Errorf("Search with MODSEQ > current returned %d messages, want 0", results.Count)
}
}

func TestCapability_CondStore(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
defer client.Close()
Expand Down Expand Up @@ -216,4 +308,4 @@ func TestCapability_CondStore(t *testing.T) {
} else {
t.Logf("CONDSTORE capability correctly announced after authentication")
}
}
}
3 changes: 3 additions & 0 deletions imapserver/imapmemserver/mailbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusD
size := mbox.sizeLocked()
data.Size = &size
}
if options.HighestModSeq {
data.HighestModSeq = mbox.highestModSeq
}
return &data
}

Expand Down
4 changes: 4 additions & 0 deletions imapserver/imapmemserver/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
return false
}

if criteria.ModSeq != nil && msg.modSeq < criteria.ModSeq.ModSeq {
return false
}

for _, flag := range criteria.Flag {
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
return false
Expand Down
31 changes: 31 additions & 0 deletions imapserver/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,37 @@ func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder,
criteria.Or = append(criteria.Or, or)
case "$":
criteria.UID = append(criteria.UID, imap.SearchRes())
case "MODSEQ":
if !dec.ExpectSP() {
return dec.Err()
}
var quotedName, name string
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need two variables for the name here? A single should be enough?

If Quoted doesn't find a quoted string, it leaves the pointer unchanged.

var metadataType imap.SearchCriteriaMetadataType
if dec.Quoted(&quotedName) {
name = quotedName
if !dec.ExpectSP() {
return dec.Err()
}
var typeName string
if !dec.ExpectAtom(&typeName) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this can be combined with ExpectSP above.

return dec.Err()
}
metadataType = imap.SearchCriteriaMetadataType(strings.ToLower(typeName))
if !dec.ExpectSP() {
return dec.Err()
}
}

var modSeq int64
if !dec.ExpectNumber64(&modSeq) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use ExpectModSeq here?

return dec.Err()
}

criteria.ModSeq = &imap.SearchCriteriaModSeq{
ModSeq: uint64(modSeq),
MetadataName: name,
MetadataType: metadataType,
}
default:
seqSet, err := imapwire.ParseSeqSet(key)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions imapserver/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func (c *Conn) writeStatus(data *imap.StatusData, options *imap.StatusOptions) e
if options.NumRecent {
listEnc.Item().Atom("RECENT").SP().Number(*data.NumRecent)
}
if options.HighestModSeq {
listEnc.Item().Atom("HIGHESTMODSEQ").SP().Number64(int64(data.HighestModSeq))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to use ModSeq instead of Number64 here. This would avoid sending a negative number if a modseq is high.

}
listEnc.End()

return enc.CRLF()
Expand Down Expand Up @@ -115,6 +118,8 @@ func readStatusItem(dec *imapwire.Decoder, options *imap.StatusOptions) error {
options.DeletedStorage = true
case "RECENT":
options.NumRecent = true
case "HIGHESTMODSEQ":
options.HighestModSeq = true
default:
return &imap.Error{
Type: imap.StatusResponseTypeBad,
Expand Down
0