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 types;
pub fn escape(bytes: &[u8]) -> String {
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("")
}
mod utils;

View File

@ -1,17 +1,38 @@
//! 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 nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case, take_while1, take_while_m_n},
character::is_hex_digit,
combinator::{opt, recognize},
combinator::{map_res, opt, recognize},
multi::{count, many_m_n},
sequence::tuple,
sequence::{delimited, tuple},
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)
pub fn IPv4_address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
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))
}
/// 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
///
/// Snum = 1*3DIGIT
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)?;
@ -77,15 +67,6 @@ pub fn IPv6_addr(input: &[u8]) -> IResult<&[u8], &[u8]> {
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)
pub fn IPv6_full(input: &[u8]) -> IResult<&[u8], &[u8]> {
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))
}
/// 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.
/// 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))
}
/// 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::{
parse::{
address::{General_address_literal, IPv4_address_literal, IPv6_address_literal},
base64,
imf::atom::is_atext,
},
types::Command,
parse::{address::address_literal, base64, Atom, Domain, Quoted_string, String},
types::{Command, Parameter},
};
use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, DQUOTE, SP};
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_res, opt, recognize},
multi::many0,
combinator::{map, map_res, opt, recognize, value},
multi::separated_list1,
sequence::{delimited, preceded, tuple},
IResult,
};
pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
let mut parser = alt((
alt((
helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit,
starttls, // Extensions
auth_login, // https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf
auth_plain, // RFC 4616
));
let (remaining, parsed) = parser(input)?;
Ok((remaining, parsed))
))(input)
}
/// helo = "HELO" SP Domain CRLF
@ -83,11 +75,49 @@ pub fn mail(input: &[u8]) -> IResult<&[u8], Command> {
remaining,
Command::Mail {
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
///
/// 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:"),
opt(SP), // Out-of-Spec, but Outlook does it ...
alt((
recognize(tuple((tag_no_case(b"<Postmaster@"), Domain, tag(b">")))),
tag_no_case(b"<Postmaster>"),
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)),
@ -112,27 +145,24 @@ pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
remaining,
Command::Rcpt {
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
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))
value(Command::Data, tuple((tag_no_case(b"DATA"), CRLF)))(input)
}
/// rset = "RSET" CRLF
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))
value(Command::Rset, tuple((tag_no_case(b"RSET"), CRLF)))(input)
}
/// vrfy = "VRFY" SP String CRLF
@ -144,7 +174,7 @@ pub fn vrfy(input: &[u8]) -> IResult<&[u8], Command> {
Ok((
remaining,
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)?;
Ok((
remaining,
Command::Expn {
mailing_list: data.into(),
},
))
Ok((remaining, Command::Expn { mailing_list: data }))
}
/// help = "HELP" [ SP String ] CRLF
@ -172,7 +197,7 @@ pub fn help(input: &[u8]) -> IResult<&[u8], Command> {
Ok((
remaining,
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((
remaining,
Command::Noop {
argument: maybe_data.map(|data| data.into()),
argument: maybe_data,
},
))
}
/// quit = "QUIT" CRLF
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))
value(Command::Quit, tuple((tag_no_case(b"QUIT"), CRLF)))(input)
}
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))
value(Command::StartTLS, tuple((tag_no_case(b"STARTTLS"), CRLF)))(input)
}
/// 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) -----
/// Reverse-path = Path / "<>"
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 Reverse_path(input: &[u8]) -> IResult<&[u8], &str> {
alt((Path, value("", tag("<>"))))(input)
}
/// Forward-path = Path
pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = Path;
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &str> {
Path(input)
}
// Path = "<" [ A-d-l ":" ] Mailbox ">"
pub fn Path(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = tuple((
pub fn Path(input: &[u8]) -> IResult<&[u8], &str> {
delimited(
tag(b"<"),
opt(tuple((A_d_l, tag(b":")))),
Mailbox,
map_res(
recognize(tuple((opt(tuple((A_d_l, tag(b":")))), Mailbox))),
std::str::from_utf8,
),
tag(b">"),
));
let (remaining, parsed) = recognize(parser)(input)?;
Ok((remaining, parsed))
)(input)
}
/// 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
/// ; generated, and SHOULD be ignored.
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)?;
@ -308,133 +315,6 @@ pub fn At_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
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 )
pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> {
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
/// ; MAY be case-sensitive
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))
alt((recognize(Dot_string), recognize(Quoted_string)))(input)
}
/// Dot-string = Atom *("." Atom)
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 Dot_string(input: &[u8]) -> IResult<&[u8], &str> {
map_res(
recognize(separated_list1(tag(b"."), Atom)),
std::str::from_utf8,
)(input)
}
/// Atom = 1*atext
pub fn Atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(is_atext)(input)
}
// Not used?
/// Keyword = Ldh-str
//pub fn Keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
// Ldh_str(input)
//}
/// Quoted-string = DQUOTE *QcontentSMTP DQUOTE
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))
}
/// 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))
}
// Not used?
/// Argument = Atom
//pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
// Atom(input)
//}
#[cfg(test)]
mod test {
use super::{ehlo, helo, mail, sub_domain};
use super::{ehlo, helo, mail};
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()
fqdn_or_address_literal: "123.123.123.123".into()
}
);
assert_eq!(rem, b"???");
@ -548,7 +373,7 @@ mod test {
assert_eq!(
parsed,
Command::Helo {
fqdn_or_address_literal: b"example.com".to_vec()
fqdn_or_address_literal: "example.com".into()
}
);
assert_eq!(rem, b"???");
@ -560,8 +385,8 @@ mod test {
assert_eq!(
parsed,
Command::Mail {
reverse_path: b"<userx@y.foo.org>".to_vec(),
parameters: None
reverse_path: "userx@y.foo.org".into(),
parameters: Vec::default(),
}
);
assert_eq!(rem, b"???");

View File

@ -1,15 +1,17 @@
#![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::{
branch::alt,
bytes::streaming::{tag, take_while},
bytes::streaming::{tag, take_while, take_while1, take_while_m_n},
character::streaming::digit1,
combinator::{map_res, opt, recognize},
sequence::tuple,
combinator::{map, map_res, opt, recognize},
multi::{many0, separated_list1},
sequence::{delimited, tuple},
IResult,
};
use std::str::from_utf8;
use std::{borrow::Cow, str::from_utf8};
pub mod address;
pub mod command;
@ -19,10 +21,6 @@ pub mod response;
pub mod trace;
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> {
let mut parser = map_res(
recognize(tuple((
@ -37,6 +35,128 @@ pub fn base64(input: &[u8]) -> IResult<&[u8], &str> {
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> {
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)
use crate::{
parse::command::{address_literal, Domain},
parse::{address::address_literal, Domain},
types::Greeting as GreetingType,
};
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 nom::{
branch::alt,
bytes::streaming::{tag, take_while, take_while1, take_while_m_n},
combinator::{map, map_res, opt, recognize},
bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
combinator::{map, map_res, opt, recognize, value},
multi::{many0, separated_list0},
sequence::{delimited, preceded, tuple},
IResult,
@ -36,27 +39,11 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> {
ehlo_line,
CRLF,
)),
|(_, domain, maybe_ehlo, _, lines, _, (keyword, params), _)| EhloOkResp {
|(_, domain, maybe_ehlo, _, mut lines, _, line, _)| 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.push(line);
lines
},
},
@ -82,8 +69,14 @@ pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> {
/// ehlo-line = ehlo-keyword *( 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 mut parser = tuple((
pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], Capability> {
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),
opt(preceded(
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
@ -125,6 +165,7 @@ pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> {
#[cfg(test)]
mod test {
use super::*;
use crate::types::AuthMechanism;
#[test]
fn test_ehlo_ok_rsp() {
@ -144,17 +185,19 @@ mod test {
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![]),
Capability::Auth(vec![
AuthMechanism::Login,
AuthMechanism::CramMD5,
AuthMechanism::Plain
]),
Capability::Auth(vec![
AuthMechanism::Login,
AuthMechanism::CramMD5,
AuthMechanism::Plain
]),
Capability::StartTLS,
Capability::Size(12345),
Capability::EightBitMIME,
],
}
);
@ -162,9 +205,8 @@ mod test {
#[test]
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!(keyword, "SIZE");
assert_eq!(params, &["123456"]);
assert_eq!(capability, Capability::Size(123456));
}
}

View File

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

View File

@ -1,21 +1,21 @@
use crate::escape;
use crate::utils::escape_quoted;
use std::io::Write;
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Command {
Ehlo {
fqdn_or_address_literal: Vec<u8>,
fqdn_or_address_literal: String,
},
Helo {
fqdn_or_address_literal: Vec<u8>,
fqdn_or_address_literal: String,
},
Mail {
reverse_path: Vec<u8>,
parameters: Option<Vec<u8>>,
reverse_path: String,
parameters: Vec<Parameter>,
},
Rcpt {
forward_path: Vec<u8>,
parameters: Option<Vec<u8>>,
forward_path: String,
parameters: Vec<Parameter>,
},
Data,
Rset,
@ -26,7 +26,7 @@ pub enum Command {
/// This command has no effect on the reverse-path buffer, the forward-
/// path buffer, or the mail data buffer.
Vrfy {
user_or_mailbox: Vec<u8>,
user_or_mailbox: AtomOrQuoted,
},
/// This command asks the receiver to confirm that the argument
/// 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
/// time.
Expn {
mailing_list: Vec<u8>,
mailing_list: AtomOrQuoted,
},
/// This command causes the server to send helpful information to the
/// 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
/// time.
Help {
argument: Option<Vec<u8>>,
argument: Option<AtomOrQuoted>,
},
/// This command does not affect any parameters or previously entered
/// 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
/// time.
Noop {
argument: Option<Vec<u8>>,
argument: Option<AtomOrQuoted>,
},
/// This command specifies that the receiver MUST send a "221 OK" reply,
/// and then close the transmission channel.
@ -90,6 +90,18 @@ pub enum Command {
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 {
pub fn name(&self) -> &'static str {
match self {
@ -112,60 +124,7 @@ impl Command {
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<()> {
use Command::*;
@ -175,50 +134,42 @@ impl Command {
fqdn_or_address_literal,
} => {
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 {
fqdn_or_address_literal,
} => {
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 {
reverse_path,
parameters: None,
parameters,
} => {
writer.write_all(b"MAIL FROM:<")?;
writer.write_all(reverse_path)?;
writer.write_all(reverse_path.as_bytes())?;
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 {
forward_path,
parameters: None,
parameters,
} => {
writer.write_all(b"RCPT TO:<")?;
writer.write_all(forward_path)?;
writer.write_all(forward_path.as_bytes())?;
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 => writer.write_all(b"DATA")?,
@ -227,12 +178,12 @@ impl Command {
// vrfy = "VRFY" SP String CRLF
Vrfy { user_or_mailbox } => {
writer.write_all(b"VRFY ")?;
writer.write_all(user_or_mailbox)?;
user_or_mailbox.serialize(writer)?;
}
// expn = "EXPN" SP String CRLF
Expn { mailing_list } => {
writer.write_all(b"EXPN ")?;
writer.write_all(mailing_list)?;
mailing_list.serialize(writer)?;
}
// help = "HELP" [ SP String ] CRLF
Help { argument: None } => writer.write_all(b"HELP")?,
@ -240,7 +191,7 @@ impl Command {
argument: Some(data),
} => {
writer.write_all(b"HELP ")?;
writer.write_all(data)?;
data.serialize(writer)?;
}
// noop = "NOOP" [ SP String ] CRLF
Noop { argument: None } => writer.write_all(b"NOOP")?,
@ -248,7 +199,7 @@ impl Command {
argument: Some(data),
} => {
writer.write_all(b"NOOP ")?;
writer.write_all(data)?;
data.serialize(writer)?;
}
// quit = "QUIT" CRLF
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)]
pub struct Greeting {
pub domain: String,
@ -289,7 +279,146 @@ pub struct Greeting {
pub struct EhloOkResp {
pub domain: 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
}