Improved Response receivers.

This commit is contained in:
Mauro D 2022-12-20 16:30:18 +00:00
parent 97092b606c
commit 81fdbe7151
6 changed files with 544 additions and 484 deletions

View file

@ -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);
}
}
}
*/

View file

@ -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]

View file

@ -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 {

View file

@ -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,

View file

@ -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,
)
}
}

View file

@ -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),
}
}
}
}
}
}