From 10f614edb6a881edf6aca94e6cba653765685a78 Mon Sep 17 00:00:00 2001 From: Damian Poddebniak Date: Sat, 15 May 2021 19:28:22 +0200 Subject: [PATCH] Too much, sorry. --- src/lib.rs | 22 +-- src/parse/address.rs | 107 ++++++------ src/parse/command.rs | 371 +++++++++++------------------------------- src/parse/mod.rs | 138 +++++++++++++++- src/parse/replies.rs | 2 +- src/parse/response.rs | 120 +++++++++----- src/parse/trace.rs | 56 +++---- src/types.rs | 319 +++++++++++++++++++++++++----------- src/utils.rs | 29 ++++ 9 files changed, 649 insertions(+), 515 deletions(-) create mode 100644 src/utils.rs diff --git a/src/lib.rs b/src/lib.rs index 597653d..6d9fce6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::>() - .join("") -} +mod utils; diff --git a/src/parse/address.rs b/src/parse/address.rs index 2f9ed3b..0a7a4da 100644 --- a/src/parse/address.rs +++ b/src/parse/address.rs @@ -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) +} diff --git a/src/parse/command.rs b/src/parse/command.rs index b4178cf..5057228 100644 --- a/src/parse/command.rs +++ b/src/parse/command.rs @@ -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> { + 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:" ( "" / "" / 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"")))), - tag_no_case(b""), + map_res( + recognize(tuple((tag_no_case("")))), + std::str::from_utf8, + ), + map_res(tag_no_case(""), 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> { + 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"".to_vec(), - parameters: None + reverse_path: "userx@y.foo.org".into(), + parameters: Vec::default(), } ); assert_eq!(rem, b"???"); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index de552a9..1a6ea55 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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::)(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"???"); + } +} diff --git a/src/parse/replies.rs b/src/parse/replies.rs index e265f20..f358992 100644 --- a/src/parse/replies.rs +++ b/src/parse/replies.rs @@ -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}; diff --git a/src/parse/response.rs b/src/parse/response.rs index 3793df4..b142072 100644 --- a/src/parse/response.rs +++ b/src/parse/response.rs @@ -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::>(); - (keyword.to_string(), params) - }) - .collect::)>>(); - lines.push(( - keyword.to_string(), - params - .iter() - .map(|param| param.to_string()) - .collect::>(), - )); + 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)); } } diff --git a/src/parse/trace.rs b/src/parse/trace.rs index 6f5c7af..9a45241 100644 --- a/src/parse/trace.rs +++ b/src/parse/trace.rs @@ -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) } diff --git a/src/types.rs b/src/types.rs index 957193b..5c0cdf3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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, + fqdn_or_address_literal: String, }, Helo { - fqdn_or_address_literal: Vec, + fqdn_or_address_literal: String, }, Mail { - reverse_path: Vec, - parameters: Option>, + reverse_path: String, + parameters: Vec, }, Rcpt { - forward_path: Vec, - parameters: Option>, + forward_path: String, + parameters: Vec, }, 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, + 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, + 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>, + argument: Option, }, /// 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>, + argument: Option, }, /// 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), } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Parameter { + keyword: String, + value: Option, +} + +#[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">")?; - } - 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)?; + + for parameter in parameters { + writer.write_all(b" ")?; + parameter.serialize(writer)?; + } } // rcpt = "RCPT TO:" ( "" / "" / 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">")?; - } - 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)?; + + for parameter in parameters { + writer.write_all(b" ")?; + parameter.serialize(writer)?; + } } // 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, V: Into>(keyword: K, value: Option) -> 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, - pub lines: Vec, + pub lines: Vec, } -pub type EhloLine = (String, Vec); +#[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), + + /// 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, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthMechanism { + Plain, + Login, + GSSAPI, + + CramMD5, + CramSHA1, + ScramMD5, + DigestMD5, + NTLM, + + Other(String), +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..23cf475 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,29 @@ +use std::borrow::Cow; + +pub(crate) fn escape_quoted(unescaped: &str) -> Cow { + 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 { + let mut unescaped = Cow::Borrowed(escaped); + + if unescaped.contains("\\\\") { + unescaped = Cow::Owned(unescaped.replace("\\\\", "\\")); + } + + if unescaped.contains("\\\"") { + unescaped = Cow::Owned(unescaped.replace("\\\"", "\"")); + } + + unescaped +}