Enhances Status Code explanations.
This commit is contained in:
parent
ee947670bd
commit
e7df79ef68
7 changed files with 1323 additions and 71 deletions
|
@ -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"
|
||||||
|
|
72
src/lib.rs
72
src/lib.rs
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
1170
src/response/generate.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue