EhloOkResp Parser + Type

* ehlo_ok_resp returns `EhloOkResp` now
* Converted some &[u8] parser to &str
* Added unit test
* Changed ehlo_line to accept "AUTH=LOGIN PLAIN" (note the "=")
This commit is contained in:
Damian Poddebniak 2020-08-10 14:25:37 +02:00
parent f14357d5b9
commit d41e8118f3
4 changed files with 176 additions and 82 deletions

View File

@ -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:<userx@y.foo.org>\r\n???").unwrap();
assert_eq!(
parsed,
Command::Mail {
data: b"<userx@y.foo.org>".to_vec(),
params: None
}
);
assert_eq!(rem, b"???");
}
}

View File

@ -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::<Vec<String>>();
(keyword.to_string(), params)
})
.collect::<Vec<(String, Vec<String>)>>();
lines.push((
keyword.to_string(),
params
.iter()
.map(|param| param.to_string())
.collect::<Vec<String>>(),
));
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"]);
}
}

View File

@ -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))),
));

View File

@ -90,43 +90,11 @@ impl std::fmt::Debug for Command {
}
}
pub type EhloLine = (String, Option<String>);
#[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:<userx@y.foo.org>\r\n???").unwrap();
assert_eq!(
parsed,
Command::Mail {
data: b"<userx@y.foo.org>".to_vec(),
params: None
}
);
assert_eq!(rem, b"???");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloOkResp {
pub domain: String,
pub greet: Option<String>,
pub lines: Vec<EhloLine>,
}
pub type EhloLine = (String, Vec<String>);