Try to create a useful `Response` struct.

This commit is contained in:
Damian Poddebniak 2021-05-23 19:26:16 +02:00
parent ad3a164d19
commit fcd8ee93ff
2 changed files with 264 additions and 74 deletions

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
parse::{address::address_literal, number, Domain}, 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 abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
use nom::{ use nom::{
@ -16,7 +16,7 @@ use nom::{
/// ( "220-" (Domain / address-literal) [ SP textstring ] CRLF /// ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
/// *( "220-" [ textstring ] CRLF ) /// *( "220-" [ textstring ] CRLF )
/// "220" [ SP 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(( let mut parser = alt((
map( map(
tuple(( tuple((
@ -25,7 +25,7 @@ pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> {
opt(preceded(SP, textstring)), opt(preceded(SP, textstring)),
CRLF, CRLF,
)), )),
|(_, domain, maybe_text, _)| GreetingType { |(_, domain, maybe_text, _)| Response::Greeting {
domain: domain.to_owned(), domain: domain.to_owned(),
text: maybe_text text: maybe_text
.map(|str| str.to_string()) .map(|str| str.to_string())
@ -43,7 +43,7 @@ pub fn Greeting(input: &[u8]) -> IResult<&[u8], GreetingType> {
opt(preceded(SP, textstring)), opt(preceded(SP, textstring)),
CRLF, CRLF,
)), )),
|(_, domain, maybe_text, _, more_text, _, moar_text, _)| GreetingType { |(_, domain, maybe_text, _, more_text, _, moar_text, _)| Response::Greeting {
domain: domain.to_owned(), domain: domain.to_owned(),
text: { text: {
let mut res = maybe_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 ) / /// ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF ) /
/// ( "250-" Domain [ SP ehlo-greet ] CRLF /// ( "250-" Domain [ SP ehlo-greet ] CRLF
/// *( "250-" ehlo-line CRLF ) /// *( "250-" ehlo-line CRLF )
/// "250 " ehlo-line CRLF ) /// "250 " ehlo-line CRLF )
/// ///
/// Edit: collapsed ("250" SP) to ("250 ") /// 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(( let mut parser = alt((
map( map(
tuple((tag(b"250 "), Domain, opt(preceded(SP, ehlo_greet)), CRLF)), tuple((tag(b"250 "), Domain, opt(preceded(SP, ehlo_greet)), CRLF)),
|(_, domain, maybe_ehlo, _)| EhloOkResp { |(_, domain, maybe_ehlo, _)| Response::Ehlo {
domain: domain.to_owned(), domain: domain.to_owned(),
greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
lines: Vec::new(), capabilities: Vec::new(),
}, },
), ),
map( map(
@ -115,10 +148,10 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> {
ehlo_line, ehlo_line,
CRLF, CRLF,
)), )),
|(_, domain, maybe_ehlo, _, mut lines, _, line, _)| EhloOkResp { |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| Response::Ehlo {
domain: domain.to_owned(), domain: domain.to_owned(),
greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
lines: { capabilities: {
lines.push(line); lines.push(line);
lines 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -286,7 +288,7 @@ mod test {
assert_eq!(rem, b""); assert_eq!(rem, b"");
assert_eq!( assert_eq!(
out, out,
GreetingType { Response::Greeting {
domain: "example.org".into(), domain: "example.org".into(),
text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\ 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\ 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!(rem, b"");
assert_eq!( assert_eq!(
out, out,
EhloOkResp { Response::Ehlo {
domain: "example.org".into(), domain: "example.org".into(),
greet: Some("hello".into()), greet: Some("hello".into()),
lines: vec![ capabilities: vec![
Capability::Auth(vec![ Capability::Auth(vec![
AuthMechanism::Login, AuthMechanism::Login,
AuthMechanism::CramMD5, AuthMechanism::CramMD5,

View File

@ -269,55 +269,121 @@ impl AtomOrQuoted {
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Greeting { pub enum Response {
pub domain: String, Greeting {
// TODO: Vec<Option<String>> would be closer to the SMTP ABNF. domain: String,
// What is wrong with you, SMTP? text: String,
pub text: String, },
Ehlo {
domain: String,
greet: Option<String>,
capabilities: Vec<Capability>,
},
Other {
code: u16,
text: String,
},
} }
#[derive(Debug, Clone, PartialEq, Eq)] impl Response {
pub struct EhloOkResp { pub fn greeting<D, T>(domain: D, text: T) -> Response
pub domain: String, where
pub greet: Option<String>, D: Into<String>,
pub lines: Vec<Capability>, T: Into<String>,
} {
Response::Greeting {
domain: domain.into(),
text: text.into(),
}
}
impl EhloOkResp { pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
pub fn new<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> EhloOkResp
where where
D: Into<String>, D: Into<String>,
G: Into<String>, G: Into<String>,
{ {
EhloOkResp { Response::Ehlo {
domain: domain.into(), domain: domain.into(),
greet: greet.map(|inner| inner.into()), greet: greet.map(Into::into),
lines: capabilities, capabilities,
}
}
pub fn other<T>(code: u16, text: T) -> Response
where
T: Into<String>,
{
Response::Other {
code,
text: text.into(),
} }
} }
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> { pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
let greet = match self.greet { match self {
Some(ref greet) => format!(" {}", greet), Response::Greeting { domain, text } => {
None => "".to_string(), let lines = text.lines().collect::<Vec<_>>();
};
if let Some((tail, head)) = self.lines.split_last() { if let Some((first, tail)) = lines.split_first() {
writer.write_all(format!("250-{}{}\r\n", self.domain, greet).as_bytes())?; if let Some((last, head)) = tail.split_last() {
write!(writer, "220-{} {}\r\n", domain, first)?;
for capability in head { for line in head {
writer.write_all(b"250-")?; write!(writer, "220-{}\r\n", line)?;
capability.serialize(writer)?; }
writer.write_all(b"\r\n")?;
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 ")?; if let Some((tail, head)) = capabilities.split_last() {
tail.serialize(writer)?; writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
writer.write_all(b"\r\n")
} else { for capability in head {
writer.write_all(format!("250 {}{}\r\n", self.domain, greet).as_bytes()) 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::<Vec<_>>();
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);
}
}
}