Skip to content

Commit

Permalink
feat: Implement ID (RFC 2971)
Browse files Browse the repository at this point in the history
  • Loading branch information
duesee committed Dec 4, 2023
1 parent 5da4c1a commit b84dcca
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 4 deletions.
1 change: 1 addition & 0 deletions imap-codec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ starttls = ["imap-types/starttls"]
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"]
# </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 @@ -18,6 +18,7 @@ starttls = ["imap-codec/starttls"]
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"]

# IMAP quirks
quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
Expand All @@ -29,6 +30,7 @@ ext = [
"ext_condstore_qresync",
#"ext_login_referrals",
#"ext_mailbox_referrals",
"ext_id",
]
# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
debug = []
Expand Down
40 changes: 40 additions & 0 deletions imap-codec/src/codec/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,26 @@ impl<'a> EncodeIntoContext for CommandBody<'a> {
ctx.write_all(b" ")?;
mailbox.encode_ctx(ctx)
}
#[cfg(feature = "ext_id")]
CommandBody::Id { parameters } => {
if let Some((first, tail)) = parameters.split_first() {
ctx.write_all(b"ID (")?;
first.0.encode_ctx(ctx)?;
ctx.write_all(b" ")?;
first.1.encode_ctx(ctx)?;

for parameter in tail {
ctx.write_all(b" ")?;
parameter.0.encode_ctx(ctx)?;
ctx.write_all(b" ")?;
parameter.1.encode_ctx(ctx)?;
}

ctx.write_all(b")")
} else {
ctx.write_all(b"ID NIL")
}
}
}
}
}
Expand Down Expand Up @@ -1201,6 +1221,26 @@ impl<'a> EncodeIntoContext for Data<'a> {
root.encode_ctx(ctx)?;
}
}
#[cfg(feature = "ext_id")]
Data::Id { parameters } => {
if let Some((first, tail)) = parameters.split_first() {
ctx.write_all(b"* ID (")?;
first.0.encode_ctx(ctx)?;
ctx.write_all(b" ")?;
first.1.encode_ctx(ctx)?;

for parameter in tail {
ctx.write_all(b" ")?;
parameter.0.encode_ctx(ctx)?;
ctx.write_all(b" ")?;
parameter.1.encode_ctx(ctx)?;
}

ctx.write_all(b")")?;
} else {
ctx.write_all(b"* ID NIL")?;
}
}
}

ctx.write_all(b"\r\n")
Expand Down
12 changes: 11 additions & 1 deletion imap-codec/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use nom::{
sequence::{delimited, preceded, terminated, tuple},
};

#[cfg(feature = "ext_id")]
use crate::extensions::id::id;
use crate::{
auth::auth_type,
core::{astring, base64, literal, tag_imap},
Expand Down Expand Up @@ -79,7 +81,13 @@ pub(crate) fn command(input: &[u8]) -> IMAPResult<&[u8], Command> {

// # Command Any

/// `command-any = "CAPABILITY" / "LOGOUT" / "NOOP" / x-command`
/// ```abnf
/// command-any = "CAPABILITY" /
/// "LOGOUT" /
/// "NOOP" /
/// x-command /
/// id ; adds id command to command_any (See RFC 2971)
/// ```
///
/// Note: Valid in all states
pub(crate) fn command_any(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
Expand All @@ -88,6 +96,8 @@ pub(crate) fn command_any(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
value(CommandBody::Logout, tag_no_case(b"LOGOUT")),
value(CommandBody::Noop, tag_no_case(b"NOOP")),
// x-command = "X" atom <experimental command arguments>
#[cfg(feature = "ext_id")]
map(id, |parameters| CommandBody::Id { parameters }),
))(input)
}

Expand Down
2 changes: 2 additions & 0 deletions imap-codec/src/extensions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod compress;
pub mod enable;
#[cfg(feature = "ext_id")]
pub mod id;
pub mod idle;
pub mod literal;
pub mod r#move;
Expand Down
135 changes: 135 additions & 0 deletions imap-codec/src/extensions/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! IMAP4 ID extension
// Additional changes:
//
// command_any ::= "CAPABILITY" / "LOGOUT" / "NOOP" / x_command / id
// response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye / mailbox_data / message_data / capability_data / id_response)

use abnf_core::streaming::sp;
use imap_types::core::{IString, NString};
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::value,
multi::separated_list0,
sequence::{delimited, preceded, separated_pair},
};

use crate::{
core::{nil, nstring, string},
decode::IMAPResult,
};

/// ```abnf_old
/// id ::= "ID" SPACE id_params_list
/// ```
pub(crate) fn id(input: &[u8]) -> IMAPResult<&[u8], Vec<(IString, NString)>> {
preceded(tag_no_case("ID "), id_params_list)(input)
}

/// ```abnf_olf
/// id_response ::= "ID" SPACE id_params_list
/// ```
#[inline]
pub(crate) fn id_response(input: &[u8]) -> IMAPResult<&[u8], Vec<(IString, NString)>> {
id(input)
}

/// ```abnf_old
/// id_params_list = "(" #(string SPACE nstring) ")" / nil
/// ```
///
/// Note: The ABNF above is non-standard. According to the examples, `#` likely means "separated by whitespace".
/// I'm not sure if `#` means `*` or `1*`, though. However, we can bypass this uncertainty by accepting both.
pub(crate) fn id_params_list(input: &[u8]) -> IMAPResult<&[u8], Vec<(IString, NString)>> {
alt((
delimited(
tag("("),
separated_list0(sp, separated_pair(string, sp, nstring)),
tag(")"),
),
value(vec![], nil),
))(input)
}

#[cfg(test)]
mod tests {
use imap_types::{
command::{Command, CommandBody},
core::{IString, NString},
response::{Data, Response},
};

use super::*;
use crate::testing::{kat_inverse_command, kat_inverse_response};

#[test]
fn test_parse_id() {
let got = id(b"id (\"name\" \"imap-codec\")\r\n").unwrap().1;
assert_eq!(
vec![(
IString::try_from("name").unwrap(),
NString::try_from("imap-codec").unwrap()
)],
got
);
}

#[test]
fn test_kat_inverse_command_id() {
kat_inverse_command(&[
(
b"A ID nil\r\n".as_ref(),
b"".as_ref(),
Command::new("A", CommandBody::Id { parameters: vec![] }).unwrap(),
),
(
b"A ID NIL\r\n".as_ref(),
b"".as_ref(),
Command::new("A", CommandBody::Id { parameters: vec![] }).unwrap(),
),
(
b"A ID ()\r\n".as_ref(),
b"".as_ref(),
Command::new("A", CommandBody::Id { parameters: vec![] }).unwrap(),
),
(
b"A ID (\"\" \"\")\r\n".as_ref(),
b"".as_ref(),
Command::new(
"A",
CommandBody::Id {
parameters: vec![(
IString::try_from("").unwrap(),
NString::try_from("").unwrap(),
)],
},
)
.unwrap(),
),
(
b"A ID (\"name\" \"imap-codec\")\r\n".as_ref(),
b"".as_ref(),
Command::new(
"A",
CommandBody::Id {
parameters: vec![(
IString::try_from("name").unwrap(),
NString::try_from("imap-codec").unwrap(),
)],
},
)
.unwrap(),
),
]);
}

#[test]
fn test_kat_inverse_response_id() {
kat_inverse_response(&[(
b"* ID nil\r\n".as_ref(),
b"".as_ref(),
Response::Data(Data::Id { parameters: vec![] }),
)]);
}
}
15 changes: 12 additions & 3 deletions imap-codec/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use nom::{
sequence::{delimited, preceded, terminated, tuple},
};

#[cfg(feature = "ext_id")]
use crate::extensions::id::id_response;
use crate::{
core::{atom, charset, nz_number, tag_imap, text},
decode::IMAPResult,
Expand Down Expand Up @@ -274,13 +276,16 @@ pub(crate) fn continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuatio
Ok((remaining, continue_request))
}

/// `response-data = "*" SP (
/// ```abnf
/// response-data = "*" SP (
/// resp-cond-state /
/// resp-cond-bye /
/// mailbox-data /
/// message-data /
/// capability-data
/// ) CRLF`
/// capability-data /
/// id_response ; (See RFC 2971)
/// ) CRLF
/// ```
pub(crate) fn response_data(input: &[u8]) -> IMAPResult<&[u8], Response> {
let mut parser = tuple((
tag(b"*"),
Expand Down Expand Up @@ -317,6 +322,10 @@ pub(crate) fn response_data(input: &[u8]) -> IMAPResult<&[u8], Response> {
Response::Data(Data::Capability(caps))
}),
map(enable_data, Response::Data),
#[cfg(feature = "ext_id")]
map(id_response, |parameters| {
Response::Data(Data::Id { parameters })
}),
)),
crlf,
));
Expand Down
1 change: 1 addition & 0 deletions imap-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ starttls = []
ext_condstore_qresync = []
ext_login_referrals = []
ext_mailbox_referrals = []
ext_id = []

# Unlock `unvalidated` constructors.
unvalidated = []
Expand Down
11 changes: 11 additions & 0 deletions imap-types/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use bounded_static::ToStatic;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "ext_id")]
use crate::core::{IString, NString};
use crate::{
auth::AuthMechanism,
command::error::{AppendError, CopyError, ListError, LoginError, RenameError},
Expand Down Expand Up @@ -1362,6 +1364,13 @@ pub enum CommandBody<'a> {
/// Use UID variant.
uid: bool,
},

#[cfg(feature = "ext_id")]
/// ID command.
Id {
/// Parameters.
parameters: Vec<(IString<'a>, NString<'a>)>,
},
}

impl<'a> CommandBody<'a> {
Expand Down Expand Up @@ -1645,6 +1654,8 @@ impl<'a> CommandBody<'a> {
Self::GetQuotaRoot { .. } => "GETQUOTAROOT",
Self::SetQuota { .. } => "SETQUOTA",
Self::Move { .. } => "MOVE",
#[cfg(feature = "ext_id")]
Self::Id { .. } => "ID",
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions imap-types/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use bounded_static::ToStatic;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "ext_id")]
use crate::core::{IString, NString};
use crate::{
auth::AuthMechanism,
core::{impl_try_from, AString, Atom, Charset, NonEmptyVec, QuotedChar, Tag, Text},
Expand Down Expand Up @@ -550,6 +552,13 @@ pub enum Data<'a> {
/// List of quota roots.
roots: Vec<AString<'a>>,
},

#[cfg(feature = "ext_id")]
/// ID Response
Id {
/// Parameters
parameters: Vec<(IString<'a>, NString<'a>)>,
},
}

impl<'a> Data<'a> {
Expand Down

0 comments on commit b84dcca

Please sign in to comment.