|
| 1 | +package imapmemserver |
| 2 | + |
| 3 | +import ( |
| 4 | + "bufio" |
| 5 | + "bytes" |
| 6 | + "net/mail" |
| 7 | + "net/textproto" |
| 8 | + "sort" |
| 9 | + "strings" |
| 10 | + |
| 11 | + "github.com/emersion/go-imap/v2" |
| 12 | + "github.com/emersion/go-imap/v2/imapserver" |
| 13 | +) |
| 14 | + |
| 15 | +// Sort performs a SORT command. |
| 16 | +func (mbox *MailboxView) Sort(numKind imapserver.NumKind, criteria *imap.SearchCriteria, sortCriteria []imap.SortCriterion) ([]uint32, error) { |
| 17 | + mbox.mutex.Lock() |
| 18 | + defer mbox.mutex.Unlock() |
| 19 | + |
| 20 | + // Apply search criteria |
| 21 | + mbox.staticSearchCriteria(criteria) |
| 22 | + |
| 23 | + // First find all messages that match the search criteria |
| 24 | + var matchedMessages []*message |
| 25 | + var matchedSeqNums []uint32 |
| 26 | + var matchedIndices []int |
| 27 | + for i, msg := range mbox.l { |
| 28 | + seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1) |
| 29 | + |
| 30 | + if !msg.search(seqNum, criteria) { |
| 31 | + continue |
| 32 | + } |
| 33 | + |
| 34 | + matchedMessages = append(matchedMessages, msg) |
| 35 | + matchedSeqNums = append(matchedSeqNums, seqNum) |
| 36 | + matchedIndices = append(matchedIndices, i) |
| 37 | + } |
| 38 | + |
| 39 | + // Sort the matched messages based on the sort criteria |
| 40 | + sortMatchedMessages(matchedMessages, matchedSeqNums, matchedIndices, sortCriteria) |
| 41 | + |
| 42 | + // Create sorted response |
| 43 | + var data []uint32 |
| 44 | + for i, msg := range matchedMessages { |
| 45 | + var num uint32 |
| 46 | + switch numKind { |
| 47 | + case imapserver.NumKindSeq: |
| 48 | + if matchedSeqNums[i] == 0 { |
| 49 | + continue |
| 50 | + } |
| 51 | + num = matchedSeqNums[i] |
| 52 | + case imapserver.NumKindUID: |
| 53 | + num = uint32(msg.uid) |
| 54 | + } |
| 55 | + data = append(data, num) |
| 56 | + } |
| 57 | + |
| 58 | + return data, nil |
| 59 | +} |
| 60 | + |
| 61 | +// sortMatchedMessages sorts messages according to the specified sort criteria |
| 62 | +func sortMatchedMessages(messages []*message, seqNums []uint32, indices []int, criteria []imap.SortCriterion) { |
| 63 | + if len(messages) < 2 { |
| 64 | + return // Nothing to sort |
| 65 | + } |
| 66 | + |
| 67 | + // Create a slice of indices for sorting |
| 68 | + indices2 := make([]int, len(messages)) |
| 69 | + for i := range indices2 { |
| 70 | + indices2[i] = i |
| 71 | + } |
| 72 | + |
| 73 | + // Sort the indices based on the criteria |
| 74 | + sort.SliceStable(indices2, func(i, j int) bool { |
| 75 | + i2, j2 := indices2[i], indices2[j] |
| 76 | + |
| 77 | + // Apply each criterion in order until we find a difference |
| 78 | + for _, criterion := range criteria { |
| 79 | + result := compareByCriterion(messages[i2], messages[j2], criterion.Key) |
| 80 | + |
| 81 | + // Apply reverse if needed |
| 82 | + if criterion.Reverse { |
| 83 | + result = -result |
| 84 | + } |
| 85 | + |
| 86 | + // If comparison yields a difference, return the result |
| 87 | + if result < 0 { |
| 88 | + return true |
| 89 | + } else if result > 0 { |
| 90 | + return false |
| 91 | + } |
| 92 | + // If equal, continue to the next criterion |
| 93 | + } |
| 94 | + |
| 95 | + // If all criteria are equal, maintain original order |
| 96 | + return i < j |
| 97 | + }) |
| 98 | + |
| 99 | + // Reorder the original slices according to the sorted indices |
| 100 | + newMessages := make([]*message, len(messages)) |
| 101 | + newSeqNums := make([]uint32, len(seqNums)) |
| 102 | + newIndices := make([]int, len(indices)) |
| 103 | + |
| 104 | + for i, idx := range indices2 { |
| 105 | + newMessages[i] = messages[idx] |
| 106 | + newSeqNums[i] = seqNums[idx] |
| 107 | + newIndices[i] = indices[idx] |
| 108 | + } |
| 109 | + |
| 110 | + // Copy sorted slices back to original slices |
| 111 | + copy(messages, newMessages) |
| 112 | + copy(seqNums, newSeqNums) |
| 113 | + copy(indices, newIndices) |
| 114 | +} |
| 115 | + |
| 116 | +// compareByCriterion compares two messages based on a single criterion |
| 117 | +// returns -1 if a < b, 0 if a == b, 1 if a > b |
| 118 | +func compareByCriterion(a, b *message, key imap.SortKey) int { |
| 119 | + switch key { |
| 120 | + case imap.SortKeyArrival: |
| 121 | + // For ARRIVAL, we use the UID as the arrival order |
| 122 | + if a.uid < b.uid { |
| 123 | + return -1 |
| 124 | + } else if a.uid > b.uid { |
| 125 | + return 1 |
| 126 | + } |
| 127 | + return 0 |
| 128 | + |
| 129 | + case imap.SortKeyDate: |
| 130 | + // Compare internal date |
| 131 | + if a.t.Before(b.t) { |
| 132 | + return -1 |
| 133 | + } else if a.t.After(b.t) { |
| 134 | + return 1 |
| 135 | + } |
| 136 | + return 0 |
| 137 | + |
| 138 | + case imap.SortKeySize: |
| 139 | + // Compare message sizes |
| 140 | + aSize := len(a.buf) |
| 141 | + bSize := len(b.buf) |
| 142 | + if aSize < bSize { |
| 143 | + return -1 |
| 144 | + } else if aSize > bSize { |
| 145 | + return 1 |
| 146 | + } |
| 147 | + return 0 |
| 148 | + |
| 149 | + case imap.SortKeyFrom: |
| 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)) |
| 156 | + |
| 157 | + case imap.SortKeyTo: |
| 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)) |
| 162 | + |
| 163 | + case imap.SortKeyCc: |
| 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)) |
| 168 | + |
| 169 | + case imap.SortKeySubject: |
| 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)) |
| 174 | + |
| 175 | + case imap.SortKeyDisplay: |
| 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)) |
| 208 | + |
| 209 | + default: |
| 210 | + // Default to no sorting for unknown criteria |
| 211 | + return 0 |
| 212 | + } |
| 213 | +} |
| 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 | +} |
0 commit comments