use crate::{
parse::{
address::{General_address_literal, IPv4_address_literal, IPv6_address_literal},
base64,
imf::atom::is_atext,
},
types::Command,
};
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::{map_res, opt, recognize},
multi::many0,
sequence::{delimited, preceded, tuple},
IResult,
};
pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = alt((
helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit,
starttls,
auth_login,
auth_plain,
));
let (remaining, parsed) = parser(input)?;
Ok((remaining, parsed))
}
pub fn helo(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"HELO"),
SP,
alt((Domain, address_literal)),
CRLF,
));
let (remaining, (_, _, data, _)) = parser(input)?;
Ok((
remaining,
Command::Helo {
fqdn_or_address_literal: data.into(),
},
))
}
pub fn ehlo(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"EHLO"),
SP,
alt((Domain, address_literal)),
CRLF,
));
let (remaining, (_, _, data, _)) = parser(input)?;
Ok((
remaining,
Command::Ehlo {
fqdn_or_address_literal: data.into(),
},
))
}
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.map(|params| params.into()),
},
))
}
pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((
tag_no_case(b"RCPT TO:"),
opt(SP),
alt((
recognize(tuple((tag_no_case(b"<Postmaster@"), Domain, tag(b">")))),
tag_no_case(b"<Postmaster>"),
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.map(|params| params.into()),
},
))
}
pub fn data(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"DATA"), CRLF));
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Data))
}
pub fn rset(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"RSET"), CRLF));
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Rset))
}
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.into(),
},
))
}
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.into(),
},
))
}
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.map(|data| data.into()),
},
))
}
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.map(|data| data.into()),
},
))
}
pub fn quit(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"QUIT"), CRLF));
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Quit))
}
pub fn starttls(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"STARTTLS"), CRLF));
let (remaining, _) = parser(input)?;
Ok((remaining, Command::StartTLS))
}
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], &[u8]> {
let parser = alt((Path, tag(b"<>")));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = Path;
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Path(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((
tag(b"<"),
opt(tuple((A_d_l, tag(b":")))),
Mailbox,
tag(b">"),
));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn A_d_l(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((At_domain, many0(tuple((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 Mail_parameters(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((esmtp_param, many0(tuple((SP, esmtp_param)))));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Rcpt_parameters(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((esmtp_param, many0(tuple((SP, esmtp_param)))));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn esmtp_param(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((esmtp_keyword, opt(tuple((tag(b"="), esmtp_value)))));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn esmtp_keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
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) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn esmtp_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn is_value_character(byte: u8) -> bool {
matches!(byte, 33..=60 | 62..=126)
}
take_while1(is_value_character)(input)
}
pub fn Keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = Ldh_str;
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
Atom(input)
}
pub fn Domain(input: &[u8]) -> IResult<&[u8], &str> {
let parser = tuple((sub_domain, many0(tuple((tag(b"."), sub_domain)))));
let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
Ok((remaining, parsed))
}
pub fn sub_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((take_while_m_n(1, 1, is_Let_dig), opt(Ldh_str)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn is_Let_dig(byte: u8) -> bool {
is_ALPHA(byte) || is_DIGIT(byte)
}
pub fn Ldh_str(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = many0(alt((
take_while_m_n(1, 1, is_ALPHA),
take_while_m_n(1, 1, is_DIGIT),
recognize(tuple((tag(b"-"), take_while_m_n(1, 1, is_Let_dig)))),
)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn address_literal(input: &[u8]) -> IResult<&[u8], &str> {
let mut parser = delimited(
tag(b"["),
map_res(
alt((
IPv4_address_literal,
IPv6_address_literal,
General_address_literal,
)),
std::str::from_utf8,
),
tag(b"]"),
);
let (remaining, parsed) = 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]> {
let parser = alt((Dot_string, Quoted_string));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Dot_string(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Atom, many0(tuple((tag(b"."), Atom)))));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn Atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(is_atext)(input)
}
pub fn Quoted_string(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = delimited(DQUOTE, many0(QcontentSMTP), DQUOTE);
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn QcontentSMTP(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = alt((take_while_m_n(1, 1, is_qtextSMTP), quoted_pairSMTP));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn quoted_pairSMTP(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn is_ascii_bs_or_sp(byte: u8) -> bool {
matches!(byte, 32..=126)
}
let parser = tuple((tag("\\"), take_while_m_n(1, 1, is_ascii_bs_or_sp)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
pub fn is_qtextSMTP(byte: u8) -> bool {
matches!(byte, 32..=33 | 35..=91 | 93..=126)
}
pub fn String(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = alt((Atom, Quoted_string));
let (remaining, parsed) = recognize(parser)(input)?;
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 {
fqdn_or_address_literal: 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 {
fqdn_or_address_literal: 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 {
reverse_path: b"<userx@y.foo.org>".to_vec(),
parameters: None
}
);
assert_eq!(rem, b"???");
}
}