From 81fdbe7151c82b90420194da5a150c117c5cdb61 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Tue, 20 Dec 2022 16:30:18 +0000 Subject: [PATCH] Improved Response receivers. --- src/lib.rs | 129 +++----- src/request/parser.rs | 16 +- src/request/receiver.rs | 63 ++-- src/response/generate.rs | 105 +++--- src/response/mod.rs | 42 +++ src/response/parser.rs | 673 +++++++++++++++++++++------------------ 6 files changed, 544 insertions(+), 484 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 07598fd..51c2329 100644 --- a/src/lib.rs +++ b/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, - }, - 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 { pub hostname: T, - pub capabilities: Vec, + 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, + 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 }, + LineTooLong, + ResponseTooLong, + InvalidResponse { code: [u8; 3] }, } pub(crate) const LF: u8 = b'\n'; @@ -274,43 +269,3 @@ impl IntoString for Vec { .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, so we check the - // error here. - let record = result.unwrap(); - let codes = record.get(0).unwrap().split('.').collect::>(); - 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::>() - .chunks(50) - .map(|s| format!("\"{}\"", s.iter().collect::())) - .collect::>() - .join(", "); - - println!("{} => (\"{}\", concat!({})).into(),", codes[0], title, desc); - } - } -} -*/ diff --git a/src/request/parser.rs b/src/request/parser.rs index c5678e2..cda44b5 100644 --- a/src/request/parser.rs +++ b/src/request/parser.rs @@ -2,10 +2,13 @@ use std::slice::Iter; use crate::*; -use super::{receiver::ReceiverParser, *}; +use super::*; -impl ReceiverParser for Request { - fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { +#[derive(Default)] +pub struct RequestParser {} + +impl Request { + pub fn parse(bytes: &mut Iter<'_, u8>) -> Result, 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 { 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 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] diff --git a/src/request/receiver.rs b/src/request/receiver.rs index fb6c026..a742911 100644 --- a/src/request/receiver.rs +++ b/src/request/receiver.rs @@ -1,14 +1,12 @@ -use std::{marker::PhantomData, slice::Iter}; +use std::slice::Iter; -use crate::Error; +use crate::{Error, Request}; -pub struct Receiver { +const MAX_LINE_LENGTH: usize = 2048; + +#[derive(Default)] +pub struct RequestReceiver { pub buf: Vec, - _p: PhantomData, -} - -pub trait ReceiverParser: Sized { - fn parse(bytes: &mut Iter<'_, u8>) -> Result; } pub struct DataReceiver { @@ -30,21 +28,24 @@ pub struct DummyDataReceiver { prev_last_ch: u8, } -impl Default for Receiver { - fn default() -> Self { - Self { - buf: Vec::with_capacity(0), - _p: Default::default(), - } - } -} +pub struct DummyLineReceiver {} -impl Receiver { - pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &[u8]) -> Result { +impl RequestReceiver { + pub fn ingest( + &mut self, + bytes: &mut Iter<'_, u8>, + buf: &[u8], + ) -> Result, 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 Receiver { 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 { diff --git a/src/response/generate.rs b/src/response/generate.rs index 8eb6053..e19bb5e 100644 --- a/src/response/generate.rs +++ b/src/response/generate.rs @@ -9,86 +9,95 @@ impl EhloResponse { 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 Response { } } -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 Response { /// 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, diff --git a/src/response/mod.rs b/src/response/mod.rs index 67f5f14..156e402 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -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 EhloResponse { + /// 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 Display for Response { + 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, + ) + } +} diff --git a/src/response/parser.rs b/src/response/parser.rs index 952e38e..ea0069c 100644 --- a/src/response/parser.rs +++ b/src/response/parser.rs @@ -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 { - fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { +pub const MAX_REPONSE_LENGTH: usize = 4096; + +#[derive(Default)] +pub struct ResponseReceiver { + buf: Vec, + 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, 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 { + pub fn parse(bytes: &mut Iter<'_, u8>) -> Result { 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 { } } } + + 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 { } } - 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 { - pub fn parse(bytes: &mut Iter<'_, u8>, has_esc: bool) -> Result, 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, 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 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), + } + } + } } } }