Skip to content

Commit 2898e19

Browse files
authored
RFC 5256 - SORT command (#180)
1 parent 400e80a commit 2898e19

File tree

5 files changed

+342
-20
lines changed

5 files changed

+342
-20
lines changed

src/client.rs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,15 +1275,53 @@ impl<T: Read + Write> Session<T> {
12751275
/// - `SINCE <date>`: Messages whose internal date (disregarding time and timezone) is within or later than the specified date.
12761276
pub fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
12771277
self.run_command_and_read_response(&format!("SEARCH {}", query.as_ref()))
1278-
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
1278+
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
12791279
}
12801280

12811281
/// Equivalent to [`Session::search`], except that the returned identifiers
12821282
/// are [`Uid`] instead of [`Seq`]. See also the [`UID`
12831283
/// command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
12841284
pub fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
12851285
self.run_command_and_read_response(&format!("UID SEARCH {}", query.as_ref()))
1286-
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
1286+
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
1287+
}
1288+
1289+
/// This issues the [SORT command](https://tools.ietf.org/html/rfc5256#section-3),
1290+
/// which returns sorted search results.
1291+
///
1292+
/// This command is like [`Session::search`], except that
1293+
/// the results are also sorted according to the supplied criteria (subject to the given charset).
1294+
pub fn sort<S: AsRef<str>>(
1295+
&mut self,
1296+
criteria: &[extensions::sort::SortCriterion<'_>],
1297+
charset: extensions::sort::SortCharset<'_>,
1298+
query: S,
1299+
) -> Result<Vec<Seq>> {
1300+
self.run_command_and_read_response(&format!(
1301+
"SORT {} {} {}",
1302+
extensions::sort::SortCriteria(criteria),
1303+
charset,
1304+
query.as_ref()
1305+
))
1306+
.and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx))
1307+
}
1308+
1309+
/// Equivalent to [`Session::sort`], except that it returns [`Uid`]s.
1310+
///
1311+
/// See also [`Session::uid_search`].
1312+
pub fn uid_sort<S: AsRef<str>>(
1313+
&mut self,
1314+
criteria: &[extensions::sort::SortCriterion<'_>],
1315+
charset: extensions::sort::SortCharset<'_>,
1316+
query: S,
1317+
) -> Result<Vec<Uid>> {
1318+
self.run_command_and_read_response(&format!(
1319+
"UID SORT {} {} {}",
1320+
extensions::sort::SortCriteria(criteria),
1321+
charset,
1322+
query.as_ref()
1323+
))
1324+
.and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx))
12871325
}
12881326

12891327
// these are only here because they are public interface, the rest is in `Connection`
@@ -1843,6 +1881,51 @@ mod tests {
18431881
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
18441882
}
18451883

1884+
#[test]
1885+
fn sort() {
1886+
use extensions::sort::{SortCharset, SortCriterion};
1887+
1888+
let response = b"* SORT 1 2 3 4 5\r\n\
1889+
a1 OK Sort completed\r\n"
1890+
.to_vec();
1891+
let mock_stream = MockStream::new(response);
1892+
let mut session = mock_session!(mock_stream);
1893+
let ids = session
1894+
.sort(&[SortCriterion::Arrival], SortCharset::Utf8, "ALL")
1895+
.unwrap();
1896+
let ids: Vec<u32> = ids.iter().cloned().collect();
1897+
assert!(
1898+
session.stream.get_ref().written_buf == b"a1 SORT (ARRIVAL) UTF-8 ALL\r\n".to_vec(),
1899+
"Invalid sort command"
1900+
);
1901+
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::<Vec<_>>());
1902+
}
1903+
1904+
#[test]
1905+
fn uid_sort() {
1906+
use extensions::sort::{SortCharset, SortCriterion};
1907+
1908+
let response = b"* SORT 1 2 3 4 5\r\n\
1909+
a1 OK Sort completed\r\n"
1910+
.to_vec();
1911+
let mock_stream = MockStream::new(response);
1912+
let mut session = mock_session!(mock_stream);
1913+
let ids = session
1914+
.uid_sort(
1915+
&[SortCriterion::Reverse(&SortCriterion::Size)],
1916+
SortCharset::UsAscii,
1917+
"SUBJECT",
1918+
)
1919+
.unwrap();
1920+
let ids: Vec<Uid> = ids.iter().cloned().collect();
1921+
assert!(
1922+
session.stream.get_ref().written_buf
1923+
== b"a1 UID SORT (REVERSE SIZE) US-ASCII SUBJECT\r\n".to_vec(),
1924+
"Invalid sort command"
1925+
);
1926+
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::<Vec<_>>());
1927+
}
1928+
18461929
#[test]
18471930
fn capability() {
18481931
let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\

src/extensions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
//! Implementations of various IMAP extensions.
22
pub mod idle;
33
pub mod metadata;
4+
pub mod sort;

src/extensions/sort.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Adds support for the IMAP SORT extension specificed in [RFC
2+
//! 5464](https://tools.ietf.org/html/rfc5256#section-3).
3+
//!
4+
//! The SORT command is a variant of SEARCH with sorting semantics for
5+
//! the results. There are two arguments before the searching
6+
//! criteria argument: a parenthesized list of sort criteria, and the
7+
//! searching charset.
8+
9+
use std::{borrow::Cow, fmt};
10+
11+
pub(crate) struct SortCriteria<'c>(pub(crate) &'c [SortCriterion<'c>]);
12+
13+
impl<'c> fmt::Display for SortCriteria<'c> {
14+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15+
if self.0.is_empty() {
16+
write!(f, "")
17+
} else {
18+
let criteria: Vec<String> = self.0.iter().map(|c| c.to_string()).collect();
19+
write!(f, "({})", criteria.join(" "))
20+
}
21+
}
22+
}
23+
24+
/// Message sorting preferences used for [`Session::sort`] and [`Session::uid_sort`].
25+
///
26+
/// Any sorting criterion that refers to an address (`From`, `To`, etc.) sorts according to the
27+
/// "addr-mailbox" of the indicated address. You can find the formal syntax for addr-mailbox [in
28+
/// the IMAP spec](https://tools.ietf.org/html/rfc3501#section-9), and a more detailed discussion
29+
/// of the relevant semantics [in RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.4.1).
30+
/// Essentially, the address refers _either_ to the name of the contact _or_ to its local-part (the
31+
/// left part of the email address, before the `@`).
32+
#[non_exhaustive]
33+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
34+
pub enum SortCriterion<'c> {
35+
/// Internal date and time of the message. This differs from the
36+
/// ON criteria in SEARCH, which uses just the internal date.
37+
Arrival,
38+
39+
/// IMAP addr-mailbox of the first "Cc" address.
40+
Cc,
41+
42+
/// Sent date and time, as described in
43+
/// [section 2.2](https://tools.ietf.org/html/rfc5256#section-2.2).
44+
Date,
45+
46+
/// IMAP addr-mailbox of the first "From" address.
47+
From,
48+
49+
/// Followed by another sort criterion, has the effect of that
50+
/// criterion but in reverse (descending) order.
51+
Reverse(&'c SortCriterion<'c>),
52+
53+
/// Size of the message in octets.
54+
Size,
55+
56+
/// Base subject text.
57+
Subject,
58+
59+
/// IMAP addr-mailbox of the first "To" address.
60+
To,
61+
}
62+
63+
impl<'c> fmt::Display for SortCriterion<'c> {
64+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65+
use SortCriterion::*;
66+
67+
match self {
68+
Arrival => write!(f, "ARRIVAL"),
69+
Cc => write!(f, "CC"),
70+
Date => write!(f, "DATE"),
71+
From => write!(f, "FROM"),
72+
Reverse(c) => write!(f, "REVERSE {}", c),
73+
Size => write!(f, "SIZE"),
74+
Subject => write!(f, "SUBJECT"),
75+
To => write!(f, "TO"),
76+
}
77+
}
78+
}
79+
80+
/// The character encoding to use for strings that are subject to a [`SortCriterion`].
81+
///
82+
/// Servers are only required to implement [`SortCharset::UsAscii`] and [`SortCharset::Utf8`].
83+
#[non_exhaustive]
84+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85+
pub enum SortCharset<'c> {
86+
/// Strings are UTF-8 encoded.
87+
Utf8,
88+
89+
/// Strings are encoded with ASCII.
90+
UsAscii,
91+
92+
/// Strings are encoded using some other character set.
93+
///
94+
/// Note that this option is subject to server support for the specified character set.
95+
Custom(Cow<'c, str>),
96+
}
97+
98+
impl<'c> fmt::Display for SortCharset<'c> {
99+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100+
use SortCharset::*;
101+
102+
match self {
103+
Utf8 => write!(f, "UTF-8"),
104+
UsAscii => write!(f, "US-ASCII"),
105+
Custom(c) => write!(f, "{}", c),
106+
}
107+
}
108+
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
114+
#[test]
115+
fn test_criterion_to_string() {
116+
use SortCriterion::*;
117+
118+
assert_eq!("ARRIVAL", Arrival.to_string());
119+
assert_eq!("CC", Cc.to_string());
120+
assert_eq!("DATE", Date.to_string());
121+
assert_eq!("FROM", From.to_string());
122+
assert_eq!("SIZE", Size.to_string());
123+
assert_eq!("SUBJECT", Subject.to_string());
124+
assert_eq!("TO", To.to_string());
125+
assert_eq!("REVERSE TO", Reverse(&To).to_string());
126+
assert_eq!("REVERSE REVERSE TO", Reverse(&Reverse(&To)).to_string());
127+
}
128+
129+
#[test]
130+
fn test_criteria_to_string() {
131+
use SortCriterion::*;
132+
133+
assert_eq!("", SortCriteria(&[]).to_string());
134+
assert_eq!("(ARRIVAL)", SortCriteria(&[Arrival]).to_string());
135+
assert_eq!(
136+
"(ARRIVAL REVERSE FROM)",
137+
SortCriteria(&[Arrival, Reverse(&From)]).to_string()
138+
);
139+
assert_eq!(
140+
"(ARRIVAL REVERSE REVERSE REVERSE FROM)",
141+
SortCriteria(&[Arrival, Reverse(&Reverse(&Reverse(&From)))]).to_string()
142+
);
143+
}
144+
145+
#[test]
146+
fn test_charset_to_string() {
147+
use SortCharset::*;
148+
149+
assert_eq!("UTF-8", Utf8.to_string());
150+
assert_eq!("US-ASCII", UsAscii.to_string());
151+
assert_eq!("CHARSET", Custom("CHARSET".into()).to_string());
152+
}
153+
}

src/parse.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,21 +315,25 @@ pub fn parse_mailbox(
315315
}
316316
}
317317

318-
pub fn parse_ids(
318+
fn parse_ids_with<T: Extend<u32>>(
319319
lines: &[u8],
320320
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
321-
) -> Result<HashSet<u32>> {
321+
mut collection: T,
322+
) -> Result<T> {
322323
let mut lines = lines;
323-
let mut ids = HashSet::new();
324324
loop {
325325
if lines.is_empty() {
326-
break Ok(ids);
326+
break Ok(collection);
327327
}
328328

329329
match imap_proto::parser::parse_response(lines) {
330330
Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => {
331331
lines = rest;
332-
ids.extend(c);
332+
collection.extend(c);
333+
}
334+
Ok((rest, Response::MailboxData(MailboxDatum::Sort(c)))) => {
335+
lines = rest;
336+
collection.extend(c);
333337
}
334338
Ok((rest, data)) => {
335339
lines = rest;
@@ -344,6 +348,20 @@ pub fn parse_ids(
344348
}
345349
}
346350

351+
pub fn parse_id_set(
352+
lines: &[u8],
353+
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
354+
) -> Result<HashSet<u32>> {
355+
parse_ids_with(lines, unsolicited, HashSet::new())
356+
}
357+
358+
pub fn parse_id_seq(
359+
lines: &[u8],
360+
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
361+
) -> Result<Vec<u32>> {
362+
parse_ids_with(lines, unsolicited, Vec::new())
363+
}
364+
347365
/// Parse a single unsolicited response from IDLE responses.
348366
pub fn parse_idle(lines: &[u8]) -> (&[u8], Option<Result<UnsolicitedResponse>>) {
349367
match imap_proto::parser::parse_response(lines) {
@@ -547,7 +565,7 @@ mod tests {
547565
* 1 RECENT\r\n\
548566
* STATUS INBOX (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n";
549567
let (mut send, recv) = mpsc::channel();
550-
let ids = parse_ids(lines, &mut send).unwrap();
568+
let ids = parse_id_set(lines, &mut send).unwrap();
551569

552570
assert_eq!(ids, [23, 42, 4711].iter().cloned().collect());
553571

@@ -571,7 +589,7 @@ mod tests {
571589
let lines = b"* SEARCH 1600 1698 1739 1781 1795 1885 1891 1892 1893 1898 1899 1901 1911 1926 1932 1933 1993 1994 2007 2032 2033 2041 2053 2062 2063 2065 2066 2072 2078 2079 2082 2084 2095 2100 2101 2102 2103 2104 2107 2116 2120 2135 2138 2154 2163 2168 2172 2189 2193 2198 2199 2205 2212 2213 2221 2227 2267 2275 2276 2295 2300 2328 2330 2332 2333 2334\r\n\
572590
* SEARCH 2335 2336 2337 2338 2339 2341 2342 2347 2349 2350 2358 2359 2362 2369 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2390 2392 2397 2400 2401 2403 2405 2409 2411 2414 2417 2419 2420 2424 2426 2428 2439 2454 2456 2467 2468 2469 2490 2515 2519 2520 2521\r\n";
573591
let (mut send, recv) = mpsc::channel();
574-
let ids = parse_ids(lines, &mut send).unwrap();
592+
let ids = parse_id_set(lines, &mut send).unwrap();
575593
assert!(recv.try_recv().is_err());
576594
let ids: HashSet<u32> = ids.iter().cloned().collect();
577595
assert_eq!(
@@ -594,10 +612,17 @@ mod tests {
594612

595613
let lines = b"* SEARCH\r\n";
596614
let (mut send, recv) = mpsc::channel();
597-
let ids = parse_ids(lines, &mut send).unwrap();
615+
let ids = parse_id_set(lines, &mut send).unwrap();
598616
assert!(recv.try_recv().is_err());
599617
let ids: HashSet<u32> = ids.iter().cloned().collect();
600618
assert_eq!(ids, HashSet::<u32>::new());
619+
620+
let lines = b"* SORT\r\n";
621+
let (mut send, recv) = mpsc::channel();
622+
let ids = parse_id_seq(lines, &mut send).unwrap();
623+
assert!(recv.try_recv().is_err());
624+
let ids: Vec<u32> = ids.iter().cloned().collect();
625+
assert_eq!(ids, Vec::<u32>::new());
601626
}
602627

603628
#[test]

0 commit comments

Comments
 (0)