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