Skip to content
Merged
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
87 changes: 85 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1275,15 +1275,53 @@ impl<T: Read + Write> Session<T> {
/// - `SINCE <date>`: Messages whose internal date (disregarding time and timezone) is within or later than the specified date.
pub fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
self.run_command_and_read_response(&format!("SEARCH {}", query.as_ref()))
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
}

/// Equivalent to [`Session::search`], except that the returned identifiers
/// are [`Uid`] instead of [`Seq`]. See also the [`UID`
/// command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
pub fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
self.run_command_and_read_response(&format!("UID SEARCH {}", query.as_ref()))
.and_then(|lines| parse_ids(&lines, &mut self.unsolicited_responses_tx))
.and_then(|lines| parse_id_set(&lines, &mut self.unsolicited_responses_tx))
}

/// This issues the [SORT command](https://tools.ietf.org/html/rfc5256#section-3),
/// which returns sorted search results.
///
/// This command is like [`Session::search`], except that
/// the results are also sorted according to the supplied criteria (subject to the given charset).
pub fn sort<S: AsRef<str>>(
&mut self,
criteria: &[extensions::sort::SortCriterion<'_>],
charset: extensions::sort::SortCharset<'_>,
query: S,
) -> Result<Vec<Seq>> {
self.run_command_and_read_response(&format!(
"SORT {} {} {}",
extensions::sort::SortCriteria(criteria),
charset,
query.as_ref()
))
.and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx))
}

/// Equivalent to [`Session::sort`], except that it returns [`Uid`]s.
///
/// See also [`Session::uid_search`].
pub fn uid_sort<S: AsRef<str>>(
&mut self,
criteria: &[extensions::sort::SortCriterion<'_>],
charset: extensions::sort::SortCharset<'_>,
query: S,
) -> Result<Vec<Uid>> {
self.run_command_and_read_response(&format!(
"UID SORT {} {} {}",
extensions::sort::SortCriteria(criteria),
charset,
query.as_ref()
))
.and_then(|lines| parse_id_seq(&lines, &mut self.unsolicited_responses_tx))
}

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

#[test]
fn sort() {
use extensions::sort::{SortCharset, SortCriterion};

let response = b"* SORT 1 2 3 4 5\r\n\
a1 OK Sort completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let ids = session
.sort(&[SortCriterion::Arrival], SortCharset::Utf8, "ALL")
.unwrap();
let ids: Vec<u32> = ids.iter().cloned().collect();
assert!(
session.stream.get_ref().written_buf == b"a1 SORT (ARRIVAL) UTF-8 ALL\r\n".to_vec(),
"Invalid sort command"
);
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::<Vec<_>>());
}

#[test]
fn uid_sort() {
use extensions::sort::{SortCharset, SortCriterion};

let response = b"* SORT 1 2 3 4 5\r\n\
a1 OK Sort completed\r\n"
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);
let ids = session
.uid_sort(
&[SortCriterion::Reverse(&SortCriterion::Size)],
SortCharset::UsAscii,
"SUBJECT",
)
.unwrap();
let ids: Vec<Uid> = ids.iter().cloned().collect();
assert!(
session.stream.get_ref().written_buf
== b"a1 UID SORT (REVERSE SIZE) US-ASCII SUBJECT\r\n".to_vec(),
"Invalid sort command"
);
assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect::<Vec<_>>());
}

#[test]
fn capability() {
let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\
Expand Down
1 change: 1 addition & 0 deletions src/extensions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Implementations of various IMAP extensions.
pub mod idle;
pub mod metadata;
pub mod sort;
153 changes: 153 additions & 0 deletions src/extensions/sort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Adds support for the IMAP SORT extension specificed in [RFC
//! 5464](https://tools.ietf.org/html/rfc5256#section-3).
//!
//! The SORT command is a variant of SEARCH with sorting semantics for
//! the results. There are two arguments before the searching
//! criteria argument: a parenthesized list of sort criteria, and the
//! searching charset.

use std::{borrow::Cow, fmt};

pub(crate) struct SortCriteria<'c>(pub(crate) &'c [SortCriterion<'c>]);

impl<'c> fmt::Display for SortCriteria<'c> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "")
} else {
let criteria: Vec<String> = self.0.iter().map(|c| c.to_string()).collect();
write!(f, "({})", criteria.join(" "))
}
}
}

/// Message sorting preferences used for [`Session::sort`] and [`Session::uid_sort`].
///
/// Any sorting criterion that refers to an address (`From`, `To`, etc.) sorts according to the
/// "addr-mailbox" of the indicated address. You can find the formal syntax for addr-mailbox [in
/// the IMAP spec](https://tools.ietf.org/html/rfc3501#section-9), and a more detailed discussion
/// of the relevant semantics [in RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.4.1).
/// Essentially, the address refers _either_ to the name of the contact _or_ to its local-part (the
/// left part of the email address, before the `@`).
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub enum SortCriterion<'c> {
/// Internal date and time of the message. This differs from the
/// ON criteria in SEARCH, which uses just the internal date.
Arrival,

/// IMAP addr-mailbox of the first "Cc" address.
Cc,

/// Sent date and time, as described in
/// [section 2.2](https://tools.ietf.org/html/rfc5256#section-2.2).
Date,

/// IMAP addr-mailbox of the first "From" address.
From,

/// Followed by another sort criterion, has the effect of that
/// criterion but in reverse (descending) order.
Reverse(&'c SortCriterion<'c>),

/// Size of the message in octets.
Size,

/// Base subject text.
Subject,

/// IMAP addr-mailbox of the first "To" address.
To,
}

impl<'c> fmt::Display for SortCriterion<'c> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SortCriterion::*;

match self {
Arrival => write!(f, "ARRIVAL"),
Cc => write!(f, "CC"),
Date => write!(f, "DATE"),
From => write!(f, "FROM"),
Reverse(c) => write!(f, "REVERSE {}", c),
Size => write!(f, "SIZE"),
Subject => write!(f, "SUBJECT"),
To => write!(f, "TO"),
}
}
}

/// The character encoding to use for strings that are subject to a [`SortCriterion`].
///
/// Servers are only required to implement [`SortCharset::UsAscii`] and [`SortCharset::Utf8`].
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SortCharset<'c> {
/// Strings are UTF-8 encoded.
Utf8,

/// Strings are encoded with ASCII.
UsAscii,

/// Strings are encoded using some other character set.
///
/// Note that this option is subject to server support for the specified character set.
Custom(Cow<'c, str>),
}

impl<'c> fmt::Display for SortCharset<'c> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SortCharset::*;

match self {
Utf8 => write!(f, "UTF-8"),
UsAscii => write!(f, "US-ASCII"),
Custom(c) => write!(f, "{}", c),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_criterion_to_string() {
use SortCriterion::*;

assert_eq!("ARRIVAL", Arrival.to_string());
assert_eq!("CC", Cc.to_string());
assert_eq!("DATE", Date.to_string());
assert_eq!("FROM", From.to_string());
assert_eq!("SIZE", Size.to_string());
assert_eq!("SUBJECT", Subject.to_string());
assert_eq!("TO", To.to_string());
assert_eq!("REVERSE TO", Reverse(&To).to_string());
assert_eq!("REVERSE REVERSE TO", Reverse(&Reverse(&To)).to_string());
}

#[test]
fn test_criteria_to_string() {
use SortCriterion::*;

assert_eq!("", SortCriteria(&[]).to_string());
assert_eq!("(ARRIVAL)", SortCriteria(&[Arrival]).to_string());
assert_eq!(
"(ARRIVAL REVERSE FROM)",
SortCriteria(&[Arrival, Reverse(&From)]).to_string()
);
assert_eq!(
"(ARRIVAL REVERSE REVERSE REVERSE FROM)",
SortCriteria(&[Arrival, Reverse(&Reverse(&Reverse(&From)))]).to_string()
);
}

#[test]
fn test_charset_to_string() {
use SortCharset::*;

assert_eq!("UTF-8", Utf8.to_string());
assert_eq!("US-ASCII", UsAscii.to_string());
assert_eq!("CHARSET", Custom("CHARSET".into()).to_string());
}
}
41 changes: 33 additions & 8 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,21 +315,25 @@ pub fn parse_mailbox(
}
}

pub fn parse_ids(
fn parse_ids_with<T: Extend<u32>>(
lines: &[u8],
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<HashSet<u32>> {
mut collection: T,
) -> Result<T> {
let mut lines = lines;
let mut ids = HashSet::new();
loop {
if lines.is_empty() {
break Ok(ids);
break Ok(collection);
}

match imap_proto::parser::parse_response(lines) {
Ok((rest, Response::MailboxData(MailboxDatum::Search(c)))) => {
lines = rest;
ids.extend(c);
collection.extend(c);
}
Ok((rest, Response::MailboxData(MailboxDatum::Sort(c)))) => {
lines = rest;
collection.extend(c);
}
Ok((rest, data)) => {
lines = rest;
Expand All @@ -344,6 +348,20 @@ pub fn parse_ids(
}
}

pub fn parse_id_set(
lines: &[u8],
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<HashSet<u32>> {
parse_ids_with(lines, unsolicited, HashSet::new())
}

pub fn parse_id_seq(
lines: &[u8],
unsolicited: &mut mpsc::Sender<UnsolicitedResponse>,
) -> Result<Vec<u32>> {
parse_ids_with(lines, unsolicited, Vec::new())
}

/// Parse a single unsolicited response from IDLE responses.
pub fn parse_idle(lines: &[u8]) -> (&[u8], Option<Result<UnsolicitedResponse>>) {
match imap_proto::parser::parse_response(lines) {
Expand Down Expand Up @@ -547,7 +565,7 @@ mod tests {
* 1 RECENT\r\n\
* STATUS INBOX (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n";
let (mut send, recv) = mpsc::channel();
let ids = parse_ids(lines, &mut send).unwrap();
let ids = parse_id_set(lines, &mut send).unwrap();

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

Expand All @@ -571,7 +589,7 @@ mod tests {
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\
* 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";
let (mut send, recv) = mpsc::channel();
let ids = parse_ids(lines, &mut send).unwrap();
let ids = parse_id_set(lines, &mut send).unwrap();
assert!(recv.try_recv().is_err());
let ids: HashSet<u32> = ids.iter().cloned().collect();
assert_eq!(
Expand All @@ -594,10 +612,17 @@ mod tests {

let lines = b"* SEARCH\r\n";
let (mut send, recv) = mpsc::channel();
let ids = parse_ids(lines, &mut send).unwrap();
let ids = parse_id_set(lines, &mut send).unwrap();
assert!(recv.try_recv().is_err());
let ids: HashSet<u32> = ids.iter().cloned().collect();
assert_eq!(ids, HashSet::<u32>::new());

let lines = b"* SORT\r\n";
let (mut send, recv) = mpsc::channel();
let ids = parse_id_seq(lines, &mut send).unwrap();
assert!(recv.try_recv().is_err());
let ids: Vec<u32> = ids.iter().cloned().collect();
assert_eq!(ids, Vec::<u32>::new());
}

#[test]
Expand Down
Loading