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
[dependencies]
[dev-dependencies]
csv = "1.1"

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
pub mod request;
pub mod response;
@ -220,16 +222,36 @@ pub enum MtPriority {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloResponse {
pub hostname: String,
pub struct EhloResponse<T: Display> {
pub hostname: T,
pub capabilities: Vec<Capability>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub code: u16,
pub struct Response<T: Display> {
pub code: [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)]
@ -240,7 +262,7 @@ pub enum Error {
SyntaxError { syntax: &'static str },
InvalidParameter { param: &'static str },
UnsupportedParameter { param: String },
InvalidResponse { response: Response },
InvalidResponse { response: Response<String> },
}
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())
}
}
#[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,
};
use super::*;
use super::{receiver::ReceiverParser, *};
impl Request<String> {
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
impl ReceiverParser for Request<String> {
fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
let mut parser = Rfc5321Parser::new(bytes);
let command = parser.hashed_value()?;
if !parser.stop_char.is_ascii_whitespace() {
@ -1137,8 +1137,8 @@ impl TryFrom<u128> for Body {
#[cfg(test)]
mod tests {
use crate::{
Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, NOTIFY_DELAY,
NOTIFY_FAILURE, NOTIFY_SUCCESS,
request::receiver::ReceiverParser, Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter,
Request, Ret, Rrvs, NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS,
};
#[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>,
_p: PhantomData<T>,
}
pub trait ReceiverParser: Sized {
fn parse(bytes: &mut Iter<'_, u8>) -> Result<Self, Error>;
}
pub struct DataReceiver {
@ -18,21 +23,19 @@ pub struct BdatReceiver {
bytes_left: usize,
}
impl RequestReceiver {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
impl<T: ReceiverParser> Default for Receiver<T> {
fn default() -> Self {
Self {
buf: Vec::with_capacity(0),
_p: Default::default(),
}
}
}
pub fn ingest(
&mut self,
bytes: &mut Iter<'_, u8>,
buf: &[u8],
) -> Result<Request<String>, Error> {
impl<T: ReceiverParser> Receiver<T> {
pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &[u8]) -> Result<T, Error> {
if self.buf.is_empty() {
match Request::parse(bytes) {
match T::parse(bytes) {
Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => {
self.buf = buf[buf.len() - bytes_left..].to_vec();
}
@ -42,7 +45,7 @@ impl RequestReceiver {
for &ch in bytes {
self.buf.push(ch);
if ch == b'\n' {
let result = Request::parse(&mut self.buf.iter());
let result = T::parse(&mut self.buf.iter());
self.buf.clear();
return result;
}
@ -112,7 +115,7 @@ impl BdatReceiver {
#[cfg(test)]
mod tests {
use crate::{request::receiver::RequestReceiver, Error, Request};
use crate::{request::receiver::Receiver, Error, Request};
use super::DataReceiver;
@ -171,7 +174,7 @@ mod tests {
),
] {
let mut requests = Vec::new();
let mut r = RequestReceiver::new();
let mut r = Receiver::default();
for data in &data {
let mut bytes = data.as_bytes().iter();
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(crate) const _8BITMIME: u128 = (b'8' as u128)

View file

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