diff --git a/src/parse/command.rs b/src/parse/command.rs index 907e801..8dbb85f 100644 --- a/src/parse/command.rs +++ b/src/parse/command.rs @@ -10,7 +10,7 @@ use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, DQUOTE, SP}; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n}, - combinator::{opt, recognize}, + combinator::{map_res, opt, recognize}, multi::many0, sequence::{delimited, preceded, tuple}, IResult, @@ -348,10 +348,10 @@ pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> { } /// Domain = sub-domain *("." sub-domain) -pub fn Domain(input: &[u8]) -> IResult<&[u8], &[u8]> { +pub fn Domain(input: &[u8]) -> IResult<&[u8], &str> { let parser = tuple((sub_domain, many0(tuple((tag(b"."), sub_domain))))); - let (remaining, parsed) = recognize(parser)(input)?; + let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?; Ok((remaining, parsed)) } @@ -389,18 +389,21 @@ pub fn Ldh_str(input: &[u8]) -> IResult<&[u8], &[u8]> { /// General-address-literal /// ) "]" /// ; See Section 4.1.3 -pub fn address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> { +pub fn address_literal(input: &[u8]) -> IResult<&[u8], &str> { let parser = delimited( tag(b"["), - alt(( - IPv4_address_literal, - IPv6_address_literal, - General_address_literal, - )), + map_res( + alt(( + IPv4_address_literal, + IPv6_address_literal, + General_address_literal, + )), + std::str::from_utf8, + ), tag(b"]"), ); - let (remaining, parsed) = recognize(parser)(input)?; + let (remaining, parsed) = parser(input)?; Ok((remaining, parsed)) } @@ -493,3 +496,43 @@ pub fn String(input: &[u8]) -> IResult<&[u8], &[u8]> { Ok((remaining, parsed)) } + +#[cfg(test)] +mod test { + use super::{ehlo, helo, mail, sub_domain}; + use crate::types::Command; + + #[test] + fn test_subdomain() { + let (rem, parsed) = sub_domain(b"example???").unwrap(); + assert_eq!(parsed, b"example"); + assert_eq!(rem, b"???"); + } + + #[test] + fn test_ehlo() { + let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap(); + assert_eq!(parsed, Command::Ehlo(b"123.123.123.123".to_vec())); + assert_eq!(rem, b"???"); + } + + #[test] + fn test_helo() { + let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap(); + assert_eq!(parsed, Command::Helo(b"example.com".to_vec())); + assert_eq!(rem, b"???"); + } + + #[test] + fn test_mail() { + let (rem, parsed) = mail(b"MAIL FROM:\r\n???").unwrap(); + assert_eq!( + parsed, + Command::Mail { + data: b"".to_vec(), + params: None + } + ); + assert_eq!(rem, b"???"); + } +} diff --git a/src/parse/response.rs b/src/parse/response.rs index 7f32e5e..f30512f 100644 --- a/src/parse/response.rs +++ b/src/parse/response.rs @@ -1,11 +1,12 @@ -use crate::parse::command::Domain; +use crate::{parse::command::Domain, types::EhloOkResp}; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP}; +use nom::multi::separated_list; use nom::{ branch::alt, bytes::streaming::{tag, take_while, take_while1, take_while_m_n}, - combinator::{opt, recognize}, + combinator::{map, map_res, opt, recognize}, multi::many0, - sequence::tuple, + sequence::{delimited, preceded, tuple}, IResult, }; @@ -13,29 +14,56 @@ use nom::{ /// ( "250-" Domain [ SP ehlo-greet ] CRLF /// *( "250-" ehlo-line CRLF ) /// "250" SP ehlo-line CRLF ) -pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], &[u8]> { +pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> { let parser = alt(( - recognize(tuple(( - tag(b"250"), - SP, - Domain, - opt(tuple((SP, ehlo_greet))), - CRLF, - ))), - recognize(tuple(( - tag(b"250-"), - Domain, - opt(tuple((SP, ehlo_greet))), - CRLF, - many0(tuple((tag(b"250-"), ehlo_line, CRLF))), - tag(b"250"), - SP, - ehlo_line, - CRLF, - ))), + map( + tuple((tag(b"250"), SP, Domain, opt(preceded(SP, ehlo_greet)), CRLF)), + |(_, _, domain, maybe_ehlo, _)| EhloOkResp { + domain: domain.to_owned(), + greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), + lines: Vec::new(), + }, + ), + map( + tuple(( + tag(b"250-"), + Domain, + opt(preceded(SP, ehlo_greet)), + CRLF, + many0(delimited(tag(b"250-"), ehlo_line, CRLF)), + tag(b"250"), + SP, + ehlo_line, + CRLF, + )), + |(_, domain, maybe_ehlo, _, lines, _, _, (keyword, params), _)| EhloOkResp { + domain: domain.to_owned(), + greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), + lines: { + let mut lines = lines + .iter() + .map(|(keyword, params)| { + let params = params + .iter() + .map(|param| param.to_string()) + .collect::>(); + (keyword.to_string(), params) + }) + .collect::)>>(); + lines.push(( + keyword.to_string(), + params + .iter() + .map(|param| param.to_string()) + .collect::>(), + )); + lines + }, + }, + ), )); - let (remaining, parsed) = recognize(parser)(input)?; + let (remaining, parsed) = parser(input)?; Ok((remaining, parsed)) } @@ -43,7 +71,7 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], &[u8]> { /// String of any characters other than CR or LF. /// /// ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) -pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &[u8]> { +pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> { fn is_valid_character(byte: u8) -> bool { match byte { 0..=9 | 11..=12 | 14..=127 => true, @@ -51,16 +79,24 @@ pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &[u8]> { } } - take_while1(is_valid_character)(input) + map_res(take_while1(is_valid_character), std::str::from_utf8)(input) } /// ehlo-line = ehlo-keyword *( SP ehlo-param ) -pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], &[u8]> { - let parser = tuple((ehlo_keyword, many0(tuple((SP, ehlo_param))))); +/// +/// TODO: SMTP servers often respond with "AUTH=LOGIN PLAIN". Why? +pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], (&str, Vec<&str>)> { + let parser = tuple(( + map_res(ehlo_keyword, std::str::from_utf8), + opt(preceded( + alt((SP, tag("="))), // TODO: For Outlook? + separated_list(SP, ehlo_param), + )), + )); - let (remaining, parsed) = recognize(parser)(input)?; + let (remaining, (ehlo_keyword, ehlo_params)) = parser(input)?; - Ok((remaining, parsed)) + Ok((remaining, (ehlo_keyword, ehlo_params.unwrap_or(vec![])))) } /// Additional syntax of ehlo-params depends on ehlo-keyword @@ -81,7 +117,7 @@ pub fn ehlo_keyword(input: &[u8]) -> IResult<&[u8], &[u8]> { /// (US-ASCII 0-31 and 127 inclusive) /// /// ehlo-param = 1*(%d33-126) -pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &[u8]> { +pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> { fn is_valid_character(byte: u8) -> bool { match byte { 33..=126 => true, @@ -89,5 +125,52 @@ pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &[u8]> { } } - take_while1(is_valid_character)(input) + map_res(take_while1(is_valid_character), std::str::from_utf8)(input) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ehlo_ok_rsp() { + let (rem, out) = ehlo_ok_rsp( + b"250-example.org hello\r\n\ +250-AUTH LOGIN CRAM-MD5 PLAIN\r\n\ +250-AUTH=LOGIN CRAM-MD5 PLAIN\r\n\ +250-STARTTLS\r\n\ +250-SIZE 12345\r\n\ +250 8BITMIME\r\n", + ) + .unwrap(); + assert_eq!(rem, b""); + assert_eq!( + out, + EhloOkResp { + domain: "example.org".into(), + greet: Some("hello".into()), + lines: vec![ + ( + "AUTH".into(), + vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()] + ), + ( + "AUTH".into(), + vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()] + ), + ("STARTTLS".into(), vec![]), + ("SIZE".into(), vec!["12345".into()]), + ("8BITMIME".into(), vec![]), + ], + } + ); + } + + #[test] + fn test_ehlo_line() { + let (rem, (keyword, params)) = ehlo_line(b"SIZE 123456\r\n").unwrap(); + assert_eq!(rem, b"\r\n"); + assert_eq!(keyword, "SIZE"); + assert_eq!(params, &["123456"]); + } } diff --git a/src/parse/trace.rs b/src/parse/trace.rs index d4a0ec4..6f5c7af 100644 --- a/src/parse/trace.rs +++ b/src/parse/trace.rs @@ -79,7 +79,7 @@ pub fn By_domain(input: &[u8]) -> IResult<&[u8], &[u8]> { /// ( address-literal FWS "(" TCP-info ")" ) pub fn Extended_Domain(input: &[u8]) -> IResult<&[u8], &[u8]> { let parser = alt(( - Domain, + recognize(Domain), recognize(tuple((Domain, FWS, tag(b"("), TCP_info, tag(b")")))), recognize(tuple(( address_literal, @@ -100,7 +100,7 @@ pub fn Extended_Domain(input: &[u8]) -> IResult<&[u8], &[u8]> { /// TCP-info = address-literal / ( Domain FWS address-literal ) pub fn TCP_info(input: &[u8]) -> IResult<&[u8], &[u8]> { let parser = alt(( - address_literal, + recognize(address_literal), recognize(tuple((Domain, FWS, address_literal))), )); diff --git a/src/types.rs b/src/types.rs index ef3f162..eaf26ab 100644 --- a/src/types.rs +++ b/src/types.rs @@ -90,43 +90,11 @@ impl std::fmt::Debug for Command { } } -pub type EhloLine = (String, Option); - -#[cfg(test)] -mod test { - use crate::{parse::command::*, types::*}; - - #[test] - fn test_subdomain() { - let (rem, parsed) = sub_domain(b"example???").unwrap(); - assert_eq!(parsed, b"example"); - assert_eq!(rem, b"???"); - } - - #[test] - fn test_ehlo() { - let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap(); - assert_eq!(parsed, Command::Ehlo(b"[123.123.123.123]".to_vec())); - assert_eq!(rem, b"???"); - } - - #[test] - fn test_helo() { - let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap(); - assert_eq!(parsed, Command::Helo(b"example.com".to_vec())); - assert_eq!(rem, b"???"); - } - - #[test] - fn test_mail() { - let (rem, parsed) = mail(b"MAIL FROM:\r\n???").unwrap(); - assert_eq!( - parsed, - Command::Mail { - data: b"".to_vec(), - params: None - } - ); - assert_eq!(rem, b"???"); - } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EhloOkResp { + pub domain: String, + pub greet: Option, + pub lines: Vec, } + +pub type EhloLine = (String, Vec);