Skip to content

Commit f2fdf37

Browse files
committed
corrected ESORT handling as per RFC5267
1 parent bff9940 commit f2fdf37

File tree

2 files changed

+93
-28
lines changed

2 files changed

+93
-28
lines changed

imapserver/imapmemserver/sort.go

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package imapmemserver
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"net/mail"
7+
"net/textproto"
48
"sort"
9+
"strings"
510

611
"github.com/emersion/go-imap/v2"
712
"github.com/emersion/go-imap/v2/imapserver"
@@ -142,31 +147,78 @@ func compareByCriterion(a, b *message, key imap.SortKey) int {
142147
return 0
143148

144149
case imap.SortKeyFrom:
145-
// TODO: For a real implementation, extract the From header and compare
146-
return 0
150+
// NOTE: A fully compliant implementation as per RFC 5256 would parse
151+
// the address and sort by mailbox, then host. This is a simplified
152+
// case-insensitive comparison of the full header value.
153+
fromA := getHeader(a.buf, "From")
154+
fromB := getHeader(b.buf, "From")
155+
return strings.Compare(strings.ToLower(fromA), strings.ToLower(fromB))
147156

148157
case imap.SortKeyTo:
149-
// TODO: For a real implementation, you would extract the To header and compare
150-
return 0
158+
// NOTE: Simplified comparison. See SortKeyFrom.
159+
toA := getHeader(a.buf, "To")
160+
toB := getHeader(b.buf, "To")
161+
return strings.Compare(strings.ToLower(toA), strings.ToLower(toB))
151162

152163
case imap.SortKeyCc:
153-
// TODO: For a real implementation, you would extract the Cc header and compare
154-
return 0
164+
// NOTE: Simplified comparison. See SortKeyFrom.
165+
ccA := getHeader(a.buf, "Cc")
166+
ccB := getHeader(b.buf, "Cc")
167+
return strings.Compare(strings.ToLower(ccA), strings.ToLower(ccB))
155168

156169
case imap.SortKeySubject:
157-
// TODO: For a real implementation, you would extract the Subject header and compare
158-
return 0
170+
// RFC 5256 specifies i;ascii-casemap collation, which is case-insensitive.
171+
subjA := getHeader(a.buf, "Subject")
172+
subjB := getHeader(b.buf, "Subject")
173+
return strings.Compare(strings.ToLower(subjA), strings.ToLower(subjB))
159174

160175
case imap.SortKeyDisplay:
161-
// SORT=DISPLAY (RFC 5957) - Use a locale-sensitive version of the string
162-
// For now, treat it the same as the subject sorting for this implementation
163-
// TODO: For a real implementation, use proper locale-aware sorting of display names
164-
// A full implementation would handle internationalized text according to
165-
// the user's locale settings and apply proper collation rules
166-
return 0
176+
// RFC 5957: sort by display-name, fallback to mailbox.
177+
fromA := getHeader(a.buf, "From")
178+
fromB := getHeader(b.buf, "From")
179+
180+
addrA, errA := mail.ParseAddress(fromA)
181+
addrB, errB := mail.ParseAddress(fromB)
182+
183+
var displayA, displayB string
184+
185+
if errA == nil {
186+
if addrA.Name != "" {
187+
displayA = addrA.Name
188+
} else {
189+
displayA = addrA.Address
190+
}
191+
} else {
192+
displayA = fromA // Fallback to raw header on parse error
193+
}
194+
195+
if errB == nil {
196+
if addrB.Name != "" {
197+
displayB = addrB.Name
198+
} else {
199+
displayB = addrB.Address
200+
}
201+
} else {
202+
displayB = fromB // Fallback to raw header on parse error
203+
}
204+
205+
// A full implementation would use locale-aware sorting (e.g., golang.org/x/text/collate).
206+
// A case-insensitive comparison is a reasonable and significant improvement.
207+
return strings.Compare(strings.ToLower(displayA), strings.ToLower(displayB))
167208

168209
default:
169210
// Default to no sorting for unknown criteria
170211
return 0
171212
}
172213
}
214+
215+
// getHeader extracts a header value from a message's raw bytes.
216+
// It performs a case-insensitive search for the key.
217+
func getHeader(buf []byte, key string) string {
218+
r := textproto.NewReader(bufio.NewReader(bytes.NewReader(buf)))
219+
hdr, err := r.ReadMIMEHeader()
220+
if err != nil {
221+
return "" // Or log the error
222+
}
223+
return hdr.Get(key)
224+
}

imapserver/sort.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -174,27 +174,34 @@ func (c *Conn) handleSort(tag string, dec *imapwire.Decoder, numKind NumKind) er
174174
data := &SortData{Nums: sortedNums}
175175
if len(sortedNums) > 0 {
176176
data.Count = uint32(len(sortedNums))
177-
data.Min = sortedNums[0]
178-
data.Max = sortedNums[len(sortedNums)-1]
177+
min, max := sortedNums[0], sortedNums[0]
178+
for _, num := range sortedNums {
179+
if num < min {
180+
min = num
181+
}
182+
if num > max {
183+
max = num
184+
}
185+
}
186+
data.Min = min
187+
data.Max = max
179188
}
180189

181190
return c.writeSortResponse(tag, numKind, data, &esortReturnOpts)
182191
}
183192

184193
func (c *Conn) writeSortResponse(tag string, numKind NumKind, data *SortData, returnOpts *esortReturnOptions) error {
185-
enc := newResponseEncoder(c)
186-
defer enc.end()
187-
188-
// For ESORT, use ESEARCH response format if not RETURN ALL
194+
// For ESORT, if RETURN options other than ALL are specified, send an ESEARCH response.
195+
// See RFC 5267 section 4.2.
189196
if c.server.options.caps().Has(imap.CapESort) && !returnOpts.All {
197+
enc := newResponseEncoder(c)
190198
enc.Atom("*").SP().Atom("ESEARCH")
191199
if tag != "" {
192-
enc.SP().Special('(').Atom("TAG").SP().Atom(tag).Special(')')
200+
enc.SP().Special('(').Atom("TAG").SP().String(tag).Special(')')
193201
}
194202
if numKind == NumKindUID {
195203
enc.SP().Atom("UID")
196204
}
197-
198205
if returnOpts.Min {
199206
if data.Count > 0 {
200207
enc.SP().Atom("MIN").SP().Number(data.Min)
@@ -208,14 +215,20 @@ func (c *Conn) writeSortResponse(tag string, numKind NumKind, data *SortData, re
208215
if returnOpts.Count {
209216
enc.SP().Atom("COUNT").SP().Number(data.Count)
210217
}
211-
// Note: No "ALL <seq-set>" here for ESORT as per RFC 5267
212-
} else {
213-
// Use regular SORT response for non-ESORT clients or when RETURN (ALL) or no RETURN option is specified
214-
enc.Atom("*").SP().Atom("SORT")
215-
for _, num := range data.Nums {
216-
enc.SP().Number(num)
218+
if err := enc.CRLF(); err != nil {
219+
enc.end()
220+
return err
217221
}
222+
enc.end()
218223
}
219224

225+
// A SORT response is always sent, either for a regular SORT, or following
226+
// an ESEARCH response for an ESORT.
227+
enc := newResponseEncoder(c)
228+
defer enc.end()
229+
enc.Atom("*").SP().Atom("SORT")
230+
for _, num := range data.Nums {
231+
enc.SP().Number(num)
232+
}
220233
return enc.CRLF()
221234
}

0 commit comments

Comments
 (0)