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::{
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,

View File

@ -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<Option<String>> 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<String>,
capabilities: Vec<Capability>,
},
Other {
code: u16,
text: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloOkResp {
pub domain: String,
pub greet: Option<String>,
pub lines: Vec<Capability>,
}
impl Response {
pub fn greeting<D, T>(domain: D, text: T) -> Response
where
D: Into<String>,
T: Into<String>,
{
Response::Greeting {
domain: domain.into(),
text: text.into(),
}
}
impl EhloOkResp {
pub fn new<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> EhloOkResp
pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
where
D: Into<String>,
G: Into<String>,
{
EhloOkResp {
Response::Ehlo {
domain: domain.into(),
greet: greet.map(|inner| inner.into()),
lines: capabilities,
greet: greet.map(Into::into),
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<()> {
let greet = match self.greet {
Some(ref greet) => format!(" {}", greet),
None => "".to_string(),
};
match self {
Response::Greeting { domain, text } => {
let lines = text.lines().collect::<Vec<_>>();
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::<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);
}
}
}