1use core::{cmp::Ordering, fmt, mem, num::NonZeroU32, str::from_utf8};
62
63use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec};
64
65use chrono::{DateTime, FixedOffset};
66
67use imap_codec::{
68 CommandCodec,
69 fragmentizer::Fragmentizer,
70 imap_types::{
71 command::{Command, CommandBody},
72 core::{Charset, TagGenerator, Vec1},
73 extensions::sort::{SortCriterion, SortKey},
74 fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
75 response::{Data, StatusKind, Tagged},
76 search::SearchKey,
77 sequence::SequenceSet,
78 },
79};
80use log::{debug, trace};
81use thiserror::Error;
82
83use crate::{
84 coroutine::*,
85 imap_try,
86 rfc3501::{fetch::*, search::*},
87 send::*,
88};
89
90const MAX_CHUNK: usize = 255;
92
93#[derive(Clone, Debug, Error)]
95pub enum ImapMessageSortError {
96 #[error("IMAP SORT failed: NO {0}")]
98 No(String),
99 #[error("IMAP SORT failed: BAD {0}")]
101 Bad(String),
102 #[error("IMAP SORT failed: BYE {0}")]
104 Bye(String),
105 #[error("IMAP SORT failed: server did not return a tagged response")]
107 MissingTagged,
108 #[error("IMAP SORT failed: server did not return any data")]
110 MissingData,
111 #[error("IMAP SORT failed: could not build the fetch sequence set")]
113 InvalidSequenceSet,
114 #[error("IMAP SORT failed: {0}")]
116 Send(#[from] ImapSendError),
117 #[error(transparent)]
119 Search(#[from] ImapMessageSearchError),
120 #[error(transparent)]
122 Fetch(#[from] ImapMessageFetchError),
123}
124
125#[derive(Clone, Debug, Default, Eq, PartialEq)]
127pub struct ImapMessageSortOptions {
128 pub uid: bool,
130 pub fallback: bool,
136}
137
138pub struct ImapMessageSort {
144 state: State,
145 uid: bool,
146 sort_criteria: Vec1<SortCriterion>,
147 items: Vec<MessageDataItemName<'static>>,
148 remaining: Vec<NonZeroU32>,
149 fetched: BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>,
150}
151
152impl ImapMessageSort {
153 pub fn new(
159 sort_criteria: Vec1<SortCriterion>,
160 search_criteria: Vec1<SearchKey<'static>>,
161 opts: ImapMessageSortOptions,
162 ) -> Self {
163 let items = fetch_items(&sort_criteria, opts.uid);
164
165 let state = if opts.fallback {
166 trace!("using IMAP SORT fallback");
167 let search =
168 ImapMessageSearch::new(search_criteria, ImapMessageSearchOptions { uid: opts.uid });
169 State::Search(search)
170 } else {
171 let command = Command {
172 tag: TagGenerator::new().generate(),
173 body: CommandBody::Sort {
174 sort_criteria: sort_criteria.clone(),
175 charset: Charset::try_from("UTF-8").unwrap(),
176 search_criteria,
177 uid: opts.uid,
178 },
179 };
180
181 trace!("send IMAP command {command:?}");
182
183 State::Sort(ImapSend::new(CommandCodec::new(), command))
184 };
185
186 Self {
187 state,
188 uid: opts.uid,
189 sort_criteria,
190 items,
191 remaining: Vec::new(),
192 fetched: BTreeMap::new(),
193 }
194 }
195
196 fn next_fetch(&mut self) -> Result<Option<ImapMessageFetch>, ImapMessageSortError> {
199 if self.remaining.is_empty() {
200 return Ok(None);
201 }
202
203 let take = self.remaining.len().min(MAX_CHUNK);
204 let chunk: Vec<NonZeroU32> = self.remaining.drain(..take).collect();
205 let sequence_set =
206 SequenceSet::try_from(chunk).map_err(|_| ImapMessageSortError::InvalidSequenceSet)?;
207 let items = MacroOrMessageDataItemNames::MessageDataItemNames(self.items.clone());
208
209 Ok(Some(ImapMessageFetch::new(
210 sequence_set,
211 items,
212 ImapMessageFetchOptions {
213 uid: self.uid,
214 ..Default::default()
215 },
216 )))
217 }
218
219 fn local_sort(&mut self) -> Vec<NonZeroU32> {
221 let uid = self.uid;
222 let criteria = self.sort_criteria.clone();
223 let mut entries: Vec<(NonZeroU32, Vec1<MessageDataItem<'static>>)> =
224 mem::take(&mut self.fetched).into_iter().collect();
225
226 entries.sort_by(|(_, a), (_, b)| {
227 for criterion in criteria.as_ref() {
228 let mut cmp = cmp_fetch_items(&criterion.key, a, b);
229
230 if criterion.reverse {
231 cmp = cmp.reverse();
232 }
233
234 if cmp.is_ne() {
235 return cmp;
236 }
237 }
238
239 cmp_fetch_items(&SortKey::Date, a, b)
240 });
241
242 entries
243 .into_iter()
244 .filter_map(|(seq, items)| {
245 if uid {
246 items.as_ref().iter().find_map(|item| match item {
247 MessageDataItem::Uid(uid) => Some(*uid),
248 _ => None,
249 })
250 } else {
251 Some(seq)
252 }
253 })
254 .collect()
255 }
256}
257
258impl ImapCoroutine for ImapMessageSort {
259 type Yield = ImapYield;
260 type Return = Result<Vec<NonZeroU32>, ImapMessageSortError>;
261
262 fn resume(
263 &mut self,
264 fragmentizer: &mut Fragmentizer,
265 arg: Option<&[u8]>,
266 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
267 loop {
268 match &mut self.state {
269 State::Sort(send) => {
270 let out = imap_try!(send, fragmentizer, arg);
271
272 if let Some(bye) = out.bye {
273 let err = ImapMessageSortError::Bye(bye.text.to_string());
274 return ImapCoroutineState::Complete(Err(err));
275 }
276
277 let Some(Tagged { body, .. }) = out.tagged else {
278 let err = ImapMessageSortError::MissingTagged;
279 return ImapCoroutineState::Complete(Err(err));
280 };
281
282 let mut ids = None;
283
284 for data in out.data {
285 if let Data::Sort(sort_ids, _) = data {
286 ids = Some(sort_ids);
287 }
288 }
289
290 return match body.kind {
291 StatusKind::Ok => match ids {
292 Some(ids) => ImapCoroutineState::Complete(Ok(ids)),
293 None => {
294 ImapCoroutineState::Complete(Err(ImapMessageSortError::MissingData))
295 }
296 },
297 StatusKind::No => {
298 let err = ImapMessageSortError::No(body.text.to_string());
299 ImapCoroutineState::Complete(Err(err))
300 }
301 StatusKind::Bad => {
302 let err = ImapMessageSortError::Bad(body.text.to_string());
303 ImapCoroutineState::Complete(Err(err))
304 }
305 };
306 }
307 State::Search(search) => {
308 self.remaining = imap_try!(search, fragmentizer, arg);
309
310 match self.next_fetch() {
311 Ok(Some(fetch)) => {
312 self.state = State::Fetch(fetch);
313 debug!("{}", self.state);
314 }
315 Ok(None) => return ImapCoroutineState::Complete(Ok(Vec::new())),
316 Err(err) => return ImapCoroutineState::Complete(Err(err)),
317 }
318 }
319 State::Fetch(fetch) => {
320 let map = imap_try!(fetch, fragmentizer, arg);
321 self.fetched.extend(map);
322
323 match self.next_fetch() {
324 Ok(Some(fetch)) => {
325 self.state = State::Fetch(fetch);
326 debug!("{}", self.state);
327 }
328 Ok(None) => return ImapCoroutineState::Complete(Ok(self.local_sort())),
329 Err(err) => return ImapCoroutineState::Complete(Err(err)),
330 }
331 }
332 }
333 }
334 }
335}
336
337enum State {
338 Sort(ImapSend<CommandCodec>),
339 Search(ImapMessageSearch),
340 Fetch(ImapMessageFetch),
341}
342
343impl fmt::Display for State {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::Sort(_) => f.write_str("send sort command"),
347 Self::Search(_) => f.write_str("search for candidates"),
348 Self::Fetch(_) => f.write_str("fetch sort keys"),
349 }
350 }
351}
352
353fn fetch_items(
359 sort_criteria: &Vec1<SortCriterion>,
360 uid: bool,
361) -> Vec<MessageDataItemName<'static>> {
362 let mut items: Vec<MessageDataItemName<'static>> = Vec::new();
363
364 for criterion in sort_criteria.as_ref() {
365 let item = match &criterion.key {
366 SortKey::Arrival => Some(MessageDataItemName::InternalDate),
367 SortKey::Size => Some(MessageDataItemName::Rfc822Size),
368 SortKey::Cc | SortKey::Date | SortKey::From | SortKey::Subject | SortKey::To => {
369 Some(MessageDataItemName::Envelope)
370 }
371 SortKey::DisplayFrom | SortKey::DisplayTo => None,
372 };
373
374 if let Some(item) = item {
375 if !items.contains(&item) {
376 items.push(item);
377 }
378 }
379 }
380
381 if !items.contains(&MessageDataItemName::Envelope) {
382 items.push(MessageDataItemName::Envelope);
383 }
384
385 if uid && !items.contains(&MessageDataItemName::Uid) {
386 items.push(MessageDataItemName::Uid);
387 }
388
389 items
390}
391
392fn cmp_fetch_items(
398 key: &SortKey,
399 a: &Vec1<MessageDataItem<'static>>,
400 b: &Vec1<MessageDataItem<'static>>,
401) -> Ordering {
402 match key {
403 SortKey::Arrival => {
404 let a = a.as_ref().iter().find_map(|item| match item {
405 MessageDataItem::InternalDate(date) => Some(date.as_ref()),
406 _ => None,
407 });
408 let b = b.as_ref().iter().find_map(|item| match item {
409 MessageDataItem::InternalDate(date) => Some(date.as_ref()),
410 _ => None,
411 });
412 a.cmp(&b)
413 }
414 SortKey::Size => {
415 let a = a.as_ref().iter().find_map(|item| match item {
416 MessageDataItem::Rfc822Size(size) => Some(size),
417 _ => None,
418 });
419 let b = b.as_ref().iter().find_map(|item| match item {
420 MessageDataItem::Rfc822Size(size) => Some(size),
421 _ => None,
422 });
423 a.cmp(&b)
424 }
425 SortKey::Date => {
426 let a = a.as_ref().iter().find_map(|item| match item {
430 MessageDataItem::Envelope(envelope) => envelope.date.0.as_ref().map(AsRef::as_ref),
431 _ => None,
432 });
433 let b = b.as_ref().iter().find_map(|item| match item {
434 MessageDataItem::Envelope(envelope) => envelope.date.0.as_ref().map(AsRef::as_ref),
435 _ => None,
436 });
437 date_sort_key(a).cmp(&date_sort_key(b))
438 }
439 SortKey::Subject => {
440 let a = a.as_ref().iter().find_map(|item| match item {
441 MessageDataItem::Envelope(envelope) => {
442 envelope.subject.0.as_ref().map(AsRef::as_ref)
443 }
444 _ => None,
445 });
446 let b = b.as_ref().iter().find_map(|item| match item {
447 MessageDataItem::Envelope(envelope) => {
448 envelope.subject.0.as_ref().map(AsRef::as_ref)
449 }
450 _ => None,
451 });
452 a.cmp(&b)
453 }
454 SortKey::Cc | SortKey::From | SortKey::To | SortKey::DisplayFrom | SortKey::DisplayTo => {
455 Ordering::Equal
456 }
457 }
458}
459
460fn date_sort_key(raw: Option<&[u8]>) -> Option<DateTime<FixedOffset>> {
466 let raw = from_utf8(raw?).ok()?;
467 DateTime::parse_from_rfc2822(raw).ok()
468}
469
470#[cfg(test)]
471mod tests {
472 use core::str;
473
474 use alloc::{borrow::ToOwned, format, vec, vec::Vec};
475
476 use crate::rfc5256::sort::*;
477
478 fn arrival() -> Vec1<SortCriterion> {
479 Vec1::try_from(vec![SortCriterion {
480 reverse: false,
481 key: SortKey::Arrival,
482 }])
483 .expect("one sort criterion")
484 }
485
486 fn search_criteria() -> Vec1<SearchKey<'static>> {
487 Vec1::try_from(vec![SearchKey::All]).expect("one search criterion")
488 }
489
490 fn first_word(line: &str) -> &str {
491 line.split_whitespace()
492 .next()
493 .expect("first whitespace-separated token")
494 }
495
496 #[test]
497 fn sort_success_returns_ids() {
498 let mut sort = ImapMessageSort::new(
499 arrival(),
500 search_criteria(),
501 ImapMessageSortOptions::default(),
502 );
503 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
504
505 let bytes = wants_write(&mut sort, &mut frag, None);
506 let line = str::from_utf8(&bytes).expect("utf8 command");
507 let tag = first_word(line).to_owned();
508 assert!(line.contains("SORT "));
509 assert!(!line.contains("SEARCH"));
510
511 wants_read(&mut sort, &mut frag, None);
512
513 let reply = format!("* SORT 3 1 2\r\n{tag} OK SORT completed\r\n");
514 let ids = complete_ok(&mut sort, &mut frag, Some(reply.as_bytes()));
515 assert_eq!(
516 vec![nz(3), nz(1), nz(2)],
517 ids,
518 "server order preserved verbatim"
519 );
520 }
521
522 #[test]
523 fn sort_uid_variant_sends_uid_sort() {
524 let mut sort = ImapMessageSort::new(
525 arrival(),
526 search_criteria(),
527 ImapMessageSortOptions {
528 uid: true,
529 ..Default::default()
530 },
531 );
532 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
533
534 let bytes = wants_write(&mut sort, &mut frag, None);
535 let line = str::from_utf8(&bytes).expect("utf8 command");
536 assert!(line.contains("UID SORT "));
537 }
538
539 #[test]
540 fn sort_missing_data_returns_missing_data_error() {
541 let mut sort = ImapMessageSort::new(
542 arrival(),
543 search_criteria(),
544 ImapMessageSortOptions::default(),
545 );
546 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
547
548 let bytes = wants_write(&mut sort, &mut frag, None);
549 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
550
551 wants_read(&mut sort, &mut frag, None);
552
553 let reply = format!("{tag} OK SORT completed\r\n");
554 let err = complete_err(&mut sort, &mut frag, reply.as_bytes());
555 assert!(matches!(err, ImapMessageSortError::MissingData));
556 }
557
558 #[test]
559 fn sort_tagged_no_returns_no_error() {
560 let mut sort = ImapMessageSort::new(
561 arrival(),
562 search_criteria(),
563 ImapMessageSortOptions::default(),
564 );
565 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
566
567 let bytes = wants_write(&mut sort, &mut frag, None);
568 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
569
570 wants_read(&mut sort, &mut frag, None);
571
572 let reply = format!("{tag} NO no mailbox selected\r\n");
573 let err = complete_err(&mut sort, &mut frag, reply.as_bytes());
574 let ImapMessageSortError::No(text) = err else {
575 panic!("expected ImapMessageSortError::No, got {err:?}");
576 };
577 assert_eq!(text, "no mailbox selected");
578 }
579
580 #[test]
581 fn fallback_searches_then_fetches_then_sorts() {
582 let mut sort = ImapMessageSort::new(
583 arrival(),
584 search_criteria(),
585 ImapMessageSortOptions {
586 uid: true,
587 fallback: true,
588 },
589 );
590 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
591
592 let bytes = wants_write(&mut sort, &mut frag, None);
593 let line = str::from_utf8(&bytes).expect("utf8 command");
594 let search_tag = first_word(line).to_owned();
595 assert!(line.contains("UID SEARCH"));
596
597 wants_read(&mut sort, &mut frag, None);
598
599 let search_reply = format!("* SEARCH 1 2\r\n{search_tag} OK SEARCH completed\r\n");
600 let bytes = wants_write(&mut sort, &mut frag, Some(search_reply.as_bytes()));
601 let line = str::from_utf8(&bytes).expect("utf8 command");
602 let fetch_tag = first_word(line).to_owned();
603 assert!(line.contains("UID FETCH"));
604 assert!(line.contains("INTERNALDATE"));
605 assert!(line.contains("UID"));
606
607 wants_read(&mut sort, &mut frag, None);
608
609 let fetch_reply = format!(
612 "* 1 FETCH (UID 1 INTERNALDATE \"02-Feb-2021 00:00:00 +0000\")\r\n\
613 * 2 FETCH (UID 2 INTERNALDATE \"01-Jan-2020 00:00:00 +0000\")\r\n\
614 {fetch_tag} OK FETCH completed\r\n"
615 );
616 let ids = complete_ok(&mut sort, &mut frag, Some(fetch_reply.as_bytes()));
617 assert_eq!(vec![nz(2), nz(1)], ids);
618 }
619
620 #[test]
621 fn fallback_reverse_flips_order() {
622 let criteria = Vec1::try_from(vec![SortCriterion {
623 reverse: true,
624 key: SortKey::Arrival,
625 }])
626 .expect("one criterion");
627 let mut sort = ImapMessageSort::new(
628 criteria,
629 search_criteria(),
630 ImapMessageSortOptions {
631 uid: true,
632 fallback: true,
633 },
634 );
635 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
636
637 let bytes = wants_write(&mut sort, &mut frag, None);
638 let search_tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
639 wants_read(&mut sort, &mut frag, None);
640
641 let search_reply = format!("* SEARCH 1 2\r\n{search_tag} OK SEARCH completed\r\n");
642 let bytes = wants_write(&mut sort, &mut frag, Some(search_reply.as_bytes()));
643 let fetch_tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
644 wants_read(&mut sort, &mut frag, None);
645
646 let fetch_reply = format!(
647 "* 1 FETCH (UID 1 INTERNALDATE \"02-Feb-2021 00:00:00 +0000\")\r\n\
648 * 2 FETCH (UID 2 INTERNALDATE \"01-Jan-2020 00:00:00 +0000\")\r\n\
649 {fetch_tag} OK FETCH completed\r\n"
650 );
651 let ids = complete_ok(&mut sort, &mut frag, Some(fetch_reply.as_bytes()));
652 assert_eq!(vec![nz(1), nz(2)], ids);
653 }
654
655 #[test]
656 fn fallback_empty_search_returns_empty_without_fetch() {
657 let mut sort = ImapMessageSort::new(
658 arrival(),
659 search_criteria(),
660 ImapMessageSortOptions {
661 uid: true,
662 fallback: true,
663 },
664 );
665 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
666
667 let bytes = wants_write(&mut sort, &mut frag, None);
668 let search_tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
669 wants_read(&mut sort, &mut frag, None);
670
671 let search_reply = format!("* SEARCH\r\n{search_tag} OK SEARCH completed\r\n");
672 let ids = complete_ok(&mut sort, &mut frag, Some(search_reply.as_bytes()));
673 assert!(ids.is_empty());
674 }
675
676 #[test]
677 fn date_sort_key_is_chronological_not_lexical() {
678 let mon = date_sort_key(Some(b"Mon, 13 Jul 2026 09:00:00 +0200"));
681 let fri = date_sort_key(Some(b"Fri, 17 Jul 2026 16:20:00 +0200"));
682
683 assert!(mon < fri, "13 Jul must sort before 17 Jul");
685 assert!(*b"Fri, 17 Jul 2026 16:20:00 +0200" < *b"Mon, 13 Jul 2026 09:00:00 +0200");
687
688 let utc = date_sort_key(Some(b"Mon, 13 Jul 2026 10:00:00 +0000"));
690 let cest = date_sort_key(Some(b"Mon, 13 Jul 2026 11:00:00 +0200"));
691 assert!(cest < utc, "09:00Z must sort before 10:00Z");
692
693 assert_eq!(date_sort_key(None), None);
695 assert_eq!(date_sort_key(Some(b"not a date")), None);
696 assert!(date_sort_key(None) < mon);
697 }
698
699 fn nz(n: u32) -> NonZeroU32 {
700 NonZeroU32::new(n).expect("non-zero")
701 }
702
703 fn wants_write(
704 cor: &mut ImapMessageSort,
705 frag: &mut Fragmentizer,
706 arg: Option<&[u8]>,
707 ) -> Vec<u8> {
708 match cor.resume(frag, arg) {
709 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
710 state => panic!("expected WantsWrite, got {state:?}"),
711 }
712 }
713
714 fn wants_read(cor: &mut ImapMessageSort, frag: &mut Fragmentizer, arg: Option<&[u8]>) {
715 match cor.resume(frag, arg) {
716 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
717 state => panic!("expected WantsRead, got {state:?}"),
718 }
719 }
720
721 fn complete_ok(
722 cor: &mut ImapMessageSort,
723 frag: &mut Fragmentizer,
724 arg: Option<&[u8]>,
725 ) -> Vec<NonZeroU32> {
726 match cor.resume(frag, arg) {
727 ImapCoroutineState::Complete(Ok(value)) => value,
728 state => panic!("expected Complete(Ok), got {state:?}"),
729 }
730 }
731
732 fn complete_err(
733 cor: &mut ImapMessageSort,
734 frag: &mut Fragmentizer,
735 reply: &[u8],
736 ) -> ImapMessageSortError {
737 match cor.resume(frag, Some(reply)) {
738 ImapCoroutineState::Complete(Err(err)) => err,
739 state => panic!("expected Complete(Err), got {state:?}"),
740 }
741 }
742}