Skip to content

Commit

Permalink
Merge 790a72e into 09b1d1a
Browse files Browse the repository at this point in the history
  • Loading branch information
duesee committed Jan 16, 2024
2 parents 09b1d1a + 790a72e commit f7f1046
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 2 deletions.
1 change: 1 addition & 0 deletions imap-codec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ext_condstore_qresync = ["imap-types/ext_condstore_qresync"]
ext_login_referrals = ["imap-types/ext_login_referrals"]
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
ext_id = ["imap-types/ext_id"]
ext_sort_thread = ["imap-types/ext_sort_thread"]
# </Forward to imap-types>

# IMAP quirks
Expand Down
2 changes: 2 additions & 0 deletions imap-codec/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ext_condstore_qresync = ["imap-codec/ext_condstore_qresync"]
ext_login_referrals = ["imap-codec/ext_login_referrals"]
ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"]
ext_id = ["imap-codec/ext_id"]
ext_sort_thread = ["imap-codec/ext_sort_thread"]

# IMAP quirks
quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
Expand All @@ -31,6 +32,7 @@ ext = [
#"ext_login_referrals",
#"ext_mailbox_referrals",
"ext_id",
"ext_sort_thread",
]
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
debug = []
Expand Down
27 changes: 27 additions & 0 deletions imap-codec/src/codec/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,24 @@ impl<'a> EncodeIntoContext for CommandBody<'a> {
ctx.write_all(b" ")?;
criteria.encode_ctx(ctx)
}
#[cfg(feature = "ext_sort_thread")]
CommandBody::Sort {
sort_criteria,
charset,
search_criteria,
uid,
} => {
if *uid {
ctx.write_all(b"UID SORT (")?;
} else {
ctx.write_all(b"SORT (")?;
}
join_serializable(sort_criteria.as_ref(), b" ", ctx)?;
ctx.write_all(b") ")?;
charset.encode_ctx(ctx)?;
ctx.write_all(b" ")?;
search_criteria.encode_ctx(ctx)
}
CommandBody::Fetch {
sequence_set,
macro_or_item_names,
Expand Down Expand Up @@ -1215,6 +1233,15 @@ impl<'a> EncodeIntoContext for Data<'a> {
join_serializable(seqs, b" ", ctx)?;
}
}
#[cfg(feature = "ext_sort_thread")]
Data::Sort(seqs) => {
if seqs.is_empty() {
ctx.write_all(b"* SORT")?;
} else {
ctx.write_all(b"* SORT ")?;
join_serializable(seqs, b" ", ctx)?;
}
}
Data::Flags(flags) => {
ctx.write_all(b"* FLAGS (")?;
join_serializable(flags, b" ", ctx)?;
Expand Down
4 changes: 4 additions & 0 deletions imap-codec/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use nom::{

#[cfg(feature = "ext_id")]
use crate::extensions::id::id;
#[cfg(feature = "ext_sort_thread")]
use crate::extensions::sort::sort;
use crate::{
auth::auth_type,
core::{astring, base64, literal, tag_imap},
Expand Down Expand Up @@ -399,6 +401,8 @@ pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
store,
uid,
search,
#[cfg(feature = "ext_sort_thread")]
sort,
value(CommandBody::Unselect, tag_no_case(b"UNSELECT")),
r#move,
))(input)
Expand Down
2 changes: 2 additions & 0 deletions imap-codec/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ pub mod idle;
pub mod literal;
pub mod r#move;
pub mod quota;
#[cfg(feature = "ext_sort_thread")]
pub mod sort;
pub mod unselect;
99 changes: 99 additions & 0 deletions imap-codec/src/extensions/sort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::io::Write;

use abnf_core::streaming::sp;
use imap_types::{
command::CommandBody,
core::NonEmptyVec,
extensions::sort::{SortCriterion, SortKey},
};
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::{map, opt, value},
multi::separated_list1,
sequence::{delimited, tuple},
};

use crate::{
decode::IMAPResult,
encode::{EncodeContext, EncodeIntoContext},
search::search_criteria,
};

/// ```abnf
/// sort = ["UID" SP] "SORT" SP sort-criteria SP search-criteria
/// ```
pub(crate) fn sort(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
map(opt(tag_no_case("UID ")), |thing| thing.is_some()),
tag_no_case("SORT "),
sort_criteria,
sp,
search_criteria,
));

let (remaining, (uid, _, sort_criteria, _, (charset, search_key))) = parser(input)?;

Ok((
remaining,
CommandBody::Sort {
sort_criteria,
charset,
search_criteria: search_key,
uid,
},
))
}

/// ```abnf
/// sort-criteria = "(" sort-criterion *(SP sort-criterion) ")"
/// ```
pub(crate) fn sort_criteria(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec<SortCriterion>> {
delimited(
tag("("),
map(
separated_list1(sp, sort_criterion),
NonEmptyVec::unvalidated,
),
tag(")"),
)(input)
}

/// ```abnf
/// sort-criterion = ["REVERSE" SP] sort-key
/// ```
pub(crate) fn sort_criterion(input: &[u8]) -> IMAPResult<&[u8], SortCriterion> {
let mut parser = tuple((
map(opt(tag_no_case(b"REVERSE ")), |thing| thing.is_some()),
sort_key,
));

let (remaining, (reverse, key)) = parser(input)?;

Ok((remaining, SortCriterion { reverse, key }))
}

/// ```abnf
/// sort-key = "ARRIVAL" / "CC" / "DATE" / "FROM" / "SIZE" / "SUBJECT" / "TO"
/// ```
pub(crate) fn sort_key(input: &[u8]) -> IMAPResult<&[u8], SortKey> {
alt((
value(SortKey::Arrival, tag_no_case("ARRIVAL")),
value(SortKey::Cc, tag_no_case("CC")),
value(SortKey::Date, tag_no_case("DATE")),
value(SortKey::From, tag_no_case("FROM")),
value(SortKey::Size, tag_no_case("SIZE")),
value(SortKey::Subject, tag_no_case("SUBJECT")),
value(SortKey::To, tag_no_case("TO")),
))(input)
}

impl EncodeIntoContext for SortCriterion {
fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> {
if self.reverse {
ctx.write_all(b"REVERSE ")?;
}

ctx.write_all(self.key.as_ref().as_bytes())
}
}
5 changes: 5 additions & 0 deletions imap-codec/src/mailbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))),
|(_, nums)| Data::Search(nums),
),
#[cfg(feature = "ext_sort_thread")]
map(
preceded(tag_no_case(b"SORT"), many0(preceded(sp, nz_number))),
Data::Sort,
),
map(
tuple((
tag_no_case(b"STATUS"),
Expand Down
23 changes: 23 additions & 0 deletions imap-codec/src/search.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use abnf_core::streaming::sp;
#[cfg(feature = "ext_sort_thread")]
use imap_types::core::Charset;
use imap_types::{command::CommandBody, core::NonEmptyVec, search::SearchKey};
#[cfg(feature = "ext_sort_thread")]
use nom::sequence::pair;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
Expand Down Expand Up @@ -221,6 +225,25 @@ fn search_key_limited<'a>(
))(input)
}

// Used by both, SORT and THREAD.
#[cfg(feature = "ext_sort_thread")]
/// ```abnf
/// search-criteria = charset 1*(SP search-key)
/// ```
pub(crate) fn search_criteria(input: &[u8]) -> IMAPResult<&[u8], (Charset, SearchKey)> {
let mut parser = pair(charset, many1(preceded(sp, search_key(9))));

let (remaining, (charset, mut search_key)) = parser(input)?;

let search_key = match search_key.len() {
0 => unreachable!(),
1 => search_key.pop().unwrap(),
_ => SearchKey::And(NonEmptyVec::unvalidated(search_key)),
};

Ok((remaining, (charset, search_key)))
}

#[cfg(test)]
mod tests {
use imap_types::{
Expand Down
16 changes: 16 additions & 0 deletions imap-codec/tests/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1403,3 +1403,19 @@ fn test_trace_rfc2088() {
.unwrap()
})
}

#[cfg(feature = "ext_sort_thread")]
#[test]
fn test_trace_sort() {
let trace = br#"C: A282 SORT (SUBJECT) UTF-8 SINCE 1-Feb-1994
S: * SORT 2 84 882
S: A282 OK SORT completed
C: A283 SORT (SUBJECT REVERSE DATE) UTF-8 ALL
S: * SORT 5 3 4 1 2
S: A283 OK SORT completed
C: A284 SORT (SUBJECT) US-ASCII TEXT "not in mailbox"
S: * SORT
S: A284 OK SORT completed"#;

test_lines_of_trace(trace);
}
1 change: 1 addition & 0 deletions imap-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ext_condstore_qresync = []
ext_login_referrals = []
ext_mailbox_referrals = []
ext_id = []
ext_sort_thread = []

# Unlock `unvalidated` constructors.
unvalidated = []
Expand Down
8 changes: 6 additions & 2 deletions imap-types/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ license = "MIT OR Apache-2.0"
cargo-fuzz = true

[features]
# <Forward to imap-codec>
# <Forward to imap-types>
# IMAP
starttls = ["imap-types/starttls"]

# IMAP Extensions
ext_condstore_qresync = ["imap-types/ext_condstore_qresync"]
ext_login_referrals = ["imap-types/ext_login_referrals"]
ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"]
# </Forward to imap-codec>
ext_id = ["imap-types/ext_id"]
ext_sort_thread = ["imap-types/ext_sort_thread"]
# </Forward to imap-types>

# Use (most) IMAP extensions.
ext = [
"starttls",
"ext_condstore_qresync",
#"ext_login_referrals",
#"ext_mailbox_referrals",
"ext_id",
"ext_sort_thread",
]
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
debug = []
Expand Down
27 changes: 27 additions & 0 deletions imap-types/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use serde::{Deserialize, Serialize};

#[cfg(feature = "ext_id")]
use crate::core::{IString, NString};
#[cfg(feature = "ext_sort_thread")]
use crate::extensions::sort::SortCriterion;
use crate::{
auth::AuthMechanism,
command::error::{AppendError, CopyError, ListError, LoginError, RenameError},
Expand Down Expand Up @@ -1018,6 +1020,29 @@ pub enum CommandBody<'a> {
uid: bool,
},

#[cfg(feature = "ext_sort_thread")]
/// SORT command.
///
/// The SORT command is a variant of SEARCH with sorting semantics for the results.
///
/// Data:
/// * untagged responses: SORT
///
/// Result:
/// * OK - sort completed
/// * NO - sort error: can't sort that charset or criteria
/// * BAD - command unknown or arguments invalid
Sort {
/// Sort criteria.
sort_criteria: NonEmptyVec<SortCriterion>,
/// Charset.
charset: Charset<'a>,
/// Search criteria.
search_criteria: SearchKey<'a>,
/// Use UID variant.
uid: bool,
},

/// ### 6.4.5. FETCH Command
///
/// * Arguments:
Expand Down Expand Up @@ -1629,6 +1654,8 @@ impl<'a> CommandBody<'a> {
Self::Authenticate { .. } => "AUTHENTICATE",
Self::Login { .. } => "LOGIN",
Self::Select { .. } => "SELECT",
#[cfg(feature = "ext_sort_thread")]
Self::Sort { .. } => "SORT",
Self::Unselect => "UNSELECT",
Self::Examine { .. } => "EXAMINE",
Self::Create { .. } => "CREATE",
Expand Down
2 changes: 2 additions & 0 deletions imap-types/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ pub mod enable;
pub mod idle;
pub mod r#move;
pub mod quota;
#[cfg(feature = "ext_sort_thread")]
pub mod sort;
pub mod unselect;
43 changes: 43 additions & 0 deletions imap-types/src/extensions/sort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
#[cfg(feature = "bounded-static")]
use bounded_static::ToStatic;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SortCriterion {
pub reverse: bool,
pub key: SortKey,
}

#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SortKey {
Arrival,
Cc,
Date,
From,
Size,
Subject,
To,
}

impl AsRef<str> for SortKey {
fn as_ref(&self) -> &str {
match self {
SortKey::Arrival => "ARRIVAL",
SortKey::Cc => "CC",
SortKey::Date => "DATE",
SortKey::From => "FROM",
SortKey::Size => "SIZE",
SortKey::Subject => "SUBJECT",
SortKey::To => "TO",
}
}
}
Loading

0 comments on commit f7f1046

Please sign in to comment.