diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 1a6ea55..d58fa6c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -16,7 +16,6 @@ use std::{borrow::Cow, str::from_utf8}; pub mod address; pub mod command; pub mod imf; -pub mod replies; pub mod response; pub mod trace; pub mod utils; diff --git a/src/parse/replies.rs b/src/parse/replies.rs deleted file mode 100644 index f1eef41..0000000 --- a/src/parse/replies.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! 4.2. SMTP Replies (RFC 5321) - -use crate::{ - parse::{address::address_literal, Domain}, - types::Greeting as GreetingType, -}; -use abnf_core::streaming::{CRLF, SP}; -use nom::{ - branch::alt, - bytes::streaming::{tag, take_while1, take_while_m_n}, - combinator::{map, map_res, opt, recognize}, - multi::many0, - sequence::{delimited, preceded, tuple}, - IResult, -}; - -/// Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF ) / -/// ( "220-" (Domain / address-literal) [ SP textstring ] CRLF -/// *( "220-" [ textstring ] CRLF ) -/// "220" [ SP textstring ] CRLF ) -pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> { - let mut parser = alt(( - map( - tuple(( - tag(b"220 "), - alt((Domain, address_literal)), - opt(preceded(SP, textstring)), - CRLF, - )), - |(_, domain, maybe_text, _)| GreetingType { - domain: domain.to_owned(), - text: maybe_text - .map(|str| str.to_string()) - .unwrap_or_else(|| "".to_string()), - }, - ), - map( - tuple(( - tag(b"220-"), - alt((Domain, address_literal)), - opt(preceded(SP, textstring)), - CRLF, - many0(delimited(tag(b"220-"), opt(textstring), CRLF)), - tag(b"220"), - opt(preceded(SP, textstring)), - CRLF, - )), - |(_, domain, maybe_text, _, more_text, _, moar_text, _)| GreetingType { - domain: domain.to_owned(), - text: { - let mut res = maybe_text - .map(|str| format!("{}\n", str)) - .unwrap_or_else(|| "\n".to_string()); - - for text in more_text { - let text = text - .map(|str| format!("{}\n", str)) - .unwrap_or_else(|| "\n".to_string()); - res.push_str(&text); - } - - let text = moar_text - .map(|str| str.to_string()) - .unwrap_or_else(|| "".to_string()); - res.push_str(&text); - - res - }, - }, - ), - )); - - let (remaining, parsed) = parser(input)?; - - Ok((remaining, parsed)) -} - -/// HT, SP, Printable US-ASCII -/// -/// textstring = 1*(%d09 / %d32-126) -pub fn textstring(input: &[u8]) -> IResult<&[u8], &str> { - fn is_value(byte: u8) -> bool { - matches!(byte, 9 | 32..=126) - } - - let (remaining, parsed) = map_res(take_while1(is_value), std::str::from_utf8)(input)?; - - Ok((remaining, parsed)) -} - -/// Reply-line = *( Reply-code "-" [ textstring ] CRLF ) -/// Reply-code [ SP textstring ] CRLF -pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> { - let parser = tuple(( - many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))), - Reply_code, - opt(tuple((SP, textstring))), - CRLF, - )); - - let (remaining, parsed) = recognize(parser)(input)?; - - Ok((remaining, parsed)) -} - -/// Reply-code = %x32-35 %x30-35 %x30-39 -/// -/// 2345 -/// 012345 -/// 0123456789 -pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> { - // FIXME: do not accept all codes. - map_res( - map_res( - take_while_m_n(3, 3, nom::character::is_digit), - std::str::from_utf8, - ), - |s| u16::from_str_radix(s, 10), - )(input) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_Greeting() { - let greeting = b"220-example.org ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\r\n\ -220-We do not authorize the use of this system to transport unsolicited,\r\n\ -220 and/or bulk e-mail.\r\n"; - - let (rem, out) = Greeting(greeting).unwrap(); - assert_eq!(rem, b""); - assert_eq!( - out, - GreetingType { - domain: "example.org".into(), - text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\ -We do not authorize the use of this system to transport unsolicited,\n\ -and/or bulk e-mail." - .into(), - } - ) - } -} diff --git a/src/parse/response.rs b/src/parse/response.rs index 26bbd97..b3938ca 100644 --- a/src/parse/response.rs +++ b/src/parse/response.rs @@ -1,6 +1,6 @@ use crate::{ - parse::{number, Domain}, - types::{AuthMechanism, Capability, EhloOkResp}, + parse::{address::address_literal, number, Domain}, + types::{AuthMechanism, Capability, EhloOkResp, Greeting as GreetingType}, }; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP}; use nom::{ @@ -12,6 +12,82 @@ use nom::{ IResult, }; +/// Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF ) / +/// ( "220-" (Domain / address-literal) [ SP textstring ] CRLF +/// *( "220-" [ textstring ] CRLF ) +/// "220" [ SP textstring ] CRLF ) +pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> { + let mut parser = alt(( + map( + tuple(( + tag(b"220 "), + alt((Domain, address_literal)), + opt(preceded(SP, textstring)), + CRLF, + )), + |(_, domain, maybe_text, _)| GreetingType { + domain: domain.to_owned(), + text: maybe_text + .map(|str| str.to_string()) + .unwrap_or_else(|| "".to_string()), + }, + ), + map( + tuple(( + tag(b"220-"), + alt((Domain, address_literal)), + opt(preceded(SP, textstring)), + CRLF, + many0(delimited(tag(b"220-"), opt(textstring), CRLF)), + tag(b"220"), + opt(preceded(SP, textstring)), + CRLF, + )), + |(_, domain, maybe_text, _, more_text, _, moar_text, _)| GreetingType { + domain: domain.to_owned(), + text: { + let mut res = maybe_text + .map(|str| format!("{}\n", str)) + .unwrap_or_else(|| "\n".to_string()); + + for text in more_text { + let text = text + .map(|str| format!("{}\n", str)) + .unwrap_or_else(|| "\n".to_string()); + res.push_str(&text); + } + + let text = moar_text + .map(|str| str.to_string()) + .unwrap_or_else(|| "".to_string()); + res.push_str(&text); + + res + }, + }, + ), + )); + + let (remaining, parsed) = parser(input)?; + + Ok((remaining, parsed)) +} + +/// HT, SP, Printable US-ASCII +/// +/// textstring = 1*(%d09 / %d32-126) +pub fn textstring(input: &[u8]) -> IResult<&[u8], &str> { + fn is_value(byte: u8) -> bool { + matches!(byte, 9 | 32..=126) + } + + let (remaining, parsed) = map_res(take_while1(is_value), std::str::from_utf8)(input)?; + + Ok((remaining, parsed)) +} + +// ------------------------------------------------------------------------------------------------- + /// ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF ) / /// ( "250-" Domain [ SP ehlo-greet ] CRLF /// *( "250-" ehlo-line CRLF ) @@ -122,20 +198,6 @@ pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], Capability> { ))(input) } -pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> { - alt(( - value(AuthMechanism::Login, tag_no_case("LOGIN")), - value(AuthMechanism::Plain, tag_no_case("PLAIN")), - value(AuthMechanism::CramMD5, tag_no_case("CRAM-MD5")), - value(AuthMechanism::CramSHA1, tag_no_case("CRAM-SHA1")), - value(AuthMechanism::DigestMD5, tag_no_case("DIGEST-MD5")), - value(AuthMechanism::ScramMD5, tag_no_case("SCRAM-MD5")), - value(AuthMechanism::GSSAPI, tag_no_case("GSSAPI")), - value(AuthMechanism::NTLM, tag_no_case("NTLM")), - map(ehlo_param, |param| AuthMechanism::Other(param.to_string())), - ))(input) -} - /// Additional syntax of ehlo-params depends on ehlo-keyword /// /// ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-") @@ -162,11 +224,78 @@ pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> { map_res(take_while1(is_valid_character), std::str::from_utf8)(input) } +pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> { + alt(( + value(AuthMechanism::Login, tag_no_case("LOGIN")), + value(AuthMechanism::Plain, tag_no_case("PLAIN")), + value(AuthMechanism::CramMD5, tag_no_case("CRAM-MD5")), + value(AuthMechanism::CramSHA1, tag_no_case("CRAM-SHA1")), + value(AuthMechanism::DigestMD5, tag_no_case("DIGEST-MD5")), + value(AuthMechanism::ScramMD5, tag_no_case("SCRAM-MD5")), + value(AuthMechanism::GSSAPI, tag_no_case("GSSAPI")), + value(AuthMechanism::NTLM, tag_no_case("NTLM")), + map(ehlo_param, |param| AuthMechanism::Other(param.to_string())), + ))(input) +} + +// ------------------------------------------------------------------------------------------------- + +/// Reply-line = *( Reply-code "-" [ textstring ] CRLF ) +/// Reply-code [ SP textstring ] CRLF +pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> { + let parser = tuple(( + many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))), + Reply_code, + opt(tuple((SP, textstring))), + CRLF, + )); + + let (remaining, parsed) = recognize(parser)(input)?; + + Ok((remaining, parsed)) +} + +/// Reply-code = %x32-35 %x30-35 %x30-39 +/// +/// 2345 +/// 012345 +/// 0123456789 +pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> { + // FIXME: do not accept all codes. + map_res( + map_res( + take_while_m_n(3, 3, nom::character::is_digit), + std::str::from_utf8, + ), + |s| u16::from_str_radix(s, 10), + )(input) +} + #[cfg(test)] mod test { use super::*; use crate::types::AuthMechanism; + #[test] + fn test_Greeting() { + let greeting = b"220-example.org ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\r\n\ +220-We do not authorize the use of this system to transport unsolicited,\r\n\ +220 and/or bulk e-mail.\r\n"; + + let (rem, out) = Greeting(greeting).unwrap(); + assert_eq!(rem, b""); + assert_eq!( + out, + GreetingType { + domain: "example.org".into(), + text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\ +We do not authorize the use of this system to transport unsolicited,\n\ +and/or bulk e-mail." + .into(), + } + ) + } + #[test] fn test_ehlo_ok_rsp() { let (rem, out) = ehlo_ok_rsp(