Improved Response receivers.
This commit is contained in:
parent
97092b606c
commit
81fdbe7151
6 changed files with 544 additions and 484 deletions
129
src/lib.rs
129
src/lib.rs
|
@ -165,61 +165,54 @@ pub const AUTH_LOGIN: u64 = 1u64 << 39;
|
|||
pub const AUTH_PLAIN: u64 = 1u64 << 40;
|
||||
pub const AUTH_ANONYMOUS: u64 = 1u64 << 41;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Capability {
|
||||
EightBitMime,
|
||||
Atrn,
|
||||
Auth {
|
||||
mechanisms: u64,
|
||||
},
|
||||
BinaryMime,
|
||||
Burl,
|
||||
Checkpoint,
|
||||
Chunking,
|
||||
Conneg,
|
||||
Conperm,
|
||||
DeliverBy {
|
||||
min: u64,
|
||||
},
|
||||
Dsn,
|
||||
EnhancedStatusCodes,
|
||||
Etrn,
|
||||
Expn,
|
||||
FutureRelease {
|
||||
max_interval: u64,
|
||||
max_datetime: u64,
|
||||
},
|
||||
Help,
|
||||
MtPriority {
|
||||
priority: MtPriority,
|
||||
},
|
||||
Mtrk,
|
||||
NoSoliciting {
|
||||
keywords: Option<String>,
|
||||
},
|
||||
Onex,
|
||||
Pipelining,
|
||||
RequireTls,
|
||||
Rrvs,
|
||||
Size {
|
||||
size: usize,
|
||||
},
|
||||
SmtpUtf8,
|
||||
StartTls,
|
||||
Verb,
|
||||
}
|
||||
pub const EXT_8BIT_MIME: u32 = 1 << 0;
|
||||
pub const EXT_ATRN: u32 = 1 << 1;
|
||||
pub const EXT_AUTH: u32 = 1 << 2;
|
||||
pub const EXT_BINARY_MIME: u32 = 1 << 3;
|
||||
pub const EXT_BURL: u32 = 1 << 4;
|
||||
pub const EXT_CHECKPOINT: u32 = 1 << 5;
|
||||
pub const EXT_CHUNKING: u32 = 1 << 6;
|
||||
pub const EXT_CONNEG: u32 = 1 << 7;
|
||||
pub const EXT_CONPERM: u32 = 1 << 8;
|
||||
pub const EXT_DELIVER_BY: u32 = 1 << 9;
|
||||
pub const EXT_DSN: u32 = 1 << 10;
|
||||
pub const EXT_ENHANCED_STATUS_CODES: u32 = 1 << 11;
|
||||
pub const EXT_ETRN: u32 = 1 << 12;
|
||||
pub const EXT_FUTURE_RELEASE: u32 = 1 << 13;
|
||||
pub const EXT_HELP: u32 = 1 << 14;
|
||||
pub const EXT_MT_PRIORITY: u32 = 1 << 15;
|
||||
pub const EXT_MTRK: u32 = 1 << 16;
|
||||
pub const EXT_NO_SOLICITING: u32 = 1 << 17;
|
||||
pub const EXT_ONEX: u32 = 1 << 18;
|
||||
pub const EXT_PIPELINING: u32 = 1 << 19;
|
||||
pub const EXT_REQUIRE_TLS: u32 = 1 << 20;
|
||||
pub const EXT_RRVS: u32 = 1 << 21;
|
||||
pub const EXT_SIZE: u32 = 1 << 22;
|
||||
pub const EXT_SMTP_UTF8: u32 = 1 << 23;
|
||||
pub const EXT_START_TLS: u32 = 1 << 24;
|
||||
pub const EXT_VERB: u32 = 1 << 25;
|
||||
pub const EXT_EXPN: u32 = 1 << 26;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MtPriority {
|
||||
#[default]
|
||||
Mixer,
|
||||
Stanag4406,
|
||||
Nsep,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct EhloResponse<T: Display> {
|
||||
pub hostname: T,
|
||||
pub capabilities: Vec<Capability>,
|
||||
pub capabilities: u32,
|
||||
|
||||
pub auth_mechanisms: u64,
|
||||
pub deliver_by: u64,
|
||||
pub future_release_interval: u64,
|
||||
pub future_release_datetime: u64,
|
||||
pub mt_priority: MtPriority,
|
||||
pub no_soliciting: Option<String>,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -258,7 +251,9 @@ pub enum Error {
|
|||
SyntaxError { syntax: &'static str },
|
||||
InvalidParameter { param: &'static str },
|
||||
UnsupportedParameter { param: String },
|
||||
InvalidResponse { response: Response<String> },
|
||||
LineTooLong,
|
||||
ResponseTooLong,
|
||||
InvalidResponse { code: [u8; 3] },
|
||||
}
|
||||
|
||||
pub(crate) const LF: u8 = b'\n';
|
||||
|
@ -274,43 +269,3 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -2,10 +2,13 @@ use std::slice::Iter;
|
|||
|
||||
use crate::*;
|
||||
|
||||
use super::{receiver::ReceiverParser, *};
|
||||
use super::*;
|
||||
|
||||
impl ReceiverParser for Request<String> {
|
||||
fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
|
||||
#[derive(Default)]
|
||||
pub struct RequestParser {}
|
||||
|
||||
impl Request<String> {
|
||||
pub 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() {
|
||||
|
@ -289,7 +292,7 @@ impl ReceiverParser for Request<String> {
|
|||
pub(crate) struct Rfc5321Parser<'x, 'y> {
|
||||
bytes: &'x mut Iter<'y, u8>,
|
||||
pub(crate) stop_char: u8,
|
||||
bytes_left: usize,
|
||||
pub bytes_left: usize,
|
||||
}
|
||||
|
||||
impl<'x, 'y> Rfc5321Parser<'x, 'y> {
|
||||
|
@ -1134,9 +1137,8 @@ impl TryFrom<u128> for Body {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
request::receiver::ReceiverParser, Body, By, Error, Mtrk, Orcpt, Parameter, Request, Ret,
|
||||
Rrvs, AUTH_ECDSA_NIST256P_CHALLENGE, AUTH_GSSAPI, AUTH_SCRAM_SHA_256_PLUS, NOTIFY_DELAY,
|
||||
NOTIFY_FAILURE, NOTIFY_SUCCESS,
|
||||
Body, By, Error, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, AUTH_ECDSA_NIST256P_CHALLENGE,
|
||||
AUTH_GSSAPI, AUTH_SCRAM_SHA_256_PLUS, NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
use std::{marker::PhantomData, slice::Iter};
|
||||
use std::slice::Iter;
|
||||
|
||||
use crate::Error;
|
||||
use crate::{Error, Request};
|
||||
|
||||
pub struct Receiver<T: ReceiverParser + Sized> {
|
||||
const MAX_LINE_LENGTH: usize = 2048;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RequestReceiver {
|
||||
pub buf: Vec<u8>,
|
||||
_p: PhantomData<T>,
|
||||
}
|
||||
|
||||
pub trait ReceiverParser: Sized {
|
||||
fn parse(bytes: &mut Iter<'_, u8>) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
pub struct DataReceiver {
|
||||
|
@ -30,21 +28,24 @@ pub struct DummyDataReceiver {
|
|||
prev_last_ch: u8,
|
||||
}
|
||||
|
||||
impl<T: ReceiverParser> Default for Receiver<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(0),
|
||||
_p: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct DummyLineReceiver {}
|
||||
|
||||
impl<T: ReceiverParser> Receiver<T> {
|
||||
pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &[u8]) -> Result<T, Error> {
|
||||
impl RequestReceiver {
|
||||
pub fn ingest(
|
||||
&mut self,
|
||||
bytes: &mut Iter<'_, u8>,
|
||||
buf: &[u8],
|
||||
) -> Result<Request<String>, Error> {
|
||||
if self.buf.is_empty() {
|
||||
match T::parse(bytes) {
|
||||
Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => {
|
||||
self.buf = buf[buf.len() - bytes_left..].to_vec();
|
||||
match Request::parse(bytes) {
|
||||
Err(Error::NeedsMoreData { bytes_left }) => {
|
||||
if bytes_left > 0 {
|
||||
if bytes_left < MAX_LINE_LENGTH {
|
||||
self.buf = buf[buf.len() - bytes_left..].to_vec();
|
||||
} else {
|
||||
return Err(Error::LineTooLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
|
@ -52,9 +53,12 @@ impl<T: ReceiverParser> Receiver<T> {
|
|||
for &ch in bytes {
|
||||
self.buf.push(ch);
|
||||
if ch == b'\n' {
|
||||
let result = T::parse(&mut self.buf.iter());
|
||||
let result = Request::parse(&mut self.buf.iter());
|
||||
self.buf.clear();
|
||||
return result;
|
||||
} else if self.buf.len() == MAX_LINE_LENGTH {
|
||||
self.buf.clear();
|
||||
return Err(Error::LineTooLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,9 +179,20 @@ impl DummyDataReceiver {
|
|||
}
|
||||
}
|
||||
|
||||
impl DummyLineReceiver {
|
||||
pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool {
|
||||
for &ch in bytes {
|
||||
if ch == b'\n' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{request::receiver::Receiver, Error, Request};
|
||||
use crate::{request::receiver::RequestReceiver, Error, Request};
|
||||
|
||||
use super::DataReceiver;
|
||||
|
||||
|
@ -237,7 +252,7 @@ mod tests {
|
|||
),
|
||||
] {
|
||||
let mut requests = Vec::new();
|
||||
let mut r = Receiver::default();
|
||||
let mut r = RequestReceiver::default();
|
||||
for data in &data {
|
||||
let mut bytes = data.as_bytes().iter();
|
||||
loop {
|
||||
|
|
|
@ -9,86 +9,95 @@ impl<T: Display> EhloResponse<T> {
|
|||
pub fn new(hostname: T) -> Self {
|
||||
Self {
|
||||
hostname,
|
||||
capabilities: Vec::with_capacity(20),
|
||||
capabilities: 0,
|
||||
auth_mechanisms: 0,
|
||||
deliver_by: 0,
|
||||
future_release_interval: 0,
|
||||
future_release_datetime: 0,
|
||||
mt_priority: MtPriority::Mixer,
|
||||
no_soliciting: None,
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, mut writer: impl Write) -> io::Result<()> {
|
||||
write!(writer, "250-{} says hello\r\n", self.hostname)?;
|
||||
let len = self.capabilities.len();
|
||||
for (pos, capability) in self.capabilities.iter().enumerate() {
|
||||
let mut capabilities = self.capabilities;
|
||||
|
||||
while capabilities != 0 {
|
||||
let capability = 63 - capabilities.leading_zeros();
|
||||
capabilities ^= 1 << capability;
|
||||
|
||||
writer.write_all(b"250")?;
|
||||
writer.write_all(if pos < len - 1 { b"-" } else { b" " })?;
|
||||
writer.write_all(if capabilities != 0 { b"-" } else { b" " })?;
|
||||
match capability {
|
||||
Capability::EightBitMime => write!(writer, "8BITMIME\r\n"),
|
||||
Capability::Atrn => write!(writer, "ATRN\r\n"),
|
||||
Capability::Auth { mechanisms } => {
|
||||
EXT_8BIT_MIME => write!(writer, "8BITMIME\r\n"),
|
||||
EXT_ATRN => write!(writer, "ATRN\r\n"),
|
||||
EXT_AUTH => {
|
||||
writer.write_all(b"AUTH")?;
|
||||
let mut mechanisms = *mechanisms;
|
||||
let mut mechanisms = self.auth_mechanisms;
|
||||
while mechanisms != 0 {
|
||||
let item = 63 - mechanisms.leading_zeros();
|
||||
mechanisms ^= 1 << item;
|
||||
write!(writer, " {}", (item as u64).as_mechanism())?;
|
||||
write!(writer, " {}", (item as u64).to_mechanism())?;
|
||||
}
|
||||
writer.write_all(b"\r\n")
|
||||
}
|
||||
Capability::BinaryMime => write!(writer, "BINARYMIME\r\n"),
|
||||
Capability::Burl => write!(writer, "BURL\r\n"),
|
||||
Capability::Checkpoint => write!(writer, "CHECKPOINT\r\n"),
|
||||
Capability::Chunking => write!(writer, "CHUNKING\r\n"),
|
||||
Capability::Conneg => write!(writer, "CONNEG\r\n"),
|
||||
Capability::Conperm => write!(writer, "CONPERM\r\n"),
|
||||
Capability::DeliverBy { min } => {
|
||||
if *min > 0 {
|
||||
write!(writer, "DELIVERBY {}\r\n", min)
|
||||
EXT_BINARY_MIME => write!(writer, "BINARYMIME\r\n"),
|
||||
EXT_BURL => write!(writer, "BURL\r\n"),
|
||||
EXT_CHECKPOINT => write!(writer, "CHECKPOINT\r\n"),
|
||||
EXT_CHUNKING => write!(writer, "CHUNKING\r\n"),
|
||||
EXT_CONNEG => write!(writer, "CONNEG\r\n"),
|
||||
EXT_CONPERM => write!(writer, "CONPERM\r\n"),
|
||||
EXT_DELIVER_BY => {
|
||||
if self.deliver_by > 0 {
|
||||
write!(writer, "DELIVERBY {}\r\n", self.deliver_by)
|
||||
} else {
|
||||
write!(writer, "DELIVERBY\r\n")
|
||||
}
|
||||
}
|
||||
Capability::Dsn => write!(writer, "DSN\r\n"),
|
||||
Capability::EnhancedStatusCodes => write!(writer, "ENHANCEDSTATUSCODES\r\n"),
|
||||
Capability::Etrn => write!(writer, "ETRN\r\n"),
|
||||
Capability::Expn => write!(writer, "EXPN\r\n"),
|
||||
Capability::FutureRelease {
|
||||
max_interval,
|
||||
max_datetime,
|
||||
} => write!(
|
||||
EXT_DSN => write!(writer, "DSN\r\n"),
|
||||
EXT_ENHANCED_STATUS_CODES => write!(writer, "ENHANCEDSTATUSCODES\r\n"),
|
||||
EXT_ETRN => write!(writer, "ETRN\r\n"),
|
||||
EXT_EXPN => write!(writer, "EXPN\r\n"),
|
||||
EXT_FUTURE_RELEASE => write!(
|
||||
writer,
|
||||
"FUTURERELEASE {} {}\r\n",
|
||||
max_interval, max_datetime
|
||||
self.future_release_interval, self.future_release_datetime
|
||||
),
|
||||
Capability::Help => write!(writer, "HELP\r\n"),
|
||||
Capability::MtPriority { priority } => write!(
|
||||
EXT_HELP => write!(writer, "HELP\r\n"),
|
||||
EXT_MT_PRIORITY => write!(
|
||||
writer,
|
||||
"MT-PRIORITY {}\r\n",
|
||||
match priority {
|
||||
match self.mt_priority {
|
||||
MtPriority::Mixer => "MIXER",
|
||||
MtPriority::Stanag4406 => "STANAG4406",
|
||||
MtPriority::Nsep => "NSEP",
|
||||
}
|
||||
),
|
||||
Capability::Mtrk => write!(writer, "MTRK\r\n"),
|
||||
Capability::NoSoliciting { keywords } => {
|
||||
if let Some(keywords) = keywords {
|
||||
EXT_MTRK => write!(writer, "MTRK\r\n"),
|
||||
EXT_NO_SOLICITING => {
|
||||
if let Some(keywords) = &self.no_soliciting {
|
||||
write!(writer, "NO-SOLICITING {}\r\n", keywords)
|
||||
} else {
|
||||
write!(writer, "NO-SOLICITING\r\n")
|
||||
}
|
||||
}
|
||||
Capability::Onex => write!(writer, "ONEX\r\n"),
|
||||
Capability::Pipelining => write!(writer, "PIPELINING\r\n"),
|
||||
Capability::RequireTls => write!(writer, "REQUIRETLS\r\n"),
|
||||
Capability::Rrvs => write!(writer, "RRVS\r\n"),
|
||||
Capability::Size { size } => {
|
||||
if *size > 0 {
|
||||
write!(writer, "SIZE {}\r\n", size)
|
||||
EXT_ONEX => write!(writer, "ONEX\r\n"),
|
||||
EXT_PIPELINING => write!(writer, "PIPELINING\r\n"),
|
||||
EXT_REQUIRE_TLS => write!(writer, "REQUIRETLS\r\n"),
|
||||
EXT_RRVS => write!(writer, "RRVS\r\n"),
|
||||
EXT_SIZE => {
|
||||
if self.size > 0 {
|
||||
write!(writer, "SIZE {}\r\n", self.size)
|
||||
} else {
|
||||
write!(writer, "SIZE\r\n")
|
||||
}
|
||||
}
|
||||
Capability::SmtpUtf8 => write!(writer, "SMTPUTF8\r\n"),
|
||||
Capability::StartTls => write!(writer, "STARTTLS\r\n"),
|
||||
Capability::Verb => write!(writer, "VERB\r\n"),
|
||||
EXT_SMTP_UTF8 => write!(writer, "SMTPUTF8\r\n"),
|
||||
EXT_START_TLS => write!(writer, "STARTTLS\r\n"),
|
||||
EXT_VERB => write!(writer, "VERB\r\n"),
|
||||
_ => write!(writer, ""),
|
||||
}?;
|
||||
}
|
||||
|
||||
|
@ -112,12 +121,12 @@ impl<T: Display> Response<T> {
|
|||
}
|
||||
}
|
||||
|
||||
trait AsMechanism {
|
||||
fn as_mechanism(&self) -> &'static str;
|
||||
pub trait BitToString {
|
||||
fn to_mechanism(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl AsMechanism for u64 {
|
||||
fn as_mechanism(&self) -> &'static str {
|
||||
impl BitToString for u64 {
|
||||
fn to_mechanism(&self) -> &'static str {
|
||||
match *self {
|
||||
AUTH_SCRAM_SHA_256_PLUS => "SCRAM-SHA-256-PLUS",
|
||||
AUTH_SCRAM_SHA_256 => "SCRAM-SHA-256",
|
||||
|
@ -187,7 +196,7 @@ impl<T: Display> Response<T> {
|
|||
|
||||
/// Returns the status severity (first digit of the status code).
|
||||
pub fn severity(&self) -> Severity {
|
||||
match self.code[0] / 100 {
|
||||
match self.code[0] {
|
||||
2 => Severity::PositiveCompletion,
|
||||
3 => Severity::PositiveIntermediate,
|
||||
4 => Severity::TransientNegativeCompletion,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use crate::{EhloResponse, Response};
|
||||
|
||||
pub mod generate;
|
||||
pub mod parser;
|
||||
|
||||
|
@ -190,3 +194,41 @@ pub(crate) const STANAG4406: u128 = (b'S' as u128)
|
|||
| (b'6' as u128) << 72;
|
||||
pub(crate) const NSEP: u128 =
|
||||
(b'N' as u128) | (b'S' as u128) << 8 | (b'E' as u128) << 16 | (b'P' as u128) << 24;
|
||||
|
||||
impl<T: Display> EhloResponse<T> {
|
||||
/// Returns the hostname of the SMTP server.
|
||||
pub fn hostname(&self) -> &T {
|
||||
&self.hostname
|
||||
}
|
||||
|
||||
/// Returns the capabilities of the SMTP server.
|
||||
pub fn capabilities(&self) -> u32 {
|
||||
self.capabilities
|
||||
}
|
||||
|
||||
/// Returns `true` if the SMTP server supports a given extension.
|
||||
pub fn has_capability(&self, capability: u32) -> bool {
|
||||
(self.capabilities & capability) != 0
|
||||
}
|
||||
|
||||
/// Returns all supported authentication mechanisms.
|
||||
pub fn auth(&self) -> u64 {
|
||||
self.auth_mechanisms
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Display for Response<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Code: {}{}{}, Enhanced code: {}.{}.{}, Message: {}",
|
||||
self.code[0],
|
||||
self.code[1],
|
||||
self.code[2],
|
||||
self.esc[0],
|
||||
self.esc[1],
|
||||
self.esc[2],
|
||||
self.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,124 @@
|
|||
use std::slice::Iter;
|
||||
|
||||
use crate::{
|
||||
request::{parser::Rfc5321Parser, receiver::ReceiverParser},
|
||||
*,
|
||||
};
|
||||
use crate::{request::parser::Rfc5321Parser, *};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ReceiverParser for EhloResponse<String> {
|
||||
fn parse(bytes: &mut Iter<'_, u8>) -> Result<EhloResponse<String>, Error> {
|
||||
pub const MAX_REPONSE_LENGTH: usize = 4096;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ResponseReceiver {
|
||||
buf: Vec<u8>,
|
||||
code: [u8; 6],
|
||||
is_last: bool,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl ResponseReceiver {
|
||||
pub fn from_code(code: [u8; 3]) -> Self {
|
||||
Self {
|
||||
code: [code[0], code[1], code[2], 0, 0, 0],
|
||||
pos: 3,
|
||||
is_last: false,
|
||||
buf: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, bytes: &mut Iter<'_, u8>) -> Result<Response<String>, Error> {
|
||||
for &ch in bytes {
|
||||
match self.pos {
|
||||
0..=2 => {
|
||||
if ch.is_ascii_digit() {
|
||||
if self.buf.is_empty() {
|
||||
self.code[self.pos] = ch - b'0';
|
||||
}
|
||||
self.pos += 1;
|
||||
} else {
|
||||
return Err(Error::SyntaxError {
|
||||
syntax: "Invalid response code",
|
||||
});
|
||||
}
|
||||
}
|
||||
3 => match ch {
|
||||
b' ' => {
|
||||
self.is_last = true;
|
||||
self.pos += 1;
|
||||
}
|
||||
b'-' => {
|
||||
self.pos += 1;
|
||||
}
|
||||
b'\r' => {
|
||||
continue;
|
||||
}
|
||||
b'\n' => {
|
||||
self.is_last = true;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::SyntaxError {
|
||||
syntax: "Invalid response separator",
|
||||
});
|
||||
}
|
||||
},
|
||||
4 | 5 | 6 => match ch {
|
||||
b'0'..=b'9' => {
|
||||
if self.buf.is_empty() {
|
||||
let code = &mut self.code[self.pos - 1];
|
||||
*code = code.saturating_mul(10).saturating_add(ch - b'0');
|
||||
}
|
||||
}
|
||||
b'.' if self.pos < 6 && self.code[self.pos - 1] > 0 => {
|
||||
self.pos += 1;
|
||||
}
|
||||
_ => {
|
||||
if !ch.is_ascii_whitespace() {
|
||||
self.buf.push(ch);
|
||||
}
|
||||
self.pos = 7;
|
||||
}
|
||||
},
|
||||
_ => match ch {
|
||||
b'\r' | b'\n' => (),
|
||||
_ => {
|
||||
if self.buf.len() < MAX_REPONSE_LENGTH {
|
||||
self.buf.push(ch);
|
||||
} else {
|
||||
return Err(Error::ResponseTooLong);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if ch == b'\n' {
|
||||
if self.is_last {
|
||||
return Ok(Response {
|
||||
code: [self.code[0], self.code[1], self.code[2]],
|
||||
esc: [self.code[3], self.code[4], self.code[5]],
|
||||
message: std::mem::take(&mut self.buf).into_string(),
|
||||
});
|
||||
} else {
|
||||
self.buf.push(b'\n');
|
||||
self.pos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::NeedsMoreData { bytes_left: 0 })
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.is_last = false;
|
||||
self.code.fill(0);
|
||||
self.pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl EhloResponse<String> {
|
||||
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Self, Error> {
|
||||
let mut parser = Rfc5321Parser::new(bytes);
|
||||
let mut response = EhloResponse {
|
||||
hostname: String::new(),
|
||||
capabilities: Vec::new(),
|
||||
};
|
||||
let mut eol = false;
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
let mut response = EhloResponse::default();
|
||||
let mut code = [0u8; 3];
|
||||
let mut eol = false;
|
||||
let mut is_first_line = true;
|
||||
let mut did_success = false;
|
||||
|
||||
while !eol {
|
||||
for code in code.iter_mut() {
|
||||
|
@ -33,6 +133,11 @@ impl ReceiverParser for EhloResponse<String> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if code[0] != 2 || code[1] != 5 || code[2] != 0 {
|
||||
return Err(Error::InvalidResponse { code });
|
||||
}
|
||||
|
||||
match parser.read_char()? {
|
||||
b' ' => {
|
||||
eol = true;
|
||||
|
@ -48,263 +153,153 @@ impl ReceiverParser for EhloResponse<String> {
|
|||
}
|
||||
}
|
||||
|
||||
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()? {
|
||||
_8BITMIME => Capability::EightBitMime,
|
||||
ATRN => Capability::Atrn,
|
||||
AUTH => {
|
||||
let mut mechanisms = 0;
|
||||
while parser.stop_char != LF {
|
||||
if let Some(mechanism) = parser.mechanism()? {
|
||||
mechanisms |= mechanism;
|
||||
}
|
||||
}
|
||||
|
||||
Capability::Auth { mechanisms }
|
||||
}
|
||||
BINARYMIME => Capability::BinaryMime,
|
||||
BURL => Capability::Burl,
|
||||
CHECKPOINT => Capability::Checkpoint,
|
||||
CHUNKING => Capability::Chunking,
|
||||
CONNEG => Capability::Conneg,
|
||||
CONPERM => Capability::Conperm,
|
||||
DELIVERBY => Capability::DeliverBy {
|
||||
min: if parser.stop_char != LF {
|
||||
let db = parser.size()?;
|
||||
if db != usize::MAX {
|
||||
db as u64
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
},
|
||||
},
|
||||
DSN => Capability::Dsn,
|
||||
ENHANCEDSTATUSCO
|
||||
if parser.stop_char.to_ascii_uppercase() == b'D'
|
||||
&& parser.read_char()?.to_ascii_uppercase() == b'E'
|
||||
&& parser.read_char()?.to_ascii_uppercase() == b'S' =>
|
||||
{
|
||||
Capability::EnhancedStatusCodes
|
||||
}
|
||||
ETRN => Capability::Etrn,
|
||||
EXPN => Capability::Expn,
|
||||
FUTURERELEASE => {
|
||||
let max_interval = if parser.stop_char != LF {
|
||||
parser.size()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let max_datetime = if parser.stop_char != LF {
|
||||
parser.size()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Capability::FutureRelease {
|
||||
max_interval: if max_interval != usize::MAX {
|
||||
max_interval as u64
|
||||
} else {
|
||||
0
|
||||
},
|
||||
max_datetime: if max_datetime != usize::MAX {
|
||||
max_datetime as u64
|
||||
} else {
|
||||
0
|
||||
},
|
||||
if !is_first_line {
|
||||
response.capabilities |= match parser.hashed_value_long()? {
|
||||
_8BITMIME => EXT_8BIT_MIME,
|
||||
ATRN => EXT_ATRN,
|
||||
AUTH => {
|
||||
while parser.stop_char != LF {
|
||||
if let Some(mechanism) = parser.mechanism()? {
|
||||
response.auth_mechanisms |= mechanism;
|
||||
}
|
||||
}
|
||||
HELP => Capability::Help,
|
||||
MT_PRIORITY => Capability::MtPriority {
|
||||
priority: if parser.stop_char != LF {
|
||||
match parser.hashed_value_long()? {
|
||||
MIXER => MtPriority::Mixer,
|
||||
STANAG4406 => MtPriority::Stanag4406,
|
||||
NSEP => MtPriority::Nsep,
|
||||
_ => MtPriority::Mixer,
|
||||
}
|
||||
|
||||
EXT_AUTH
|
||||
}
|
||||
BINARYMIME => EXT_BINARY_MIME,
|
||||
BURL => EXT_BURL,
|
||||
CHECKPOINT => EXT_CHECKPOINT,
|
||||
CHUNKING => EXT_CHUNKING,
|
||||
CONNEG => EXT_CONNEG,
|
||||
CONPERM => EXT_CONPERM,
|
||||
DELIVERBY => {
|
||||
response.deliver_by = if parser.stop_char != LF {
|
||||
let db = parser.size()?;
|
||||
if db != usize::MAX {
|
||||
db as u64
|
||||
} else {
|
||||
MtPriority::Mixer
|
||||
},
|
||||
},
|
||||
MTRK => Capability::Mtrk,
|
||||
NO_SOLICITING => Capability::NoSoliciting {
|
||||
keywords: if parser.stop_char != LF {
|
||||
let text = parser.text()?;
|
||||
if !text.is_empty() {
|
||||
text.into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
EXT_DELIVER_BY
|
||||
}
|
||||
DSN => EXT_DSN,
|
||||
ENHANCEDSTATUSCO
|
||||
if parser.stop_char.to_ascii_uppercase() == b'D'
|
||||
&& parser.read_char()?.to_ascii_uppercase() == b'E'
|
||||
&& parser.read_char()?.to_ascii_uppercase() == b'S' =>
|
||||
{
|
||||
EXT_ENHANCED_STATUS_CODES
|
||||
}
|
||||
ETRN => EXT_ETRN,
|
||||
EXPN => EXT_EXPN,
|
||||
FUTURERELEASE => {
|
||||
let max_interval = if parser.stop_char != LF {
|
||||
parser.size()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let max_datetime = if parser.stop_char != LF {
|
||||
parser.size()?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
response.future_release_interval = if max_interval != usize::MAX {
|
||||
max_interval as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
response.future_release_datetime = if max_datetime != usize::MAX {
|
||||
max_datetime as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
EXT_FUTURE_RELEASE
|
||||
}
|
||||
HELP => EXT_HELP,
|
||||
MT_PRIORITY => {
|
||||
response.mt_priority = if parser.stop_char != LF {
|
||||
match parser.hashed_value_long()? {
|
||||
MIXER => MtPriority::Mixer,
|
||||
STANAG4406 => MtPriority::Stanag4406,
|
||||
NSEP => MtPriority::Nsep,
|
||||
_ => MtPriority::Mixer,
|
||||
}
|
||||
} else {
|
||||
MtPriority::Mixer
|
||||
};
|
||||
EXT_MT_PRIORITY
|
||||
}
|
||||
MTRK => EXT_MTRK,
|
||||
NO_SOLICITING => {
|
||||
response.no_soliciting = if parser.stop_char != LF {
|
||||
let text = parser.text()?;
|
||||
if !text.is_empty() {
|
||||
text.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
ONEX => Capability::Onex,
|
||||
PIPELINING => Capability::Pipelining,
|
||||
REQUIRETLS => Capability::RequireTls,
|
||||
RRVS => Capability::Rrvs,
|
||||
SIZE => Capability::Size {
|
||||
size: if parser.stop_char != LF {
|
||||
let size = parser.size()?;
|
||||
if size != usize::MAX {
|
||||
size
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
EXT_NO_SOLICITING
|
||||
}
|
||||
ONEX => EXT_ONEX,
|
||||
PIPELINING => EXT_PIPELINING,
|
||||
REQUIRETLS => EXT_REQUIRE_TLS,
|
||||
RRVS => EXT_RRVS,
|
||||
SIZE => {
|
||||
response.size = if parser.stop_char != LF {
|
||||
let size = parser.size()?;
|
||||
if size != usize::MAX {
|
||||
size
|
||||
} else {
|
||||
0
|
||||
},
|
||||
},
|
||||
SMTPUTF8 => Capability::SmtpUtf8,
|
||||
STARTTLS => Capability::StartTls,
|
||||
VERB => Capability::Verb,
|
||||
_ => {
|
||||
parser.seek_lf()?;
|
||||
continue;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
EXT_SIZE
|
||||
}
|
||||
SMTPUTF8 => EXT_SMTP_UTF8,
|
||||
STARTTLS => EXT_START_TLS,
|
||||
VERB => EXT_VERB,
|
||||
_ => 0,
|
||||
};
|
||||
parser.seek_lf()?;
|
||||
} else {
|
||||
if is_first_line {
|
||||
is_first_line = false;
|
||||
} else if !buf.is_empty() {
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
}
|
||||
|
||||
let mut buf = Vec::with_capacity(16);
|
||||
loop {
|
||||
match parser.read_char()? {
|
||||
b'\n' => break,
|
||||
b'\r' => (),
|
||||
b' ' if did_success => {
|
||||
b' ' => {
|
||||
parser.seek_lf()?;
|
||||
break;
|
||||
}
|
||||
ch => {
|
||||
ch if buf.len() < MAX_REPONSE_LENGTH => {
|
||||
buf.push(ch);
|
||||
}
|
||||
_ => return Err(Error::ResponseTooLong),
|
||||
}
|
||||
}
|
||||
|
||||
if did_success {
|
||||
response.hostname = buf.into_string();
|
||||
buf = Vec::new();
|
||||
}
|
||||
is_first_line = false;
|
||||
response.hostname = buf.into_string();
|
||||
}
|
||||
}
|
||||
|
||||
if did_success {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(Error::InvalidResponse {
|
||||
response: Response {
|
||||
code,
|
||||
esc: [0, 0, 0],
|
||||
message: buf.into_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = [0u8; 3];
|
||||
let mut message = Vec::with_capacity(32);
|
||||
let mut esc = [0u8; 3];
|
||||
let mut eol = false;
|
||||
|
||||
'outer: while !eol {
|
||||
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[0] < 6 => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::SyntaxError {
|
||||
syntax: "unexpected token",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut esc_parse_error = 0;
|
||||
if has_esc {
|
||||
if esc[0] == 0 {
|
||||
for (pos, esc) in esc.iter_mut().enumerate() {
|
||||
let val = parser.size()?;
|
||||
*esc = if val < 100 { val as u8 } else { 0 };
|
||||
if pos < 2 && parser.stop_char != b'.' {
|
||||
esc_parse_error = parser.stop_char;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if parser.stop_char == LF {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
loop {
|
||||
match parser.read_char()? {
|
||||
b'0'..=b'9' | b'.' => (),
|
||||
b'\n' => continue 'outer,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !message.is_empty() && !matches!(message.last(), Some(b' ')) {
|
||||
message.push(b' ');
|
||||
}
|
||||
if esc_parse_error != 0 {
|
||||
message.push(esc_parse_error);
|
||||
}
|
||||
|
||||
loop {
|
||||
match parser.read_char()? {
|
||||
b'\n' => break,
|
||||
b'\r' => (),
|
||||
ch => {
|
||||
message.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Response {
|
||||
code,
|
||||
esc,
|
||||
message: message.into_string(),
|
||||
})
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
request::receiver::ReceiverParser, Capability, EhloResponse, Error, MtPriority, Response,
|
||||
AUTH_DIGEST_MD5, AUTH_GSSAPI, AUTH_PLAIN,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
use super::ResponseReceiver;
|
||||
|
||||
#[test]
|
||||
fn parse_ehlo() {
|
||||
|
@ -322,97 +317,106 @@ mod tests {
|
|||
"250-CONNEG\n",
|
||||
"250-CONPERM\n",
|
||||
"250-DELIVERBY\n",
|
||||
"250-DELIVERBY 240\n",
|
||||
"250-DSN\n",
|
||||
"250-ENHANCEDSTATUSCODES\n",
|
||||
"250-ETRN\n",
|
||||
"250-EXPN\n",
|
||||
"250-FUTURERELEASE 1234 5678\n",
|
||||
"250-FUTURERELEASE 123\n",
|
||||
"250-FUTURERELEASE\n",
|
||||
"250-HELP\n",
|
||||
"250-MT-PRIORITY\n",
|
||||
"250-MT-PRIORITY MIXER\n",
|
||||
"250-MT-PRIORITY STANAG4406\n",
|
||||
"250-MTRK\n",
|
||||
"250-NO-SOLICITING net.example:ADV\n",
|
||||
"250-NO-SOLICITING\n",
|
||||
"250-PIPELINING\n",
|
||||
"250-REQUIRETLS\n",
|
||||
"250-RRVS\n",
|
||||
"250-SIZE 1000000\n",
|
||||
"250-SIZE\n",
|
||||
"250-SMTPUTF8 ignore\n",
|
||||
"250-SMTPUTF8\n",
|
||||
"250 STARTTLS\n",
|
||||
),
|
||||
Ok(EhloResponse {
|
||||
hostname: "dbc.mtview.ca.us".to_string(),
|
||||
capabilities: vec![
|
||||
Capability::EightBitMime,
|
||||
Capability::Atrn,
|
||||
Capability::Auth {
|
||||
mechanisms: AUTH_GSSAPI | AUTH_DIGEST_MD5 | AUTH_PLAIN,
|
||||
},
|
||||
Capability::BinaryMime,
|
||||
Capability::Burl,
|
||||
Capability::Checkpoint,
|
||||
Capability::Chunking,
|
||||
Capability::Conneg,
|
||||
Capability::Conperm,
|
||||
Capability::DeliverBy { min: 0 },
|
||||
Capability::DeliverBy { min: 240 },
|
||||
Capability::Dsn,
|
||||
Capability::EnhancedStatusCodes,
|
||||
Capability::Etrn,
|
||||
Capability::Expn,
|
||||
Capability::FutureRelease {
|
||||
max_interval: 1234,
|
||||
max_datetime: 5678,
|
||||
},
|
||||
Capability::FutureRelease {
|
||||
max_interval: 123,
|
||||
max_datetime: 0,
|
||||
},
|
||||
Capability::FutureRelease {
|
||||
max_interval: 0,
|
||||
max_datetime: 0,
|
||||
},
|
||||
Capability::Help,
|
||||
Capability::MtPriority {
|
||||
priority: MtPriority::Mixer,
|
||||
},
|
||||
Capability::MtPriority {
|
||||
priority: MtPriority::Mixer,
|
||||
},
|
||||
Capability::MtPriority {
|
||||
priority: MtPriority::Stanag4406,
|
||||
},
|
||||
Capability::Mtrk,
|
||||
Capability::NoSoliciting {
|
||||
keywords: Some("net.example:ADV".to_string()),
|
||||
},
|
||||
Capability::NoSoliciting { keywords: None },
|
||||
Capability::Pipelining,
|
||||
Capability::RequireTls,
|
||||
Capability::Rrvs,
|
||||
Capability::Size { size: 1000000 },
|
||||
Capability::Size { size: 0 },
|
||||
Capability::SmtpUtf8,
|
||||
Capability::SmtpUtf8,
|
||||
Capability::StartTls,
|
||||
],
|
||||
capabilities: EXT_8BIT_MIME
|
||||
| EXT_ATRN
|
||||
| EXT_AUTH
|
||||
| EXT_BINARY_MIME
|
||||
| EXT_BURL
|
||||
| EXT_CHECKPOINT
|
||||
| EXT_CHUNKING
|
||||
| EXT_CONNEG
|
||||
| EXT_CONPERM
|
||||
| EXT_DELIVER_BY
|
||||
| EXT_DSN
|
||||
| EXT_ENHANCED_STATUS_CODES
|
||||
| EXT_ETRN
|
||||
| EXT_EXPN
|
||||
| EXT_FUTURE_RELEASE
|
||||
| EXT_HELP
|
||||
| EXT_MT_PRIORITY
|
||||
| EXT_MTRK
|
||||
| EXT_NO_SOLICITING
|
||||
| EXT_PIPELINING
|
||||
| EXT_REQUIRE_TLS
|
||||
| EXT_RRVS
|
||||
| EXT_SIZE
|
||||
| EXT_SMTP_UTF8
|
||||
| EXT_START_TLS,
|
||||
auth_mechanisms: AUTH_GSSAPI | AUTH_DIGEST_MD5 | AUTH_PLAIN,
|
||||
deliver_by: 0,
|
||||
future_release_interval: 1234,
|
||||
future_release_datetime: 5678,
|
||||
mt_priority: MtPriority::Mixer,
|
||||
no_soliciting: Some("net.example:ADV".to_string()),
|
||||
size: 1000000,
|
||||
}),
|
||||
),
|
||||
(
|
||||
concat!(
|
||||
"250-\n",
|
||||
"250-DELIVERBY 240\n",
|
||||
"250-FUTURERELEASE 123\n",
|
||||
"250-MT-PRIORITY MIXER\n",
|
||||
"250-NO-SOLICITING\n",
|
||||
"250-SIZE\n",
|
||||
"250 SMTPUTF8\n",
|
||||
),
|
||||
Ok(EhloResponse {
|
||||
hostname: "".to_string(),
|
||||
capabilities: EXT_DELIVER_BY
|
||||
| EXT_FUTURE_RELEASE
|
||||
| EXT_MT_PRIORITY
|
||||
| EXT_NO_SOLICITING
|
||||
| EXT_SIZE
|
||||
| EXT_SMTP_UTF8,
|
||||
auth_mechanisms: 0,
|
||||
deliver_by: 240,
|
||||
future_release_interval: 123,
|
||||
future_release_datetime: 0,
|
||||
mt_priority: MtPriority::Mixer,
|
||||
no_soliciting: None,
|
||||
size: 0,
|
||||
}),
|
||||
),
|
||||
(
|
||||
concat!(
|
||||
"250-dbc.mtview.ca.us says hello\n",
|
||||
"250-FUTURERELEASE\n",
|
||||
"250 MT-PRIORITY STANAG4406\n",
|
||||
),
|
||||
Ok(EhloResponse {
|
||||
hostname: "dbc.mtview.ca.us".to_string(),
|
||||
capabilities: EXT_FUTURE_RELEASE | EXT_MT_PRIORITY,
|
||||
auth_mechanisms: 0,
|
||||
deliver_by: 0,
|
||||
future_release_interval: 0,
|
||||
future_release_datetime: 0,
|
||||
mt_priority: MtPriority::Stanag4406,
|
||||
no_soliciting: None,
|
||||
size: 0,
|
||||
}),
|
||||
),
|
||||
(
|
||||
concat!("523-Massive\n", "523-Error\n", "523 Message\n"),
|
||||
Err(Error::InvalidResponse {
|
||||
response: Response {
|
||||
code: [5, 2, 3],
|
||||
esc: [0, 0, 0],
|
||||
message: "Massive\r\nError\r\nMessage".to_string(),
|
||||
},
|
||||
}),
|
||||
Err(Error::UnknownCommand),
|
||||
),
|
||||
] {
|
||||
let (response, parsed_response): (&str, Result<EhloResponse<String>, Error>) = item;
|
||||
|
@ -435,7 +439,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_response() {
|
||||
for (response, parsed_response, has_esc) in [
|
||||
let mut all_responses = Vec::new();
|
||||
let mut all_parsed_responses = Vec::new();
|
||||
|
||||
for (response, parsed_response, _) in [
|
||||
(
|
||||
"250 2.1.1 Originator <ned@ymir.claremont.edu> ok\n",
|
||||
Response {
|
||||
|
@ -454,7 +461,7 @@ mod tests {
|
|||
code: [5, 5, 1],
|
||||
esc: [5, 7, 1],
|
||||
message: concat!(
|
||||
"Forwarding to remote hosts disabled ",
|
||||
"Forwarding to remote hosts disabled\n",
|
||||
"Select another host to act as your forwarder"
|
||||
)
|
||||
.to_string(),
|
||||
|
@ -469,7 +476,7 @@ mod tests {
|
|||
Response {
|
||||
code: [5, 5, 0],
|
||||
esc: [0, 0, 0],
|
||||
message: "mailbox unavailable user has moved with no forwarding address"
|
||||
message: "mailbox unavailable\nuser has moved with no forwarding address"
|
||||
.to_string(),
|
||||
},
|
||||
false,
|
||||
|
@ -482,7 +489,7 @@ mod tests {
|
|||
Response {
|
||||
code: [5, 5, 0],
|
||||
esc: [0, 0, 0],
|
||||
message: "mailbox unavailable user has moved with no forwarding address"
|
||||
message: "mailbox unavailable\nuser has moved with no forwarding address"
|
||||
.to_string(),
|
||||
},
|
||||
true,
|
||||
|
@ -503,7 +510,7 @@ mod tests {
|
|||
Response {
|
||||
code: [4, 3, 2],
|
||||
esc: [6, 8, 9],
|
||||
message: "Hello , World!".to_string(),
|
||||
message: "\nHello\n\n,\n\n\n\n\n\nWorld!".to_string(),
|
||||
},
|
||||
true,
|
||||
),
|
||||
|
@ -512,17 +519,47 @@ mod tests {
|
|||
Response {
|
||||
code: [2, 5, 0],
|
||||
esc: [0, 0, 0],
|
||||
message: "Missing space".to_string(),
|
||||
message: "Missing space\n".to_string(),
|
||||
},
|
||||
true,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
parsed_response,
|
||||
Response::parse(&mut response.as_bytes().iter(), has_esc).unwrap(),
|
||||
ResponseReceiver::default()
|
||||
.parse(&mut response.as_bytes().iter())
|
||||
.unwrap(),
|
||||
"failed for {:?}",
|
||||
response
|
||||
);
|
||||
all_responses.extend_from_slice(response.as_bytes());
|
||||
all_parsed_responses.push(parsed_response);
|
||||
}
|
||||
|
||||
// Test receiver
|
||||
for chunk_size in [5, 10, 20, 30, 40, 50, 60] {
|
||||
let mut receiver = ResponseReceiver::default();
|
||||
let mut parsed_response = all_parsed_responses.clone().into_iter();
|
||||
for chunk in all_responses.chunks(chunk_size) {
|
||||
let mut bytes = chunk.iter();
|
||||
loop {
|
||||
match receiver.parse(&mut bytes) {
|
||||
Ok(response) => {
|
||||
assert_eq!(
|
||||
parsed_response.next(),
|
||||
Some(response),
|
||||
"chunk size {}",
|
||||
chunk_size
|
||||
);
|
||||
receiver.reset();
|
||||
}
|
||||
Err(Error::NeedsMoreData { .. }) => {
|
||||
break;
|
||||
}
|
||||
err => panic!("Unexpected error {:?} for chunk size {}", err, chunk_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue