use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
combinator::{map, map_res, opt, recognize, value},
multi::separated_list1,
sequence::{delimited, preceded, tuple},
IResult,
};
use crate::{
parse::{address::address_literal, base64, Atom, Domain, Quoted_string, String},
types::{Command, DomainOrAddress, Parameter},
};
pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
alt((
helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit,
starttls, auth_login, auth_plain, ))(input)
}
pub fn helo(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"HELO"),
SP,
alt((
map(Domain, |domain| DomainOrAddress::Domain(domain.into())),
map(address_literal, |address| {
DomainOrAddress::Address(address.into())
}),
)),
CRLF,
));
let (remaining, (_, _, domain_or_address, _)) = parser(input)?;
Ok((remaining, Command::Helo { domain_or_address }))
}
pub fn ehlo(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"EHLO"),
SP,
alt((
map(Domain, |domain| DomainOrAddress::Domain(domain.into())),
map(address_literal, |address| {
DomainOrAddress::Address(address.into())
}),
)),
CRLF,
));
let (remaining, (_, _, domain_or_address, _)) = parser(input)?;
Ok((remaining, Command::Ehlo { domain_or_address }))
}
pub fn mail(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"MAIL FROM:"),
opt(SP), Reverse_path,
opt(preceded(SP, Mail_parameters)),
CRLF,
));
let (remaining, (_, _, data, maybe_params, _)) = parser(input)?;
Ok((
remaining,
Command::Mail {
reverse_path: data.into(),
parameters: maybe_params.unwrap_or_default(),
},
))
}
pub fn Mail_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
separated_list1(SP, esmtp_param)(input)
}
pub fn esmtp_param(input: &[u8]) -> IResult<&[u8], Parameter> {
map(
tuple((esmtp_keyword, opt(preceded(tag(b"="), esmtp_value)))),
|(keyword, value)| Parameter::new(keyword, value),
)(input)
}
pub fn esmtp_keyword(input: &[u8]) -> IResult<&[u8], &str> {
let parser = tuple((
take_while_m_n(1, 1, |byte| is_ALPHA(byte) || is_DIGIT(byte)),
take_while(|byte| is_ALPHA(byte) || is_DIGIT(byte) || byte == b'-'),
));
let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
Ok((remaining, parsed))
}
pub fn esmtp_value(input: &[u8]) -> IResult<&[u8], &str> {
fn is_value_character(byte: u8) -> bool {
matches!(byte, 33..=60 | 62..=126)
}
map_res(take_while1(is_value_character), std::str::from_utf8)(input)
}
pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"RCPT TO:"),
opt(SP), alt((
map_res(
recognize(tuple((tag_no_case("<Postmaster@"), Domain, tag(">")))),
std::str::from_utf8,
),
map_res(tag_no_case("<Postmaster>"), std::str::from_utf8),
Forward_path,
)),
opt(preceded(SP, Rcpt_parameters)),
CRLF,
));
let (remaining, (_, _, data, maybe_params, _)) = parser(input)?;
Ok((
remaining,
Command::Rcpt {
forward_path: data.into(),
parameters: maybe_params.unwrap_or_default(),
},
))
}
pub fn Rcpt_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
separated_list1(SP, esmtp_param)(input)
}
pub fn data(input: &[u8]) -> IResult<&[u8], Command> {
value(Command::Data, tuple((tag_no_case(b"DATA"), CRLF)))(input)
}
pub fn rset(input: &[u8]) -> IResult<&[u8], Command> {
value(Command::Rset, tuple((tag_no_case(b"RSET"), CRLF)))(input)
}
pub fn vrfy(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"VRFY"), SP, String, CRLF));
let (remaining, (_, _, data, _)) = parser(input)?;
Ok((
remaining,
Command::Vrfy {
user_or_mailbox: data,
},
))
}
pub fn expn(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"EXPN"), SP, String, CRLF));
let (remaining, (_, _, data, _)) = parser(input)?;
Ok((remaining, Command::Expn { mailing_list: data }))
}
pub fn help(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"HELP"), opt(preceded(SP, String)), CRLF));
let (remaining, (_, maybe_data, _)) = parser(input)?;
Ok((
remaining,
Command::Help {
argument: maybe_data,
},
))
}
pub fn noop(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"NOOP"), opt(preceded(SP, String)), CRLF));
let (remaining, (_, maybe_data, _)) = parser(input)?;
Ok((
remaining,
Command::Noop {
argument: maybe_data,
},
))
}
pub fn quit(input: &[u8]) -> IResult<&[u8], Command> {
value(Command::Quit, tuple((tag_no_case(b"QUIT"), CRLF)))(input)
}
pub fn starttls(input: &[u8]) -> IResult<&[u8], Command> {
value(Command::StartTLS, tuple((tag_no_case(b"STARTTLS"), CRLF)))(input)
}
pub fn auth_login(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"AUTH"),
SP,
tag_no_case("LOGIN"),
opt(preceded(SP, base64)),
CRLF,
));
let (remaining, (_, _, _, maybe_username_b64, _)) = parser(input)?;
Ok((
remaining,
Command::AuthLogin(maybe_username_b64.map(|i| i.to_owned())),
))
}
pub fn auth_plain(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"AUTH"),
SP,
tag_no_case("PLAIN"),
opt(preceded(SP, base64)),
CRLF,
));
let (remaining, (_, _, _, maybe_credentials_b64, _)) = parser(input)?;
Ok((
remaining,
Command::AuthPlain(maybe_credentials_b64.map(|i| i.to_owned())),
))
}
pub fn Reverse_path(input: &[u8]) -> IResult<&[u8], &str> {
alt((Path, value("", tag("<>"))))(input)
}
pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &str> {
Path(input)
}
pub fn Path(input: &[u8]) -> IResult<&[u8], &str> {
delimited(
tag(b"<"),
map_res(
recognize(tuple((opt(tuple((A_d_l, tag(b":")))), Mailbox))),
std::str::from_utf8,
),
tag(b">"),
)(input)
}
pub fn A_d_l(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = separated_list1(tag(b","), At_domain);
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn At_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((tag(b"@"), Domain));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Local_part, tag(b"@"), alt((Domain, address_literal))));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((recognize(Dot_string), recognize(Quoted_string)))(input)
}
pub fn Dot_string(input: &[u8]) -> IResult<&[u8], &str> {
map_res(
recognize(separated_list1(tag(b"."), Atom)),
std::str::from_utf8,
)(input)
}
#[cfg(test)]
mod test {
use super::{ehlo, helo, mail};
use crate::types::{Command, DomainOrAddress};
#[test]
fn test_ehlo() {
let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap();
assert_eq!(
parsed,
Command::Ehlo {
domain_or_address: DomainOrAddress::Address("123.123.123.123".into()),
}
);
assert_eq!(rem, b"???");
}
#[test]
fn test_helo() {
let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap();
assert_eq!(
parsed,
Command::Helo {
domain_or_address: DomainOrAddress::Domain("example.com".into()),
}
);
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 {
reverse_path: "userx@y.foo.org".into(),
parameters: Vec::default(),
}
);
assert_eq!(rem, b"???");
}
}