Try to create a useful `Response` struct.
This commit is contained in:
parent
ad3a164d19
commit
fcd8ee93ff
|
@ -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,
|
||||
|
|
250
src/types.rs
250
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue