diff --git a/src/parse/response.rs b/src/parse/response.rs index b3938ca..bbf4570 100644 --- a/src/parse/response.rs +++ b/src/parse/response.rs @@ -1,6 +1,6 @@ use crate::{ parse::{address::address_literal, number, Domain}, - types::{AuthMechanism, Capability, EhloOkResp, Greeting as GreetingType}, + types::{AuthMechanism, Capability, Response}, }; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP}; use nom::{ @@ -16,7 +16,7 @@ use nom::{ /// ( "220-" (Domain / address-literal) [ SP textstring ] CRLF /// *( "220-" [ textstring ] CRLF ) /// "220" [ SP textstring ] CRLF ) -pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> { +pub fn Greeting(input: &[u8]) -> IResult<&[u8], Response> { let mut parser = alt(( map( tuple(( @@ -25,7 +25,7 @@ pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> { opt(preceded(SP, textstring)), CRLF, )), - |(_, domain, maybe_text, _)| GreetingType { + |(_, domain, maybe_text, _)| Response::Greeting { domain: domain.to_owned(), text: maybe_text .map(|str| str.to_string()) @@ -43,7 +43,7 @@ pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> { opt(preceded(SP, textstring)), CRLF, )), - |(_, domain, maybe_text, _, more_text, _, moar_text, _)| GreetingType { + |(_, domain, maybe_text, _, more_text, _, moar_text, _)| Response::Greeting { domain: domain.to_owned(), text: { let mut res = maybe_text @@ -88,20 +88,53 @@ pub fn textstring(input: &[u8]) -> IResult<&[u8], &str> { // ------------------------------------------------------------------------------------------------- +/// Reply-line = *( Reply-code "-" [ textstring ] CRLF ) +/// Reply-code [ SP textstring ] CRLF +pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> { + let parser = tuple(( + many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))), + Reply_code, + opt(tuple((SP, textstring))), + CRLF, + )); + + let (remaining, parsed) = recognize(parser)(input)?; + + Ok((remaining, parsed)) +} + +/// Reply-code = %x32-35 %x30-35 %x30-39 +/// +/// 2345 +/// 012345 +/// 0123456789 +pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> { + // FIXME: do not accept all codes. + map_res( + map_res( + take_while_m_n(3, 3, nom::character::is_digit), + std::str::from_utf8, + ), + |s| u16::from_str_radix(s, 10), + )(input) +} + +// ------------------------------------------------------------------------------------------------- + /// ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF ) / /// ( "250-" Domain [ SP ehlo-greet ] CRLF /// *( "250-" ehlo-line CRLF ) /// "250 " ehlo-line CRLF ) /// /// Edit: collapsed ("250" SP) to ("250 ") -pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> { +pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], Response> { let mut parser = alt(( map( tuple((tag(b"250 "), Domain, opt(preceded(SP, ehlo_greet)), CRLF)), - |(_, domain, maybe_ehlo, _)| EhloOkResp { + |(_, domain, maybe_ehlo, _)| Response::Ehlo { domain: domain.to_owned(), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), - lines: Vec::new(), + capabilities: Vec::new(), }, ), map( @@ -115,10 +148,10 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> { ehlo_line, CRLF, )), - |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| EhloOkResp { + |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| Response::Ehlo { domain: domain.to_owned(), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), - lines: { + capabilities: { lines.push(line); lines }, @@ -240,37 +273,6 @@ pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> { // ------------------------------------------------------------------------------------------------- -/// Reply-line = *( Reply-code "-" [ textstring ] CRLF ) -/// Reply-code [ SP textstring ] CRLF -pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> { - let parser = tuple(( - many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))), - Reply_code, - opt(tuple((SP, textstring))), - CRLF, - )); - - let (remaining, parsed) = recognize(parser)(input)?; - - Ok((remaining, parsed)) -} - -/// Reply-code = %x32-35 %x30-35 %x30-39 -/// -/// 2345 -/// 012345 -/// 0123456789 -pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> { - // FIXME: do not accept all codes. - map_res( - map_res( - take_while_m_n(3, 3, nom::character::is_digit), - std::str::from_utf8, - ), - |s| u16::from_str_radix(s, 10), - )(input) -} - #[cfg(test)] mod test { use super::*; @@ -286,7 +288,7 @@ mod test { assert_eq!(rem, b""); assert_eq!( out, - GreetingType { + Response::Greeting { domain: "example.org".into(), text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\ We do not authorize the use of this system to transport unsolicited,\n\ @@ -310,10 +312,10 @@ and/or bulk e-mail." assert_eq!(rem, b""); assert_eq!( out, - EhloOkResp { + Response::Ehlo { domain: "example.org".into(), greet: Some("hello".into()), - lines: vec![ + capabilities: vec![ Capability::Auth(vec![ AuthMechanism::Login, AuthMechanism::CramMD5, diff --git a/src/types.rs b/src/types.rs index 1db9f3f..29fe61b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -269,55 +269,121 @@ impl AtomOrQuoted { // ------------------------------------------------------------------------------------------------- +#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Greeting { - pub domain: String, - // TODO: Vec> would be closer to the SMTP ABNF. - // What is wrong with you, SMTP? - pub text: String, +pub enum Response { + Greeting { + domain: String, + text: String, + }, + Ehlo { + domain: String, + greet: Option, + capabilities: Vec, + }, + Other { + code: u16, + text: String, + }, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EhloOkResp { - pub domain: String, - pub greet: Option, - pub lines: Vec, -} +impl Response { + pub fn greeting(domain: D, text: T) -> Response + where + D: Into, + T: Into, + { + Response::Greeting { + domain: domain.into(), + text: text.into(), + } + } -impl EhloOkResp { - pub fn new(domain: D, greet: Option, capabilities: Vec) -> EhloOkResp + pub fn ehlo(domain: D, greet: Option, capabilities: Vec) -> Response where D: Into, G: Into, { - EhloOkResp { + Response::Ehlo { domain: domain.into(), - greet: greet.map(|inner| inner.into()), - lines: capabilities, + greet: greet.map(Into::into), + capabilities, + } + } + + pub fn other(code: u16, text: T) -> Response + where + T: Into, + { + Response::Other { + code, + text: text.into(), } } pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> { - let greet = match self.greet { - Some(ref greet) => format!(" {}", greet), - None => "".to_string(), - }; + match self { + Response::Greeting { domain, text } => { + let lines = text.lines().collect::>(); - if let Some((tail, head)) = self.lines.split_last() { - writer.write_all(format!("250-{}{}\r\n", self.domain, greet).as_bytes())?; + if let Some((first, tail)) = lines.split_first() { + if let Some((last, head)) = tail.split_last() { + write!(writer, "220-{} {}\r\n", domain, first)?; - for capability in head { - writer.write_all(b"250-")?; - capability.serialize(writer)?; - writer.write_all(b"\r\n")?; + for line in head { + write!(writer, "220-{}\r\n", line)?; + } + + write!(writer, "220 {}\r\n", last)?; + } else { + write!(writer, "220 {} {}\r\n", domain, first)?; + } + } else { + write!(writer, "220 {}\r\n", domain)?; + } } + Response::Ehlo { + domain, + greet, + capabilities, + } => { + let greet = match greet { + Some(greet) => format!(" {}", greet), + None => "".to_string(), + }; - writer.write_all(b"250 ")?; - tail.serialize(writer)?; - writer.write_all(b"\r\n") - } else { - writer.write_all(format!("250 {}{}\r\n", self.domain, greet).as_bytes()) + if let Some((tail, head)) = capabilities.split_last() { + writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?; + + for capability in head { + writer.write_all(b"250-")?; + capability.serialize(writer)?; + writer.write_all(b"\r\n")?; + } + + writer.write_all(b"250 ")?; + tail.serialize(writer)?; + writer.write_all(b"\r\n")?; + } else { + writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?; + } + } + Response::Other { code, text } => { + let lines = text.lines().collect::>(); + + if let Some((last, head)) = lines.split_last() { + for line in head { + write!(writer, "{}-{}\r\n", code, line)?; + } + + write!(writer, "{} {}\r\n", code, last)?; + } else { + write!(writer, "{}\r\n", code)?; + } + } } + + Ok(()) } } @@ -538,3 +604,125 @@ impl AuthMechanism { } } } + +#[cfg(test)] +mod tests { + use crate::types::{Capability, Response}; + + #[test] + fn test_serialize_greeting() { + let tests = &[ + ( + Response::Greeting { + domain: "example.org".into(), + text: "".into(), + }, + b"220 example.org\r\n".as_ref(), + ), + ( + Response::Greeting { + domain: "example.org".into(), + text: "A".into(), + }, + b"220 example.org A\r\n".as_ref(), + ), + ( + Response::Greeting { + domain: "example.org".into(), + text: "A\nB".into(), + }, + b"220-example.org A\r\n220 B\r\n".as_ref(), + ), + ( + Response::Greeting { + domain: "example.org".into(), + text: "A\nB\nC".into(), + }, + b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(), + ), + ]; + + for (test, expected) in tests.into_iter() { + let mut got = Vec::new(); + test.serialize(&mut got).unwrap(); + assert_eq!(expected, &got); + } + } + + #[test] + fn test_serialize_ehlo() { + let tests = &[ + ( + Response::Ehlo { + domain: "example.org".into(), + greet: None, + capabilities: vec![], + }, + b"250 example.org\r\n".as_ref(), + ), + ( + Response::Ehlo { + domain: "example.org".into(), + greet: Some("...".into()), + capabilities: vec![], + }, + b"250 example.org ...\r\n".as_ref(), + ), + ( + Response::Ehlo { + domain: "example.org".into(), + greet: Some("...".into()), + capabilities: vec![Capability::StartTLS], + }, + b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(), + ), + ( + Response::Ehlo { + domain: "example.org".into(), + greet: Some("...".into()), + capabilities: vec![Capability::StartTLS, Capability::Size(12345)], + }, + b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(), + ), + ]; + + for (test, expected) in tests.into_iter() { + let mut got = Vec::new(); + test.serialize(&mut got).unwrap(); + assert_eq!(expected, &got); + } + } + + #[test] + fn test_serialize_other() { + let tests = &[ + ( + Response::Other { + code: 333, + text: "".into(), + }, + b"333\r\n".as_ref(), + ), + ( + Response::Other { + code: 333, + text: "A".into(), + }, + b"333 A\r\n".as_ref(), + ), + ( + Response::Other { + code: 333, + text: "A\nB".into(), + }, + b"333-A\r\n333 B\r\n".as_ref(), + ), + ]; + + for (test, expected) in tests.into_iter() { + let mut got = Vec::new(); + test.serialize(&mut got).unwrap(); + assert_eq!(expected, &got); + } + } +}