Skip to content
Open
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
69 changes: 67 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,37 @@ impl<T: Read + Write> Session<T> {
.and_then(|lines| QuotaRootResponse::parse(lines, &mut self.unsolicited_responses))
}

/// The [`ID` command](https://datatracker.ietf.org/doc/html/rfc2971)
pub fn id(&mut self, fields: &[(&str, Option<&str>)]) -> Result<IdResponse> {
let mut cmd = String::from("ID ");

if fields.is_empty() {
cmd.push_str("NIL");
} else {
cmd.push('(');
for (i, (k, v)) in fields.iter().enumerate() {
if i > 0 {
cmd.push(' ');
}

cmd.push_str(&quote!(k));
cmd.push_str(" ");
match v {
Some(val) => {
let quoted = validate_str("ID", format!("field #{}", i + 1), val)?;
cmd.push_str(&quoted);
}
None => cmd.push_str("NIL"),
}
}
cmd.push(')');
}

let response = self.run_command_and_read_response(cmd)?;
let response = IdResponse::parse(&response);
Ok(response)
}

// these are only here because they are public interface, the rest is in `Connection`
/// Runs a command and checks if it returns OK.
pub fn run_command_and_check_ok(&mut self, command: impl AsRef<str>) -> Result<()> {
Expand Down Expand Up @@ -1742,12 +1773,11 @@ pub(crate) mod testutils {
#[cfg(test)]
mod tests {
use super::super::mock_stream::MockStream;
use super::testutils::*;
use super::*;
use imap_proto::types::Capability;
use std::borrow::Cow;

use super::testutils::*;

macro_rules! mock_session {
($s:expr) => {
Session::new(Client::new($s).conn)
Expand Down Expand Up @@ -3113,4 +3143,39 @@ a1 OK completed\r
}
panic!("No error");
}

#[test]
fn id_command() {
let response = "* ID (\"name\" NIL \"version\" \"1.0\")\r\n\
a1 OK ID command completed\r\n"
.as_bytes()
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);

let response = session
.id(&[
("version", Some("1.0.0")),
("vendor", None),
("client", Some(r#"my "client""#)),
])
.unwrap();

assert_eq!(response.len(), 2);
assert_eq!(response.get(b"name"), None);
assert_eq!(response.get(b"version"), Some(String::from("1.0")));
}

#[test]
fn id_command_nil_response() {
let response = "* ID NIL\r\n\
a1 OK ID command completed\r\n"
.as_bytes()
.to_vec();
let mock_stream = MockStream::new(response);
let mut session = mock_session!(mock_stream);

let response = session.id(&[]).unwrap();
assert_eq!(response.len(), 0);
}
}
90 changes: 90 additions & 0 deletions src/types/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case, take_until};
use nom::character::complete::{char, multispace0, multispace1};
use nom::combinator::{map, value};
use nom::multi::separated_list0;
use nom::sequence::{delimited, preceded};
use nom::IResult;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};

/// From [ID Response](https://datatracker.ietf.org/doc/html/rfc2971#section-3.2)
///
/// Used by [`Session::id`](crate::Session::id)
#[derive(Debug, Clone)]
pub struct IdResponse {
/// Fields of the response
pub fields: HashMap<Vec<u8>, Option<Vec<u8>>>,
}

impl IdResponse {
/// Parse from the server raw response
pub fn parse(data: &[u8]) -> Self {
let mut parser = preceded(
alt((tag("* ID "), tag_no_case("* ID "))),
alt((
value(HashMap::new(), tag_no_case("NIL")), /* The whole list is a NIL */
delimited(
char('('),
map(separated_list0(multispace1, pair_parser), |vec| {
vec.into_iter().collect::<HashMap<_, _>>()
}),
char(')'),
),
)),
);

match parser(data) {
Ok((_, fields)) => Self { fields },
Err(_) => Self {
fields: HashMap::new(),
},
}
}

/// Get field as UTF-8
pub fn get(&self, key: &[u8]) -> Option<String> {
self.fields
.get(key)
.and_then(|x| x.as_ref().map(|x| String::from_utf8_lossy(x).into_owned()))
}

/// Field length
pub fn len(&self) -> usize {
self.fields.len()
}
}

/// Parse quoted string
fn quoted_string(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
map(
delimited(char('"'), take_until("\""), char('"')),
|s: &[u8]| s.to_vec(),
)(input)
}

/// Parse 'nstring': "string" or NIL
fn nstring(input: &[u8]) -> IResult<&[u8], Option<Vec<u8>>> {
alt((map(quoted_string, Some), value(None, tag_no_case("NIL"))))(input)
}

/// Parse key-value pair: "name" "value"
fn pair_parser(input: &[u8]) -> IResult<&[u8], (Vec<u8>, Option<Vec<u8>>)> {
let (input, key) = preceded(multispace0, quoted_string)(input)?;
let (input, val) = preceded(multispace1, nstring)(input)?;
Ok((input, (key, val)))
}

impl Display for IdResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for (k, v) in &self.fields {
let key_str = String::from_utf8_lossy(k);
let val_str = v
.as_ref()
.map(|b| format!("\"{}\"", String::from_utf8_lossy(b)))
.unwrap_or_else(|| "NIL".to_string());
write!(f, "{}={}; ", key_str, val_str)?;
}
Ok(())
}
}
3 changes: 3 additions & 0 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ pub use self::acls::*;
mod quota;
pub use self::quota::*;

mod id;
pub use self::id::*;

mod unsolicited_response;
pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse};

Expand Down