Enhances Status Code explanations.

This commit is contained in:
Mauro D 2022-12-05 15:09:17 +00:00
parent ee947670bd
commit e7df79ef68
7 changed files with 1323 additions and 71 deletions

View file

@ -6,3 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
[dev-dependencies]
csv = "1.1"

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
pub mod request; pub mod request;
pub mod response; pub mod response;
@ -220,16 +222,36 @@ pub enum MtPriority {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloResponse { pub struct EhloResponse<T: Display> {
pub hostname: String, pub hostname: T,
pub capabilities: Vec<Capability>, pub capabilities: Vec<Capability>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response { pub struct Response<T: Display> {
pub code: u16, pub code: [u8; 3],
pub esc: [u8; 3], pub esc: [u8; 3],
pub message: String, pub message: T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
PositiveCompletion = 2,
PositiveIntermediate = 3,
TransientNegativeCompletion = 4,
PermanentNegativeCompletion = 5,
Invalid = 0,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Syntax = 0,
Information = 1,
Connections = 2,
Unspecified3 = 3,
Unspecified4 = 4,
MailSystem = 5,
Invalid = 6,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -240,7 +262,7 @@ pub enum Error {
SyntaxError { syntax: &'static str }, SyntaxError { syntax: &'static str },
InvalidParameter { param: &'static str }, InvalidParameter { param: &'static str },
UnsupportedParameter { param: String }, UnsupportedParameter { param: String },
InvalidResponse { response: Response }, InvalidResponse { response: Response<String> },
} }
pub(crate) const LF: u8 = b'\n'; pub(crate) const LF: u8 = b'\n';
@ -256,3 +278,41 @@ impl IntoString for Vec<u8> {
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
} }
} }
#[cfg(test)]
mod tests {
#[test]
fn csv() {
// Build the CSV reader and iterate over each record.
let mut rdr = csv::Reader::from_path("smtp-enhanced-status-codes-1.csv").unwrap();
for result in rdr.records() {
// The iterator yields Result<StringRecord, Error>, so we check the
// error here.
let record = result.unwrap();
let codes = record.get(0).unwrap().split('.').collect::<Vec<_>>();
let title = record.get(1).unwrap().replace('\n', " ");
let desc = record
.get(2)
.unwrap()
.replace('\n', " ")
.replace('"', "\\\"")
.replace("This is useful only as a persistent transient error.", "")
.replace(
"This is useful for both permanent and persistent transient errors.",
"",
)
.replace("This is useful only as a permanent error.", "")
.trim()
.replace(" ", " ")
.chars()
.collect::<Vec<_>>()
.chunks(50)
.map(|s| format!("\"{}\"", s.iter().collect::<String>()))
.collect::<Vec<_>>()
.join(", ");
println!("{} => (\"{}\", concat!({})).into(),", codes[0], title, desc);
}
}
}

View file

@ -5,10 +5,10 @@ use crate::{
NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS, SP, NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS, SP,
}; };
use super::*; use super::{receiver::ReceiverParser, *};
impl Request<String> { impl ReceiverParser for Request<String> {
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> { fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
let mut parser = Rfc5321Parser::new(bytes); let mut parser = Rfc5321Parser::new(bytes);
let command = parser.hashed_value()?; let command = parser.hashed_value()?;
if !parser.stop_char.is_ascii_whitespace() { if !parser.stop_char.is_ascii_whitespace() {
@ -1137,8 +1137,8 @@ impl TryFrom<u128> for Body {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, NOTIFY_DELAY, request::receiver::ReceiverParser, Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter,
NOTIFY_FAILURE, NOTIFY_SUCCESS, Request, Ret, Rrvs, NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS,
}; };
#[test] #[test]

View file

@ -1,9 +1,14 @@
use std::slice::Iter; use std::{marker::PhantomData, slice::Iter};
use crate::{Error, Request}; use crate::Error;
pub struct RequestReceiver { pub struct Receiver<T: ReceiverParser + Sized> {
pub buf: Vec<u8>, pub buf: Vec<u8>,
_p: PhantomData<T>,
}
pub trait ReceiverParser: Sized {
fn parse(bytes: &mut Iter<'_, u8>) -> Result<Self, Error>;
} }
pub struct DataReceiver { pub struct DataReceiver {
@ -18,21 +23,19 @@ pub struct BdatReceiver {
bytes_left: usize, bytes_left: usize,
} }
impl RequestReceiver { impl<T: ReceiverParser> Default for Receiver<T> {
#[allow(clippy::new_without_default)] fn default() -> Self {
pub fn new() -> Self {
Self { Self {
buf: Vec::with_capacity(0), buf: Vec::with_capacity(0),
_p: Default::default(),
} }
} }
}
pub fn ingest( impl<T: ReceiverParser> Receiver<T> {
&mut self, pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &[u8]) -> Result<T, Error> {
bytes: &mut Iter<'_, u8>,
buf: &[u8],
) -> Result<Request<String>, Error> {
if self.buf.is_empty() { if self.buf.is_empty() {
match Request::parse(bytes) { match T::parse(bytes) {
Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => { Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => {
self.buf = buf[buf.len() - bytes_left..].to_vec(); self.buf = buf[buf.len() - bytes_left..].to_vec();
} }
@ -42,7 +45,7 @@ impl RequestReceiver {
for &ch in bytes { for &ch in bytes {
self.buf.push(ch); self.buf.push(ch);
if ch == b'\n' { if ch == b'\n' {
let result = Request::parse(&mut self.buf.iter()); let result = T::parse(&mut self.buf.iter());
self.buf.clear(); self.buf.clear();
return result; return result;
} }
@ -112,7 +115,7 @@ impl BdatReceiver {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{request::receiver::RequestReceiver, Error, Request}; use crate::{request::receiver::Receiver, Error, Request};
use super::DataReceiver; use super::DataReceiver;
@ -171,7 +174,7 @@ mod tests {
), ),
] { ] {
let mut requests = Vec::new(); let mut requests = Vec::new();
let mut r = RequestReceiver::new(); let mut r = Receiver::default();
for data in &data { for data in &data {
let mut bytes = data.as_bytes().iter(); let mut bytes = data.as_bytes().iter();
loop { loop {

1170
src/response/generate.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
pub mod generate;
pub mod parser; pub mod parser;
pub(crate) const _8BITMIME: u128 = (b'8' as u128) pub(crate) const _8BITMIME: u128 = (b'8' as u128)

View file

@ -1,14 +1,14 @@
use std::slice::Iter; use std::slice::Iter;
use crate::{ use crate::{
request::parser::Rfc5321Parser, Capability, EhloResponse, Error, IntoString, MtPriority, request::{parser::Rfc5321Parser, receiver::ReceiverParser},
Response, LF, Capability, EhloResponse, Error, IntoString, MtPriority, Response, LF,
}; };
use super::*; use super::*;
impl EhloResponse { impl ReceiverParser for EhloResponse<String> {
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<EhloResponse, Error> { fn parse(bytes: &mut Iter<'_, u8>) -> Result<EhloResponse<String>, Error> {
let mut parser = Rfc5321Parser::new(bytes); let mut parser = Rfc5321Parser::new(bytes);
let mut response = EhloResponse { let mut response = EhloResponse {
hostname: String::new(), hostname: String::new(),
@ -16,17 +16,29 @@ impl EhloResponse {
}; };
let mut eol = false; let mut eol = false;
let mut buf = Vec::with_capacity(32); let mut buf = Vec::with_capacity(32);
let mut code = u16::MAX; let mut code = [0u8; 3];
let mut is_first_line = true; let mut is_first_line = true;
let mut did_success = false;
while !eol { while !eol {
code = parser.size()? as u16; for code in code.iter_mut() {
match parser.stop_char { match parser.read_char()? {
ch @ b'0'..=b'9' => {
*code = ch - b'0';
}
_ => {
return Err(Error::SyntaxError {
syntax: "unexpected token",
});
}
}
}
match parser.read_char()? {
b' ' => { b' ' => {
eol = true; eol = true;
} }
b'-' => (), b'-' => (),
b'\n' if code < 600 => { b'\n' if code[0] < 6 => {
break; break;
} }
_ => { _ => {
@ -36,7 +48,9 @@ impl EhloResponse {
} }
} }
if !is_first_line && code == 250 { did_success = code[0] == 2 && code[1] == 5 && code[2] == 0;
if !is_first_line && did_success {
response response
.capabilities .capabilities
.push(match parser.hashed_value_long()? { .push(match parser.hashed_value_long()? {
@ -157,18 +171,17 @@ impl EhloResponse {
}); });
parser.seek_lf()?; parser.seek_lf()?;
} else { } else {
let is_hostname = code == 250;
if is_first_line { if is_first_line {
is_first_line = false; is_first_line = false;
} else if !buf.is_empty() && !matches!(buf.last(), Some(b' ')) { } else if !buf.is_empty() {
buf.push(b' '); buf.extend_from_slice(b"\r\n");
} }
loop { loop {
match parser.read_char()? { match parser.read_char()? {
b'\n' => break, b'\n' => break,
b'\r' => (), b'\r' => (),
b' ' if is_hostname => { b' ' if did_success => {
parser.seek_lf()?; parser.seek_lf()?;
break; break;
} }
@ -178,14 +191,14 @@ impl EhloResponse {
} }
} }
if is_hostname { if did_success {
response.hostname = buf.into_string(); response.hostname = buf.into_string();
buf = Vec::new(); buf = Vec::new();
} }
} }
} }
if code == 250 { if did_success {
Ok(response) Ok(response)
} else { } else {
Err(Error::InvalidResponse { Err(Error::InvalidResponse {
@ -199,25 +212,33 @@ impl EhloResponse {
} }
} }
impl Response { impl Response<String> {
pub fn parse(bytes: &mut Iter<'_, u8>, has_esc: bool) -> Result<Response, Error> { pub fn parse(bytes: &mut Iter<'_, u8>, has_esc: bool) -> Result<Response<String>, Error> {
let mut parser = Rfc5321Parser::new(bytes); let mut parser = Rfc5321Parser::new(bytes);
let mut code = 0; let mut code = [0u8; 3];
let mut message = Vec::with_capacity(32); let mut message = Vec::with_capacity(32);
let mut esc = [0u8; 3]; let mut esc = [0u8; 3];
let mut eol = false; let mut eol = false;
'outer: while !eol { 'outer: while !eol {
code = match parser.size()? { for code in code.iter_mut() {
val @ 100..=999 => val as u16, match parser.read_char()? {
_ => 0, ch @ b'0'..=b'9' => {
}; *code = ch - b'0';
match parser.stop_char { }
_ => {
return Err(Error::SyntaxError {
syntax: "unexpected token",
})
}
}
}
match parser.read_char()? {
b' ' => { b' ' => {
eol = true; eol = true;
} }
b'-' => (), b'-' => (),
b'\n' if code < 600 => { b'\n' if code[0] < 6 => {
break; break;
} }
_ => { _ => {
@ -352,7 +373,10 @@ impl Capability {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{Capability, EhloResponse, Error, Mechanism, MtPriority, Response}; use crate::{
request::receiver::ReceiverParser, Capability, EhloResponse, Error, Mechanism, MtPriority,
Response,
};
#[test] #[test]
fn parse_ehlo() { fn parse_ehlo() {
@ -460,33 +484,24 @@ mod tests {
concat!("523-Massive\n", "523-Error\n", "523 Message\n"), concat!("523-Massive\n", "523-Error\n", "523 Message\n"),
Err(Error::InvalidResponse { Err(Error::InvalidResponse {
response: Response { response: Response {
code: 523, code: [5, 2, 3],
esc: [0, 0, 0], esc: [0, 0, 0],
message: "Massive Error Message".to_string(), message: "Massive\r\nError\r\nMessage".to_string(),
}, },
}), }),
), ),
] { ] {
let (response, parsed_response): (&str, Result<EhloResponse, Error>) = item; let (response, parsed_response): (&str, Result<EhloResponse<String>, Error>) = item;
for replacement in ["", "\r\n", " \n", " \r\n"] { for replacement in ["", "\r\n", " \n", " \r\n"] {
let response = if !replacement.is_empty() { let response = if !replacement.is_empty() && parsed_response.is_ok() {
response.replace('\n', replacement) response.replace('\n', replacement)
} else { } else {
response.to_string() response.to_string()
}; };
assert_eq!( assert_eq!(
parsed_response, parsed_response,
EhloResponse::parse(&mut response.as_bytes().iter()).map_err(|err| match err { EhloResponse::parse(&mut response.as_bytes().iter()),
Error::InvalidResponse { response } => Error::InvalidResponse {
response: Response {
code: response.code,
esc: response.esc,
message: response.message.trim_end().to_string()
}
},
err => err,
}),
"failed for {:?}", "failed for {:?}",
response response
); );
@ -500,7 +515,7 @@ mod tests {
( (
"250 2.1.1 Originator <ned@ymir.claremont.edu> ok\n", "250 2.1.1 Originator <ned@ymir.claremont.edu> ok\n",
Response { Response {
code: 250, code: [2, 5, 0],
esc: [2, 1, 1], esc: [2, 1, 1],
message: "Originator <ned@ymir.claremont.edu> ok".to_string(), message: "Originator <ned@ymir.claremont.edu> ok".to_string(),
}, },
@ -512,7 +527,7 @@ mod tests {
"551 5.7.1 Select another host to act as your forwarder\n" "551 5.7.1 Select another host to act as your forwarder\n"
), ),
Response { Response {
code: 551, code: [5, 5, 1],
esc: [5, 7, 1], esc: [5, 7, 1],
message: concat!( message: concat!(
"Forwarding to remote hosts disabled ", "Forwarding to remote hosts disabled ",
@ -528,7 +543,7 @@ mod tests {
"550 user has moved with no forwarding address\n" "550 user has moved with no forwarding address\n"
), ),
Response { Response {
code: 550, code: [5, 5, 0],
esc: [0, 0, 0], esc: [0, 0, 0],
message: "mailbox unavailable user has moved with no forwarding address" message: "mailbox unavailable user has moved with no forwarding address"
.to_string(), .to_string(),
@ -541,7 +556,7 @@ mod tests {
"550 user has moved with no forwarding address\n" "550 user has moved with no forwarding address\n"
), ),
Response { Response {
code: 550, code: [5, 5, 0],
esc: [0, 0, 0], esc: [0, 0, 0],
message: "mailbox unavailable user has moved with no forwarding address" message: "mailbox unavailable user has moved with no forwarding address"
.to_string(), .to_string(),
@ -562,7 +577,7 @@ mod tests {
"432 6.8.9 World!\n" "432 6.8.9 World!\n"
), ),
Response { Response {
code: 432, code: [4, 3, 2],
esc: [6, 8, 9], esc: [6, 8, 9],
message: "Hello , World!".to_string(), message: "Hello , World!".to_string(),
}, },
@ -571,7 +586,7 @@ mod tests {
( (
concat!("250-Missing space\n", "250\n", "250 Ignore this"), concat!("250-Missing space\n", "250\n", "250 Ignore this"),
Response { Response {
code: 250, code: [2, 5, 0],
esc: [0, 0, 0], esc: [0, 0, 0],
message: "Missing space".to_string(), message: "Missing space".to_string(),
}, },