Too much, sorry.

This commit is contained in:
Damian Poddebniak 2021-05-15 19:28:22 +02:00
parent a327db7285
commit 10f614edb6
9 changed files with 649 additions and 515 deletions

View File

@ -1,24 +1,4 @@
pub mod parse; pub mod parse;
pub mod types; pub mod types;
pub fn escape(bytes: &[u8]) -> String { mod utils;
bytes
.iter()
.map(|byte| match byte {
0x00..=0x08 => format!("\\x{:02x}", byte),
0x09 => String::from("\\t"),
0x0A => String::from("\\n\n"),
0x0B => format!("\\x{:02x}", byte),
0x0C => format!("\\x{:02x}", byte),
0x0D => String::from("\\r"),
0x0e..=0x1f => format!("\\x{:02x}", byte),
0x20..=0x22 => format!("{}", *byte as char),
0x23..=0x5B => format!("{}", *byte as char),
0x5C => String::from("\\\\"),
0x5D..=0x7E => format!("{}", *byte as char),
0x7f => format!("\\x{:02x}", byte),
0x80..=0xff => format!("\\x{:02x}", byte),
})
.collect::<Vec<String>>()
.join("")
}

View File

@ -1,17 +1,38 @@
//! 4.1.3. Address Literals (RFC 5321) //! 4.1.3. Address Literals (RFC 5321)
use crate::parse::command::Ldh_str; use crate::parse::Ldh_str;
use abnf_core::streaming::is_DIGIT; use abnf_core::streaming::is_DIGIT;
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, tag_no_case, take_while1, take_while_m_n}, bytes::streaming::{tag, tag_no_case, take_while1, take_while_m_n},
character::is_hex_digit, character::is_hex_digit,
combinator::{opt, recognize}, combinator::{map_res, opt, recognize},
multi::{count, many_m_n}, multi::{count, many_m_n},
sequence::tuple, sequence::{delimited, tuple},
IResult, IResult,
}; };
/// address-literal = "[" (
/// IPv4-address-literal /
/// IPv6-address-literal /
/// General-address-literal
/// ) "]"
/// ; See Section 4.1.3
pub fn address_literal(input: &[u8]) -> IResult<&[u8], &str> {
delimited(
tag(b"["),
map_res(
alt((
IPv4_address_literal,
IPv6_address_literal,
General_address_literal,
)),
std::str::from_utf8,
),
tag(b"]"),
)(input)
}
/// IPv4-address-literal = Snum 3("." Snum) /// IPv4-address-literal = Snum 3("." Snum)
pub fn IPv4_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn IPv4_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Snum, count(tuple((tag(b"."), Snum)), 3))); let parser = tuple((Snum, count(tuple((tag(b"."), Snum)), 3)));
@ -21,47 +42,16 @@ pub fn IPv4_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
/// IPv6-address-literal = "IPv6:" IPv6-addr
pub fn IPv6_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((tag_no_case(b"IPv6:"), IPv6_addr));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// General-address-literal = Standardized-tag ":" 1*dcontent
pub fn General_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Standardized_tag, tag(b":"), take_while1(is_dcontent)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// Standardized-tag MUST be specified in a Standards-Track RFC and registered with IANA
///
/// Standardized-tag = Ldh-str
pub fn Standardized_tag(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = Ldh_str;
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// Printable US-ASCII excl. "[", "\", "]"
///
/// dcontent = %d33-90 / %d94-126
pub fn is_dcontent(byte: u8) -> bool {
matches!(byte, 33..=90 | 94..=126)
}
/// Representing a decimal integer value in the range 0 through 255 /// Representing a decimal integer value in the range 0 through 255
/// ///
/// Snum = 1*3DIGIT /// Snum = 1*3DIGIT
pub fn Snum(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Snum(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = take_while_m_n(1, 3, is_DIGIT); take_while_m_n(1, 3, is_DIGIT)(input)
}
/// IPv6-address-literal = "IPv6:" IPv6-addr
pub fn IPv6_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((tag_no_case(b"IPv6:"), IPv6_addr));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = recognize(parser)(input)?;
@ -77,15 +67,6 @@ pub fn IPv6_addr(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
/// IPv6-hex = 1*4HEXDIG
pub fn IPv6_hex(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = take_while_m_n(1, 4, is_hex_digit);
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// IPv6-full = IPv6-hex 7(":" IPv6-hex) /// IPv6-full = IPv6-hex 7(":" IPv6-hex)
pub fn IPv6_full(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn IPv6_full(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((IPv6_hex, count(tuple((tag(b":"), IPv6_hex)), 7))); let parser = tuple((IPv6_hex, count(tuple((tag(b":"), IPv6_hex)), 7)));
@ -95,6 +76,11 @@ pub fn IPv6_full(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
/// IPv6-hex = 1*4HEXDIG
pub fn IPv6_hex(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while_m_n(1, 4, is_hex_digit)(input)
}
/// The "::" represents at least 2 16-bit groups of zeros. /// The "::" represents at least 2 16-bit groups of zeros.
/// No more than 6 groups in addition to the "::" may be present. /// No more than 6 groups in addition to the "::" may be present.
/// ///
@ -156,3 +142,26 @@ pub fn IPv6v4_comp(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
/// General-address-literal = Standardized-tag ":" 1*dcontent
pub fn General_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Standardized_tag, tag(b":"), take_while1(is_dcontent)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// Standardized-tag MUST be specified in a Standards-Track RFC and registered with IANA
///
/// Standardized-tag = Ldh-str
pub fn Standardized_tag(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ldh_str(input)
}
/// Printable US-ASCII excl. "[", "\", "]"
///
/// dcontent = %d33-90 / %d94-126
pub fn is_dcontent(byte: u8) -> bool {
matches!(byte, 33..=90 | 94..=126)
}

View File

@ -1,32 +1,24 @@
use crate::{ use crate::{
parse::{ parse::{address::address_literal, base64, Atom, Domain, Quoted_string, String},
address::{General_address_literal, IPv4_address_literal, IPv6_address_literal}, types::{Command, Parameter},
base64,
imf::atom::is_atext,
},
types::Command,
}; };
use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, DQUOTE, SP}; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n}, bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
combinator::{map_res, opt, recognize}, combinator::{map, map_res, opt, recognize, value},
multi::many0, multi::separated_list1,
sequence::{delimited, preceded, tuple}, sequence::{delimited, preceded, tuple},
IResult, IResult,
}; };
pub fn command(input: &[u8]) -> IResult<&[u8], Command> { pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = alt(( alt((
helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit, helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit,
starttls, // Extensions starttls, // Extensions
auth_login, // https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf auth_login, // https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf
auth_plain, // RFC 4616 auth_plain, // RFC 4616
)); ))(input)
let (remaining, parsed) = parser(input)?;
Ok((remaining, parsed))
} }
/// helo = "HELO" SP Domain CRLF /// helo = "HELO" SP Domain CRLF
@ -83,11 +75,49 @@ pub fn mail(input: &[u8]) -> IResult<&[u8], Command> {
remaining, remaining,
Command::Mail { Command::Mail {
reverse_path: data.into(), reverse_path: data.into(),
parameters: maybe_params.map(|params| params.into()), parameters: maybe_params.unwrap_or_default(),
}, },
)) ))
} }
/// Mail-parameters = esmtp-param *(SP esmtp-param)
pub fn Mail_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
separated_list1(SP, esmtp_param)(input)
}
/// esmtp-param = esmtp-keyword ["=" esmtp-value]
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)
}
/// esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
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))
}
/// Any CHAR excluding "=", SP, and control characters.
/// If this string is an email address, i.e., a Mailbox,
/// then the "xtext" syntax [32] SHOULD be used.
///
/// esmtp-value = 1*(%d33-60 / %d62-126)
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)
}
/// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF /// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
/// ///
/// Note that, in a departure from the usual rules for /// Note that, in a departure from the usual rules for
@ -98,8 +128,11 @@ pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
tag_no_case(b"RCPT TO:"), tag_no_case(b"RCPT TO:"),
opt(SP), // Out-of-Spec, but Outlook does it ... opt(SP), // Out-of-Spec, but Outlook does it ...
alt(( alt((
recognize(tuple((tag_no_case(b"<Postmaster@"), Domain, tag(b">")))), map_res(
tag_no_case(b"<Postmaster>"), recognize(tuple((tag_no_case("<Postmaster@"), Domain, tag(">")))),
std::str::from_utf8,
),
map_res(tag_no_case("<Postmaster>"), std::str::from_utf8),
Forward_path, Forward_path,
)), )),
opt(preceded(SP, Rcpt_parameters)), opt(preceded(SP, Rcpt_parameters)),
@ -112,27 +145,24 @@ pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
remaining, remaining,
Command::Rcpt { Command::Rcpt {
forward_path: data.into(), forward_path: data.into(),
parameters: maybe_params.map(|params| params.into()), parameters: maybe_params.unwrap_or_default(),
}, },
)) ))
} }
/// Rcpt-parameters = esmtp-param *(SP esmtp-param)
pub fn Rcpt_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
separated_list1(SP, esmtp_param)(input)
}
/// data = "DATA" CRLF /// data = "DATA" CRLF
pub fn data(input: &[u8]) -> IResult<&[u8], Command> { pub fn data(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"DATA"), CRLF)); value(Command::Data, tuple((tag_no_case(b"DATA"), CRLF)))(input)
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Data))
} }
/// rset = "RSET" CRLF /// rset = "RSET" CRLF
pub fn rset(input: &[u8]) -> IResult<&[u8], Command> { pub fn rset(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"RSET"), CRLF)); value(Command::Rset, tuple((tag_no_case(b"RSET"), CRLF)))(input)
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Rset))
} }
/// vrfy = "VRFY" SP String CRLF /// vrfy = "VRFY" SP String CRLF
@ -144,7 +174,7 @@ pub fn vrfy(input: &[u8]) -> IResult<&[u8], Command> {
Ok(( Ok((
remaining, remaining,
Command::Vrfy { Command::Vrfy {
user_or_mailbox: data.into(), user_or_mailbox: data,
}, },
)) ))
} }
@ -155,12 +185,7 @@ pub fn expn(input: &[u8]) -> IResult<&[u8], Command> {
let (remaining, (_, _, data, _)) = parser(input)?; let (remaining, (_, _, data, _)) = parser(input)?;
Ok(( Ok((remaining, Command::Expn { mailing_list: data }))
remaining,
Command::Expn {
mailing_list: data.into(),
},
))
} }
/// help = "HELP" [ SP String ] CRLF /// help = "HELP" [ SP String ] CRLF
@ -172,7 +197,7 @@ pub fn help(input: &[u8]) -> IResult<&[u8], Command> {
Ok(( Ok((
remaining, remaining,
Command::Help { Command::Help {
argument: maybe_data.map(|data| data.into()), argument: maybe_data,
}, },
)) ))
} }
@ -186,26 +211,18 @@ pub fn noop(input: &[u8]) -> IResult<&[u8], Command> {
Ok(( Ok((
remaining, remaining,
Command::Noop { Command::Noop {
argument: maybe_data.map(|data| data.into()), argument: maybe_data,
}, },
)) ))
} }
/// quit = "QUIT" CRLF /// quit = "QUIT" CRLF
pub fn quit(input: &[u8]) -> IResult<&[u8], Command> { pub fn quit(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"QUIT"), CRLF)); value(Command::Quit, tuple((tag_no_case(b"QUIT"), CRLF)))(input)
let (remaining, _) = parser(input)?;
Ok((remaining, Command::Quit))
} }
pub fn starttls(input: &[u8]) -> IResult<&[u8], Command> { pub fn starttls(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = tuple((tag_no_case(b"STARTTLS"), CRLF)); value(Command::StartTLS, tuple((tag_no_case(b"STARTTLS"), CRLF)))(input)
let (remaining, _) = parser(input)?;
Ok((remaining, Command::StartTLS))
} }
/// https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf /// https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf
@ -256,35 +273,25 @@ pub fn auth_plain(input: &[u8]) -> IResult<&[u8], Command> {
// ----- 4.1.2. Command Argument Syntax (RFC 5321) ----- // ----- 4.1.2. Command Argument Syntax (RFC 5321) -----
/// Reverse-path = Path / "<>" /// Reverse-path = Path / "<>"
pub fn Reverse_path(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Reverse_path(input: &[u8]) -> IResult<&[u8], &str> {
let parser = alt((Path, tag(b"<>"))); alt((Path, value("", tag("<>"))))(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
/// Forward-path = Path /// Forward-path = Path
pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &str> {
let parser = Path; Path(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
// Path = "<" [ A-d-l ":" ] Mailbox ">" // Path = "<" [ A-d-l ":" ] Mailbox ">"
pub fn Path(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Path(input: &[u8]) -> IResult<&[u8], &str> {
let parser = tuple(( delimited(
tag(b"<"), tag(b"<"),
opt(tuple((A_d_l, tag(b":")))), map_res(
Mailbox, recognize(tuple((opt(tuple((A_d_l, tag(b":")))), Mailbox))),
std::str::from_utf8,
),
tag(b">"), tag(b">"),
)); )(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
/// A-d-l = At-domain *( "," At-domain ) /// A-d-l = At-domain *( "," At-domain )
@ -292,7 +299,7 @@ pub fn Path(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// ; route", MUST BE accepted, SHOULD NOT be /// ; route", MUST BE accepted, SHOULD NOT be
/// ; generated, and SHOULD be ignored. /// ; generated, and SHOULD be ignored.
pub fn A_d_l(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn A_d_l(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((At_domain, many0(tuple((tag(b","), At_domain))))); let parser = separated_list1(tag(b","), At_domain);
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = recognize(parser)(input)?;
@ -308,133 +315,6 @@ pub fn At_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
/// Mail-parameters = esmtp-param *(SP esmtp-param)
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))
}
/// Rcpt-parameters = esmtp-param *(SP esmtp-param)
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))
}
/// esmtp-param = esmtp-keyword ["=" esmtp-value]
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))
}
/// esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
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))
}
/// Any CHAR excluding "=", SP, and control characters.
/// If this string is an email address, i.e., a Mailbox,
/// then the "xtext" syntax [32] SHOULD be used.
///
/// esmtp-value = 1*(%d33-60 / %d62-126)
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)
}
/// Keyword = Ldh-str
pub fn Keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = Ldh_str;
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
/// Argument = Atom
pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
Atom(input)
}
/// Domain = sub-domain *("." sub-domain)
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))
}
/// sub-domain = Let-dig [Ldh-str]
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))
}
/// Let-dig = ALPHA / DIGIT
pub fn is_Let_dig(byte: u8) -> bool {
is_ALPHA(byte) || is_DIGIT(byte)
}
/// Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
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))
}
/// address-literal = "[" (
/// IPv4-address-literal /
/// IPv6-address-literal /
/// General-address-literal
/// ) "]"
/// ; See Section 4.1.3
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))
}
/// Mailbox = Local-part "@" ( Domain / address-literal ) /// Mailbox = Local-part "@" ( Domain / address-literal )
pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((Local_part, tag(b"@"), alt((Domain, address_literal)))); let parser = tuple((Local_part, tag(b"@"), alt((Domain, address_literal))));
@ -447,96 +327,41 @@ pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// Local-part = Dot-string / Quoted-string /// Local-part = Dot-string / Quoted-string
/// ; MAY be case-sensitive /// ; MAY be case-sensitive
pub fn Local_part(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = alt((Dot_string, Quoted_string)); alt((recognize(Dot_string), recognize(Quoted_string)))(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
/// Dot-string = Atom *("." Atom) /// Dot-string = Atom *("." Atom)
pub fn Dot_string(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Dot_string(input: &[u8]) -> IResult<&[u8], &str> {
let parser = tuple((Atom, many0(tuple((tag(b"."), Atom))))); map_res(
recognize(separated_list1(tag(b"."), Atom)),
let (remaining, parsed) = recognize(parser)(input)?; std::str::from_utf8,
)(input)
Ok((remaining, parsed))
} }
/// Atom = 1*atext // Not used?
pub fn Atom(input: &[u8]) -> IResult<&[u8], &[u8]> { /// Keyword = Ldh-str
take_while1(is_atext)(input) //pub fn Keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
} // Ldh_str(input)
//}
/// Quoted-string = DQUOTE *QcontentSMTP DQUOTE // Not used?
pub fn Quoted_string(input: &[u8]) -> IResult<&[u8], &[u8]> { /// Argument = Atom
let parser = delimited(DQUOTE, many0(QcontentSMTP), DQUOTE); //pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
// Atom(input)
let (remaining, parsed) = recognize(parser)(input)?; //}
Ok((remaining, parsed))
}
/// QcontentSMTP = qtextSMTP / quoted-pairSMTP
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))
}
/// Backslash followed by any ASCII graphic (including itself) or SPace
///
/// quoted-pairSMTP = %d92 %d32-126
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))
}
/// Within a quoted string, any ASCII graphic or space is permitted
/// without blackslash-quoting except double-quote and the backslash itself.
///
/// qtextSMTP = %d32-33 / %d35-91 / %d93-126
pub fn is_qtextSMTP(byte: u8) -> bool {
matches!(byte, 32..=33 | 35..=91 | 93..=126)
}
/// String = Atom / Quoted-string
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)] #[cfg(test)]
mod test { mod test {
use super::{ehlo, helo, mail, sub_domain}; use super::{ehlo, helo, mail};
use crate::types::Command; 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] #[test]
fn test_ehlo() { fn test_ehlo() {
let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap(); let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap();
assert_eq!( assert_eq!(
parsed, parsed,
Command::Ehlo { Command::Ehlo {
fqdn_or_address_literal: b"123.123.123.123".to_vec() fqdn_or_address_literal: "123.123.123.123".into()
} }
); );
assert_eq!(rem, b"???"); assert_eq!(rem, b"???");
@ -548,7 +373,7 @@ mod test {
assert_eq!( assert_eq!(
parsed, parsed,
Command::Helo { Command::Helo {
fqdn_or_address_literal: b"example.com".to_vec() fqdn_or_address_literal: "example.com".into()
} }
); );
assert_eq!(rem, b"???"); assert_eq!(rem, b"???");
@ -560,8 +385,8 @@ mod test {
assert_eq!( assert_eq!(
parsed, parsed,
Command::Mail { Command::Mail {
reverse_path: b"<userx@y.foo.org>".to_vec(), reverse_path: "userx@y.foo.org".into(),
parameters: None parameters: Vec::default(),
} }
); );
assert_eq!(rem, b"???"); assert_eq!(rem, b"???");

View File

@ -1,15 +1,17 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use abnf_core::streaming::{is_ALPHA, is_DIGIT}; use crate::{parse::imf::atom::is_atext, types::AtomOrQuoted, utils::unescape_quoted};
use abnf_core::streaming::{is_ALPHA, is_DIGIT, DQUOTE};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, take_while}, bytes::streaming::{tag, take_while, take_while1, take_while_m_n},
character::streaming::digit1, character::streaming::digit1,
combinator::{map_res, opt, recognize}, combinator::{map, map_res, opt, recognize},
sequence::tuple, multi::{many0, separated_list1},
sequence::{delimited, tuple},
IResult, IResult,
}; };
use std::str::from_utf8; use std::{borrow::Cow, str::from_utf8};
pub mod address; pub mod address;
pub mod command; pub mod command;
@ -19,10 +21,6 @@ pub mod response;
pub mod trace; pub mod trace;
pub mod utils; pub mod utils;
fn is_base64_char(i: u8) -> bool {
is_ALPHA(i) || is_DIGIT(i) || i == b'+' || i == b'/'
}
pub fn base64(input: &[u8]) -> IResult<&[u8], &str> { pub fn base64(input: &[u8]) -> IResult<&[u8], &str> {
let mut parser = map_res( let mut parser = map_res(
recognize(tuple(( recognize(tuple((
@ -37,6 +35,128 @@ pub fn base64(input: &[u8]) -> IResult<&[u8], &str> {
Ok((remaining, base64)) Ok((remaining, base64))
} }
fn is_base64_char(i: u8) -> bool {
is_ALPHA(i) || is_DIGIT(i) || i == b'+' || i == b'/'
}
pub fn number(input: &[u8]) -> IResult<&[u8], u32> { pub fn number(input: &[u8]) -> IResult<&[u8], u32> {
map_res(map_res(digit1, from_utf8), str::parse::<u32>)(input) // FIXME(perf): use from_utf8_unchecked map_res(map_res(digit1, from_utf8), str::parse::<u32>)(input) // FIXME(perf): use from_utf8_unchecked
} }
// -------------------------------------------------------------------------------------------------
/// String = Atom / Quoted-string
pub fn String(input: &[u8]) -> IResult<&[u8], AtomOrQuoted> {
alt((
map(Atom, |atom| AtomOrQuoted::Atom(atom.into())),
map(Quoted_string, |quoted| AtomOrQuoted::Quoted(quoted.into())),
))(input)
}
/// Atom = 1*atext
pub fn Atom(input: &[u8]) -> IResult<&[u8], &str> {
map_res(take_while1(is_atext), std::str::from_utf8)(input)
}
/// Quoted-string = DQUOTE *QcontentSMTP DQUOTE
pub fn Quoted_string(input: &[u8]) -> IResult<&[u8], Cow<'_, str>> {
map(
delimited(
DQUOTE,
map_res(recognize(many0(QcontentSMTP)), std::str::from_utf8),
DQUOTE,
),
|s| unescape_quoted(s),
)(input)
}
/// QcontentSMTP = qtextSMTP / quoted-pairSMTP
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))
}
/// Within a quoted string, any ASCII graphic or space is permitted
/// without blackslash-quoting except double-quote and the backslash itself.
///
/// qtextSMTP = %d32-33 / %d35-91 / %d93-126
pub fn is_qtextSMTP(byte: u8) -> bool {
matches!(byte, 32..=33 | 35..=91 | 93..=126)
}
/// Backslash followed by any ASCII graphic (including itself) or SPace
///
/// quoted-pairSMTP = %d92 %d32-126
///
/// FIXME: How should e.g. "\a" be interpreted?
pub fn quoted_pairSMTP(input: &[u8]) -> IResult<&[u8], &[u8]> {
//fn is_value(byte: u8) -> bool {
// matches!(byte, 32..=126)
//}
// FIXME: Only allow "\\" and "\"" for now ...
fn is_value(byte: u8) -> bool {
byte == b'\\' || byte == b'\"'
}
let parser = tuple((tag("\\"), take_while_m_n(1, 1, is_value)));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
}
// -------------------------------------------------------------------------------------------------
/// Domain = sub-domain *("." sub-domain)
pub fn Domain(input: &[u8]) -> IResult<&[u8], &str> {
let parser = separated_list1(tag(b"."), sub_domain);
let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
Ok((remaining, parsed))
}
/// sub-domain = Let-dig [Ldh-str]
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))
}
/// Let-dig = ALPHA / DIGIT
pub fn is_Let_dig(byte: u8) -> bool {
is_ALPHA(byte) || is_DIGIT(byte)
}
/// Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
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))
}
// -------------------------------------------------------------------------------------------------
#[cfg(test)]
pub mod test {
use super::sub_domain;
#[test]
fn test_subdomain() {
let (rem, parsed) = sub_domain(b"example???").unwrap();
assert_eq!(parsed, b"example");
assert_eq!(rem, b"???");
}
}

View File

@ -1,7 +1,7 @@
//! 4.2. SMTP Replies (RFC 5321) //! 4.2. SMTP Replies (RFC 5321)
use crate::{ use crate::{
parse::command::{address_literal, Domain}, parse::{address::address_literal, Domain},
types::Greeting as GreetingType, types::Greeting as GreetingType,
}; };
use abnf_core::streaming::{CRLF, SP}; use abnf_core::streaming::{CRLF, SP};

View File

@ -1,9 +1,12 @@
use crate::{parse::command::Domain, types::EhloOkResp}; use crate::{
parse::{number, Domain},
types::{AuthMechanism, Capability, EhloOkResp},
};
use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP}; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, take_while, take_while1, take_while_m_n}, bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
combinator::{map, map_res, opt, recognize}, combinator::{map, map_res, opt, recognize, value},
multi::{many0, separated_list0}, multi::{many0, separated_list0},
sequence::{delimited, preceded, tuple}, sequence::{delimited, preceded, tuple},
IResult, IResult,
@ -36,27 +39,11 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> {
ehlo_line, ehlo_line,
CRLF, CRLF,
)), )),
|(_, domain, maybe_ehlo, _, lines, _, (keyword, params), _)| EhloOkResp { |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| EhloOkResp {
domain: domain.to_owned(), domain: domain.to_owned(),
greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
lines: { lines: {
let mut lines = lines lines.push(line);
.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 lines
}, },
}, },
@ -82,8 +69,14 @@ pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> {
/// ehlo-line = ehlo-keyword *( SP ehlo-param ) /// ehlo-line = ehlo-keyword *( SP ehlo-param )
/// ///
/// TODO: SMTP servers often respond with "AUTH=LOGIN PLAIN". Why? /// TODO: SMTP servers often respond with "AUTH=LOGIN PLAIN". Why?
pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], (&str, Vec<&str>)> { pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], Capability> {
let mut parser = tuple(( let auth = tuple((
tag_no_case("AUTH"),
alt((tag_no_case(" "), tag_no_case("="))),
separated_list0(SP, auth_mechanism),
));
let other = tuple((
map_res(ehlo_keyword, std::str::from_utf8), map_res(ehlo_keyword, std::str::from_utf8),
opt(preceded( opt(preceded(
alt((SP, tag("="))), // TODO: For Outlook? alt((SP, tag("="))), // TODO: For Outlook?
@ -91,9 +84,56 @@ pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], (&str, Vec<&str>)> {
)), )),
)); ));
let (remaining, (ehlo_keyword, ehlo_params)) = parser(input)?; alt((
value(Capability::EXPN, tag_no_case("EXPN")),
value(Capability::Help, tag_no_case("HELP")),
value(Capability::EightBitMIME, tag_no_case("8BITMIME")),
map(preceded(tag_no_case("SIZE "), number), Capability::Size),
value(Capability::Chunking, tag_no_case("CHUNKING")),
value(Capability::BinaryMIME, tag_no_case("BINARYMIME")),
value(Capability::Checkpoint, tag_no_case("CHECKPOINT")),
value(Capability::DeliverBy, tag_no_case("DELIVERBY")),
value(Capability::Pipelining, tag_no_case("PIPELINING")),
value(Capability::DSN, tag_no_case("DSN")),
value(Capability::ETRN, tag_no_case("ETRN")),
value(
Capability::EnhancedStatusCodes,
tag_no_case("ENHANCEDSTATUSCODES"),
),
value(Capability::StartTLS, tag_no_case("STARTTLS")),
// FIXME: NO-SOLICITING
value(Capability::MTRK, tag_no_case("MTRK")),
value(Capability::ATRN, tag_no_case("ATRN")),
map(auth, |(_, _, mechanisms)| Capability::Auth(mechanisms)),
value(Capability::BURL, tag_no_case("BURL")),
// FIXME: FUTURERELEASE
// FIXME: CONPERM
// FIXME: CONNEG
value(Capability::SMTPUTF8, tag_no_case("SMTPUTF8")),
// FIXME: MT-PRIORITY
value(Capability::RRVS, tag_no_case("RRVS")),
value(Capability::RequireTLS, tag_no_case("REQUIRETLS")),
map(other, |(keyword, params)| Capability::Other {
keyword: keyword.into(),
params: params
.map(|v| v.iter().map(|s| s.to_string()).collect())
.unwrap_or_default(),
}),
))(input)
}
Ok((remaining, (ehlo_keyword, ehlo_params.unwrap_or_default()))) 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 /// Additional syntax of ehlo-params depends on ehlo-keyword
@ -125,6 +165,7 @@ pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::types::AuthMechanism;
#[test] #[test]
fn test_ehlo_ok_rsp() { fn test_ehlo_ok_rsp() {
@ -144,17 +185,19 @@ mod test {
domain: "example.org".into(), domain: "example.org".into(),
greet: Some("hello".into()), greet: Some("hello".into()),
lines: vec![ lines: vec![
( Capability::Auth(vec![
"AUTH".into(), AuthMechanism::Login,
vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()] AuthMechanism::CramMD5,
), AuthMechanism::Plain
( ]),
"AUTH".into(), Capability::Auth(vec![
vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()] AuthMechanism::Login,
), AuthMechanism::CramMD5,
("STARTTLS".into(), vec![]), AuthMechanism::Plain
("SIZE".into(), vec!["12345".into()]), ]),
("8BITMIME".into(), vec![]), Capability::StartTLS,
Capability::Size(12345),
Capability::EightBitMIME,
], ],
} }
); );
@ -162,9 +205,8 @@ mod test {
#[test] #[test]
fn test_ehlo_line() { fn test_ehlo_line() {
let (rem, (keyword, params)) = ehlo_line(b"SIZE 123456\r\n").unwrap(); let (rem, capability) = ehlo_line(b"SIZE 123456\r\n").unwrap();
assert_eq!(rem, b"\r\n"); assert_eq!(rem, b"\r\n");
assert_eq!(keyword, "SIZE"); assert_eq!(capability, Capability::Size(123456));
assert_eq!(params, &["123456"]);
} }
} }

View File

@ -1,17 +1,19 @@
use crate::parse::{ use crate::parse::{
command::{address_literal, Atom, Domain, Mailbox, Path, Reverse_path, String}, address::address_literal,
command::{Mailbox, Path, Reverse_path},
imf::{ imf::{
datetime::date_time, datetime::date_time,
folding_ws_and_comment::{CFWS, FWS}, folding_ws_and_comment::{CFWS, FWS},
identification::msg_id, identification::msg_id,
}, },
Atom, Domain, String,
}; };
/// 4.4. Trace Information (RFC 5321) /// 4.4. Trace Information (RFC 5321)
use abnf_core::streaming::CRLF; use abnf_core::streaming::CRLF;
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, tag_no_case}, bytes::streaming::{tag, tag_no_case},
combinator::{opt, recognize}, combinator::{map_res, opt, recognize},
multi::many1, multi::many1,
sequence::tuple, sequence::tuple,
IResult, IResult,
@ -145,7 +147,12 @@ pub fn With(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// ID = CFWS "ID" FWS ( Atom / msg-id ) /// ID = CFWS "ID" FWS ( Atom / msg-id )
/// ; msg-id is defined in RFC 5322 [4] /// ; msg-id is defined in RFC 5322 [4]
pub fn ID(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn ID(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((CFWS, tag_no_case(b"ID"), FWS, alt((Atom, msg_id)))); let parser = tuple((
CFWS,
tag_no_case(b"ID"),
FWS,
recognize(alt((recognize(Atom), msg_id))),
));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = recognize(parser)(input)?;
@ -154,7 +161,12 @@ pub fn ID(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// For = CFWS "FOR" FWS ( Path / Mailbox ) /// For = CFWS "FOR" FWS ( Path / Mailbox )
pub fn For(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn For(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((CFWS, tag_no_case(b"FOR"), FWS, alt((Path, Mailbox)))); let parser = tuple((
CFWS,
tag_no_case(b"FOR"),
FWS,
alt((recognize(Path), Mailbox)),
));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = recognize(parser)(input)?;
@ -174,12 +186,8 @@ pub fn Additional_Registered_Clauses(input: &[u8]) -> IResult<&[u8], &[u8]> {
} }
/// Link = "TCP" / Addtl-Link /// Link = "TCP" / Addtl-Link
pub fn Link(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Link(input: &[u8]) -> IResult<&[u8], &str> {
let parser = alt((tag_no_case(b"TCP"), Addtl_Link)); alt((map_res(tag_no_case("TCP"), std::str::from_utf8), Addtl_Link))(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
/// Additional standard names for links are registered with the Internet Assigned Numbers /// Additional standard names for links are registered with the Internet Assigned Numbers
@ -187,21 +195,17 @@ pub fn Link(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// SHOULD NOT use unregistered names. /// SHOULD NOT use unregistered names.
/// ///
/// Addtl-Link = Atom /// Addtl-Link = Atom
pub fn Addtl_Link(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Addtl_Link(input: &[u8]) -> IResult<&[u8], &str> {
let parser = Atom; Atom(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }
/// Protocol = "ESMTP" / "SMTP" / Attdl-Protocol /// Protocol = "ESMTP" / "SMTP" / Attdl-Protocol
pub fn Protocol(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Protocol(input: &[u8]) -> IResult<&[u8], &str> {
let parser = alt((tag_no_case(b"ESMTP"), tag_no_case(b"SMTP"), Attdl_Protocol)); alt((
map_res(tag_no_case(b"ESMTP"), std::str::from_utf8),
let (remaining, parsed) = recognize(parser)(input)?; map_res(tag_no_case(b"SMTP"), std::str::from_utf8),
Attdl_Protocol,
Ok((remaining, parsed)) ))(input)
} }
/// Additional standard names for protocols are registered with the Internet Assigned Numbers /// Additional standard names for protocols are registered with the Internet Assigned Numbers
@ -209,10 +213,6 @@ pub fn Protocol(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// use unregistered names. /// use unregistered names.
/// ///
/// Attdl-Protocol = Atom /// Attdl-Protocol = Atom
pub fn Attdl_Protocol(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Attdl_Protocol(input: &[u8]) -> IResult<&[u8], &str> {
let parser = Atom; Atom(input)
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
} }

View File

@ -1,21 +1,21 @@
use crate::escape; use crate::utils::escape_quoted;
use std::io::Write; use std::io::Write;
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Command { pub enum Command {
Ehlo { Ehlo {
fqdn_or_address_literal: Vec<u8>, fqdn_or_address_literal: String,
}, },
Helo { Helo {
fqdn_or_address_literal: Vec<u8>, fqdn_or_address_literal: String,
}, },
Mail { Mail {
reverse_path: Vec<u8>, reverse_path: String,
parameters: Option<Vec<u8>>, parameters: Vec<Parameter>,
}, },
Rcpt { Rcpt {
forward_path: Vec<u8>, forward_path: String,
parameters: Option<Vec<u8>>, parameters: Vec<Parameter>,
}, },
Data, Data,
Rset, Rset,
@ -26,7 +26,7 @@ pub enum Command {
/// This command has no effect on the reverse-path buffer, the forward- /// This command has no effect on the reverse-path buffer, the forward-
/// path buffer, or the mail data buffer. /// path buffer, or the mail data buffer.
Vrfy { Vrfy {
user_or_mailbox: Vec<u8>, user_or_mailbox: AtomOrQuoted,
}, },
/// This command asks the receiver to confirm that the argument /// This command asks the receiver to confirm that the argument
/// identifies a mailing list, and if so, to return the membership of /// identifies a mailing list, and if so, to return the membership of
@ -38,7 +38,7 @@ pub enum Command {
/// path buffer, or the mail data buffer, and it may be issued at any /// path buffer, or the mail data buffer, and it may be issued at any
/// time. /// time.
Expn { Expn {
mailing_list: Vec<u8>, mailing_list: AtomOrQuoted,
}, },
/// This command causes the server to send helpful information to the /// This command causes the server to send helpful information to the
/// client. The command MAY take an argument (e.g., any command name) /// client. The command MAY take an argument (e.g., any command name)
@ -51,7 +51,7 @@ pub enum Command {
/// path buffer, or the mail data buffer, and it may be issued at any /// path buffer, or the mail data buffer, and it may be issued at any
/// time. /// time.
Help { Help {
argument: Option<Vec<u8>>, argument: Option<AtomOrQuoted>,
}, },
/// This command does not affect any parameters or previously entered /// This command does not affect any parameters or previously entered
/// commands. It specifies no action other than that the receiver send a /// commands. It specifies no action other than that the receiver send a
@ -63,7 +63,7 @@ pub enum Command {
/// path buffer, or the mail data buffer, and it may be issued at any /// path buffer, or the mail data buffer, and it may be issued at any
/// time. /// time.
Noop { Noop {
argument: Option<Vec<u8>>, argument: Option<AtomOrQuoted>,
}, },
/// This command specifies that the receiver MUST send a "221 OK" reply, /// This command specifies that the receiver MUST send a "221 OK" reply,
/// and then close the transmission channel. /// and then close the transmission channel.
@ -90,6 +90,18 @@ pub enum Command {
AuthPlain(Option<String>), AuthPlain(Option<String>),
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Parameter {
keyword: String,
value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AtomOrQuoted {
Atom(String),
Quoted(String),
}
impl Command { impl Command {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
@ -112,60 +124,7 @@ impl Command {
Command::AuthPlain(_) => "AUTHPLAIN", Command::AuthPlain(_) => "AUTHPLAIN",
} }
} }
}
// FIXME: try to derive(Debug) instead
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
use Command::*;
match self {
Ehlo {
fqdn_or_address_literal,
} => write!(f, "Ehlo({})", escape(fqdn_or_address_literal)),
Helo {
fqdn_or_address_literal,
} => write!(f, "Helo({})", escape(fqdn_or_address_literal)),
Mail {
reverse_path: path,
parameters: None,
} => write!(f, "Mail({})", escape(path)),
Mail {
reverse_path: path,
parameters: Some(params),
} => write!(f, "Mail({}, {})", escape(path), escape(params)),
Rcpt {
forward_path: data,
parameters: None,
} => write!(f, "Rcpt({})", escape(data)),
Rcpt {
forward_path: data,
parameters: Some(params),
} => write!(f, "Rcpt({}, {})", escape(data), escape(params)),
Data => write!(f, "Data"),
Rset => write!(f, "Rset"),
Vrfy { user_or_mailbox } => write!(f, "Vrfy({})", escape(user_or_mailbox)),
Expn { mailing_list } => write!(f, "Expn({})", escape(mailing_list)),
Help { argument: None } => write!(f, "Help"),
Help {
argument: Some(data),
} => write!(f, "Help({})", escape(data)),
Noop { argument: None } => write!(f, "Noop"),
Noop {
argument: Some(data),
} => write!(f, "Noop({})", escape(data)),
Quit => write!(f, "Quit"),
// Extensions
StartTLS => write!(f, "StartTLS"),
// TODO: SMTP Auth
AuthLogin(data) => write!(f, "AuthLogin({:?})", data),
// TODO: SMTP Auth
AuthPlain(data) => write!(f, "AuthPlain({:?})", data),
}
}
}
impl Command {
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> { pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
use Command::*; use Command::*;
@ -175,50 +134,42 @@ impl Command {
fqdn_or_address_literal, fqdn_or_address_literal,
} => { } => {
writer.write_all(b"HELO ")?; writer.write_all(b"HELO ")?;
writer.write_all(fqdn_or_address_literal)?; writer.write_all(fqdn_or_address_literal.as_bytes())?;
} }
// ehlo = "EHLO" SP ( Domain / address-literal ) CRLF // ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
Ehlo { Ehlo {
fqdn_or_address_literal, fqdn_or_address_literal,
} => { } => {
writer.write_all(b"EHLO ")?; writer.write_all(b"EHLO ")?;
writer.write_all(fqdn_or_address_literal)?; writer.write_all(fqdn_or_address_literal.as_bytes())?;
} }
// mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF // mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
Mail { Mail {
reverse_path, reverse_path,
parameters: None, parameters,
} => { } => {
writer.write_all(b"MAIL FROM:<")?; writer.write_all(b"MAIL FROM:<")?;
writer.write_all(reverse_path)?; writer.write_all(reverse_path.as_bytes())?;
writer.write_all(b">")?; writer.write_all(b">")?;
for parameter in parameters {
writer.write_all(b" ")?;
parameter.serialize(writer)?;
} }
Mail {
reverse_path,
parameters: Some(parameters),
} => {
writer.write_all(b"MAIL FROM:<")?;
writer.write_all(reverse_path)?;
writer.write_all(b"> ")?;
writer.write_all(parameters)?;
} }
// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF // rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
Rcpt { Rcpt {
forward_path, forward_path,
parameters: None, parameters,
} => { } => {
writer.write_all(b"RCPT TO:<")?; writer.write_all(b"RCPT TO:<")?;
writer.write_all(forward_path)?; writer.write_all(forward_path.as_bytes())?;
writer.write_all(b">")?; writer.write_all(b">")?;
for parameter in parameters {
writer.write_all(b" ")?;
parameter.serialize(writer)?;
} }
Rcpt {
forward_path,
parameters: Some(parameters),
} => {
writer.write_all(b"RCPT TO:<")?;
writer.write_all(forward_path)?;
writer.write_all(b"> ")?;
writer.write_all(parameters)?;
} }
// data = "DATA" CRLF // data = "DATA" CRLF
Data => writer.write_all(b"DATA")?, Data => writer.write_all(b"DATA")?,
@ -227,12 +178,12 @@ impl Command {
// vrfy = "VRFY" SP String CRLF // vrfy = "VRFY" SP String CRLF
Vrfy { user_or_mailbox } => { Vrfy { user_or_mailbox } => {
writer.write_all(b"VRFY ")?; writer.write_all(b"VRFY ")?;
writer.write_all(user_or_mailbox)?; user_or_mailbox.serialize(writer)?;
} }
// expn = "EXPN" SP String CRLF // expn = "EXPN" SP String CRLF
Expn { mailing_list } => { Expn { mailing_list } => {
writer.write_all(b"EXPN ")?; writer.write_all(b"EXPN ")?;
writer.write_all(mailing_list)?; mailing_list.serialize(writer)?;
} }
// help = "HELP" [ SP String ] CRLF // help = "HELP" [ SP String ] CRLF
Help { argument: None } => writer.write_all(b"HELP")?, Help { argument: None } => writer.write_all(b"HELP")?,
@ -240,7 +191,7 @@ impl Command {
argument: Some(data), argument: Some(data),
} => { } => {
writer.write_all(b"HELP ")?; writer.write_all(b"HELP ")?;
writer.write_all(data)?; data.serialize(writer)?;
} }
// noop = "NOOP" [ SP String ] CRLF // noop = "NOOP" [ SP String ] CRLF
Noop { argument: None } => writer.write_all(b"NOOP")?, Noop { argument: None } => writer.write_all(b"NOOP")?,
@ -248,7 +199,7 @@ impl Command {
argument: Some(data), argument: Some(data),
} => { } => {
writer.write_all(b"NOOP ")?; writer.write_all(b"NOOP ")?;
writer.write_all(data)?; data.serialize(writer)?;
} }
// quit = "QUIT" CRLF // quit = "QUIT" CRLF
Quit => writer.write_all(b"QUIT")?, Quit => writer.write_all(b"QUIT")?,
@ -277,6 +228,45 @@ impl Command {
} }
} }
impl Parameter {
pub fn new<K: Into<String>, V: Into<String>>(keyword: K, value: Option<V>) -> Parameter {
Parameter {
keyword: keyword.into(),
value: value.map(Into::into),
}
}
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
writer.write_all(self.keyword.as_bytes())?;
if let Some(ref value) = self.value {
writer.write_all(b"=")?;
writer.write_all(value.as_bytes())?;
}
Ok(())
}
}
impl AtomOrQuoted {
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
match self {
AtomOrQuoted::Atom(atom) => {
writer.write_all(atom.as_bytes())?;
}
AtomOrQuoted::Quoted(quoted) => {
writer.write_all(b"\"")?;
writer.write_all(escape_quoted(quoted).as_bytes())?;
writer.write_all(b"\"")?;
}
}
Ok(())
}
}
// -------------------------------------------------------------------------------------------------
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Greeting { pub struct Greeting {
pub domain: String, pub domain: String,
@ -289,7 +279,146 @@ pub struct Greeting {
pub struct EhloOkResp { pub struct EhloOkResp {
pub domain: String, pub domain: String,
pub greet: Option<String>, pub greet: Option<String>,
pub lines: Vec<EhloLine>, pub lines: Vec<Capability>,
} }
pub type EhloLine = (String, Vec<String>); #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Capability {
// Send as mail [RFC821]
// The description of SEND was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
// SEND,
// Send as mail or to terminal [RFC821]
// The description of SOML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
// SOML,
// Send as mail and to terminal [RFC821]
// The description of SAML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
// SAML,
// Interchange the client and server roles [RFC821]
// The actual use of TURN was deprecated in [RFC2821]
// TURN,
// SMTP Responsible Submitter [RFC4405]
// Deprecated by [https://datatracker.ietf.org/doc/status-change-change-sender-id-to-historic].
// SUBMITTER,
// Internationalized email address [RFC5336]
// Experimental; deprecated in [RFC6531].
// UTF8SMTP,
// ---------------------------------------------------------------------------------------------
/// Verbose [Eric Allman]
// VERB,
/// One message transaction only [Eric Allman]
// ONEX,
// ---------------------------------------------------------------------------------------------
/// Expand the mailing list [RFC821]
/// Command description updated by [RFC5321]
EXPN,
/// Supply helpful information [RFC821]
/// Command description updated by [RFC5321]
Help,
/// SMTP and Submit transport of 8bit MIME content [RFC6152]
EightBitMIME,
/// Message size declaration [RFC1870]
Size(u32),
/// Chunking [RFC3030]
Chunking,
/// Binary MIME [RFC3030]
BinaryMIME,
/// Checkpoint/Restart [RFC1845]
Checkpoint,
/// Deliver By [RFC2852]
DeliverBy,
/// Command Pipelining [RFC2920]
Pipelining,
/// Delivery Status Notification [RFC3461]
DSN,
/// Extended Turn [RFC1985]
/// SMTP [RFC5321] only. Not for use on Submit port 587.
ETRN,
/// Enhanced Status Codes [RFC2034]
EnhancedStatusCodes,
/// Start TLS [RFC3207]
StartTLS,
/// Notification of no soliciting [RFC3865]
// NoSoliciting,
/// Message Tracking [RFC3885]
MTRK,
/// Authenticated TURN [RFC2645]
/// SMTP [RFC5321] only. Not for use on Submit port 587.
ATRN,
/// Authentication [RFC4954]
Auth(Vec<AuthMechanism>),
/// Remote Content [RFC4468]
/// Submit [RFC6409] only. Not for use with SMTP on port 25.
BURL,
/// Future Message Release [RFC4865]
// FutureRelease,
/// Content Conversion Permission [RFC4141]
// ConPerm,
/// Content Conversion Negotiation [RFC4141]
// ConNeg,
/// Internationalized email address [RFC6531]
SMTPUTF8,
/// Priority Message Handling [RFC6710]
// MTPRIORITY,
/// Require Recipient Valid Since [RFC7293]
RRVS,
/// Require TLS [RFC8689]
RequireTLS,
// Observed ...
// TIME,
// XACK,
// VERP,
// VRFY,
/// Other
Other {
keyword: String,
params: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthMechanism {
Plain,
Login,
GSSAPI,
CramMD5,
CramSHA1,
ScramMD5,
DigestMD5,
NTLM,
Other(String),
}

29
src/utils.rs Normal file
View File

@ -0,0 +1,29 @@
use std::borrow::Cow;
pub(crate) fn escape_quoted(unescaped: &str) -> Cow<str> {
let mut escaped = Cow::Borrowed(unescaped);
if escaped.contains('\\') {
escaped = Cow::Owned(escaped.replace("\\", "\\\\"));
}
if escaped.contains('\"') {
escaped = Cow::Owned(escaped.replace("\"", "\\\""));
}
escaped
}
pub(crate) fn unescape_quoted(escaped: &str) -> Cow<str> {
let mut unescaped = Cow::Borrowed(escaped);
if unescaped.contains("\\\\") {
unescaped = Cow::Owned(unescaped.replace("\\\\", "\\"));
}
if unescaped.contains("\\\"") {
unescaped = Cow::Owned(unescaped.replace("\\\"", "\""));
}
unescaped
}