Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions imapclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,8 @@ func (c *Client) readResponseData(typ string) error {
return c.handleESearch()
case "SORT":
return c.handleSort()
case "ESORT":
return c.handleESort()
case "THREAD":
return c.handleThread()
case "METADATA":
Expand Down
3 changes: 3 additions & 0 deletions imapclient/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchDa
if isUID {
numKind = imapwire.NumKindUID
}
if !dec.ExpectSP() {
return "", nil, dec.Err()
}
if !dec.ExpectNumSet(numKind, &data.All) {
return "", nil, dec.Err()
}
Expand Down
152 changes: 129 additions & 23 deletions imapclient/sort.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
package imapclient

import (
"fmt"
"strings"

"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapnum"
"github.com/emersion/go-imap/v2/internal/imapwire"
)

type SortKey string

const (
SortKeyArrival SortKey = "ARRIVAL"
SortKeyCc SortKey = "CC"
SortKeyDate SortKey = "DATE"
SortKeyFrom SortKey = "FROM"
SortKeySize SortKey = "SIZE"
SortKeySubject SortKey = "SUBJECT"
SortKeyTo SortKey = "TO"
)

type SortCriterion struct {
Key SortKey
Reverse bool
}

// SortOptions contains options for the SORT command.
type SortOptions struct {
// The search criteria for the messages to sort.
SearchCriteria *imap.SearchCriteria
SortCriteria []SortCriterion
// A list of criteria to sort by.
SortCriteria []imap.SortCriterion
// Return options for ESORT. If any are set, an extended SORT is used.
Return imap.SortOptions
}

// SortData is the data returned by a SORT or ESORT command.
type SortData struct {
// A list of matching message numbers, in sorted order.
// Populated if Return.All is true (default for SORT).
// Either SeqNums or UIDs is populated.
SeqNums []uint32
UIDs []imap.UID

// The following fields are only populated for ESORT.
Min uint32
Max uint32
Count uint32
}

func (c *Client) sort(numKind imapwire.NumKind, options *SortOptions) *SortCommand {
cmd := &SortCommand{}
cmd := &SortCommand{numKind: numKind}
enc := c.beginCommand(uidCmdName("SORT", numKind), cmd)

isESort := options.Return.ReturnMin || options.Return.ReturnMax || options.Return.ReturnAll || options.Return.ReturnCount
if isESort {
enc.SP().Atom("RETURN").SP()
var returnOpts []string
if options.Return.ReturnMin {
returnOpts = append(returnOpts, "MIN")
}
if options.Return.ReturnMax {
returnOpts = append(returnOpts, "MAX")
}
if options.Return.ReturnAll {
returnOpts = append(returnOpts, "ALL")
}
if options.Return.ReturnCount {
returnOpts = append(returnOpts, "COUNT")
}
enc.List(len(returnOpts), func(i int) {
enc.Atom(returnOpts[i])
})
}

enc.SP().List(len(options.SortCriteria), func(i int) {
criterion := options.SortCriteria[i]
if criterion.Reverse {
Expand All @@ -52,12 +79,90 @@ func (c *Client) handleSort() error {
return c.dec.Err()
}
if cmd != nil {
cmd.nums = append(cmd.nums, num)
if cmd.numKind == imapwire.NumKindSeq {
cmd.data.SeqNums = append(cmd.data.SeqNums, num)
} else {
cmd.data.UIDs = append(cmd.data.UIDs, imap.UID(num))
}
}
}
return nil
}

func (c *Client) handleESort() error {
cmd := findPendingCmdByType[*SortCommand](c)
if cmd == nil {
// This is an unsolicited ESORT response, parse and discard its parameters
for c.dec.SP() {
if !c.dec.DiscardValue() {
return c.dec.Err()
}
}
return nil
}

isUID := cmd.numKind == imapwire.NumKindUID

for c.dec.SP() {
var s string
if c.dec.Special('(') {
var key, tag string
if !c.dec.ExpectAtom(&key) || !strings.EqualFold(key, "TAG") || !c.dec.ExpectSP() || !c.dec.ExpectAString(&tag) || !c.dec.ExpectSpecial(')') {
return c.dec.Err()
}
continue
}

if !c.dec.ExpectAtom(&s) {
return c.dec.Err()
}

switch strings.ToUpper(s) {
case "UID":
isUID = true
case "MIN":
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&cmd.data.Min) {
return c.dec.Err()
}
case "MAX":
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&cmd.data.Max) {
return c.dec.Err()
}
case "COUNT":
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&cmd.data.Count) {
return c.dec.Err()
}
case "ALL":
var seqSetStr string
if !c.dec.ExpectSP() || !c.dec.ExpectAtom(&seqSetStr) {
return c.dec.Err()
}
set, err := imapnum.ParseSet(seqSetStr)
if err != nil {
return fmt.Errorf("in ALL seq-set: %w", err)
}

nums, ok := set.Nums()
if !ok {
return fmt.Errorf("esort: ALL contained a dynamic set, which is not allowed")
}

if isUID {
cmd.data.UIDs = make([]imap.UID, len(nums))
for i, n := range nums {
cmd.data.UIDs[i] = imap.UID(n)
}
} else {
cmd.data.SeqNums = nums
}
default:
return fmt.Errorf("unknown ESORT return option: %q", s)
}
}

return nil
}

// Sort sends a SORT command.
//
// This command requires support for the SORT extension.
Expand All @@ -75,10 +180,11 @@ func (c *Client) UIDSort(options *SortOptions) *SortCommand {
// SortCommand is a SORT command.
type SortCommand struct {
commandBase
nums []uint32
numKind imapwire.NumKind
data SortData
}

func (cmd *SortCommand) Wait() ([]uint32, error) {
func (cmd *SortCommand) Wait() (*SortData, error) {
err := cmd.wait()
return cmd.nums, err
return &cmd.data, err
}
Loading