Response parsing

This commit is contained in:
Mauro D 2022-12-04 18:15:15 +00:00
parent 24e0230bab
commit ee947670bd
6 changed files with 1592 additions and 518 deletions

View file

@ -120,7 +120,7 @@ pub const NOTIFY_SUCCESS: u8 = 0x01;
pub const NOTIFY_FAILURE: u8 = 0x02;
pub const NOTIFY_DELAY: u8 = 0x04;
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Mechanism {
_9798MDsaSha1,
_9798MEcdsaSha1,
@ -168,15 +168,79 @@ pub enum Mechanism {
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Capability {
EightBitMime,
Atrn,
Auth {
mechanisms: Vec<Mechanism>,
},
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,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MtPriority {
Mixer,
Stanag4406,
Nsep,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloResponse {
pub hostname: String,
pub capabilities: Vec<Capability>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub code: u16,
pub esc: [u8; 3],
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NeedsMoreData,
NeedsMoreData { bytes_left: usize },
UnknownCommand,
InvalidAddress,
SyntaxError { syntax: &'static str },
InvalidParameter { param: &'static str },
UnsupportedParameter { param: String },
UnexpectedChar { char: u8 },
InvalidResponse { response: Response },
}
pub(crate) const LF: u8 = b'\n';

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,8 @@ use crate::{
use super::*;
impl Request<String> {
pub fn parse<'x>(bytes: &'x mut Iter<'x, u8>) -> Result<Request<String>, Error> {
let mut parser = RequestParser::new(bytes);
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() {
return Err(Error::UnknownCommand);
@ -289,15 +289,18 @@ impl Request<String> {
}
}
struct RequestParser<'x> {
bytes: &'x mut Iter<'x, u8>,
stop_char: u8,
pub(crate) struct Rfc5321Parser<'x, 'y> {
bytes: &'x mut Iter<'y, u8>,
pub(crate) stop_char: u8,
bytes_left: usize,
}
impl<'x> RequestParser<'x> {
pub fn new(bytes: &'x mut Iter<'x, u8>) -> Self {
RequestParser {
impl<'x, 'y> Rfc5321Parser<'x, 'y> {
pub fn new(bytes: &'x mut Iter<'y, u8>) -> Self {
let (bytes_left, _) = bytes.size_hint();
Rfc5321Parser {
bytes,
bytes_left,
stop_char: 0,
}
}
@ -309,12 +312,12 @@ impl<'x> RequestParser<'x> {
while let Some(&ch) = self.bytes.next() {
match ch {
b'A'..=b'Z' if shift < 64 => {
value |= ((ch - b'A' + b'a') as u64) << shift;
b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 64 => {
value |= (ch as u64) << shift;
shift += 8;
}
b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 64 => {
value |= (ch as u64) << shift;
b'a'..=b'z' if shift < 64 => {
value |= ((ch - b'a' + b'A') as u64) << shift;
shift += 8;
}
b'\r' => (),
@ -331,7 +334,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[allow(clippy::while_let_on_iterator)]
@ -341,12 +346,12 @@ impl<'x> RequestParser<'x> {
while let Some(&ch) = self.bytes.next() {
match ch {
b'A'..=b'Z' if shift < 128 => {
value |= ((ch - b'A' + b'a') as u128) << shift;
b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 128 => {
value |= (ch as u128) << shift;
shift += 8;
}
b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 128 => {
value |= (ch as u128) << shift;
b'a'..=b'z' if shift < 128 => {
value |= ((ch - b'a' + b'A') as u128) << shift;
shift += 8;
}
b' ' => {
@ -363,7 +368,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn address(&mut self) -> Result<Option<String>, Error> {
@ -470,7 +477,9 @@ impl<'x> RequestParser<'x> {
last_ch = ch;
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn string(&mut self) -> Result<String, Error> {
@ -503,7 +512,9 @@ impl<'x> RequestParser<'x> {
last_ch = ch;
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[allow(clippy::while_let_on_iterator)]
@ -526,7 +537,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[allow(clippy::while_let_on_iterator)]
@ -572,7 +585,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[allow(clippy::while_let_on_iterator)]
@ -602,7 +617,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[inline(always)]
@ -613,7 +630,9 @@ impl<'x> RequestParser<'x> {
return Ok(());
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
} else {
Ok(())
}
@ -630,7 +649,16 @@ impl<'x> RequestParser<'x> {
}
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
#[inline(always)]
pub fn read_char(&mut self) -> Result<u8, Error> {
self.bytes.next().copied().ok_or(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn size(&mut self) -> Result<usize, Error> {
@ -659,7 +687,9 @@ impl<'x> RequestParser<'x> {
}
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn integer(&mut self) -> Result<i64, Error> {
@ -696,7 +726,9 @@ impl<'x> RequestParser<'x> {
}
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn timestamp(&mut self) -> Result<i64, Error> {
@ -752,7 +784,9 @@ impl<'x> RequestParser<'x> {
}
}
Err(Error::NeedsMoreData)
Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
})
}
pub fn parameters(&mut self) -> Result<Vec<Parameter<String>>, Error> {
@ -1012,7 +1046,7 @@ impl<'x> RequestParser<'x> {
Ok(params)
}
fn mechanism(&mut self) -> Result<Option<Mechanism>, Error> {
pub(crate) fn mechanism(&mut self) -> Result<Option<Mechanism>, Error> {
let mut trailing_chars = [0u8; 8];
let mut pos = 0;
let mechanism = self.hashed_value_long()?;
@ -1031,7 +1065,9 @@ impl<'x> RequestParser<'x> {
}
}
if !self.stop_char.is_ascii_whitespace() {
return Err(Error::NeedsMoreData);
return Err(Error::NeedsMoreData {
bytes_left: self.bytes_left,
});
} else if pos > 8 {
return Ok(Mechanism::Unknown.into());
}

193
src/request/receiver.rs Normal file
View file

@ -0,0 +1,193 @@
use std::slice::Iter;
use crate::{Error, Request};
pub struct RequestReceiver {
pub buf: Vec<u8>,
}
pub struct DataReceiver {
pub buf: Vec<u8>,
crlf_dot: bool,
last_ch: u8,
prev_last_ch: u8,
}
pub struct BdatReceiver {
pub buf: Vec<u8>,
bytes_left: usize,
}
impl RequestReceiver {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
buf: Vec::with_capacity(0),
}
}
pub fn ingest(
&mut self,
bytes: &mut Iter<'_, u8>,
buf: &[u8],
) -> Result<Request<String>, Error> {
if self.buf.is_empty() {
match Request::parse(bytes) {
Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => {
self.buf = buf[buf.len() - bytes_left..].to_vec();
}
result => return result,
}
} else {
for &ch in bytes {
self.buf.push(ch);
if ch == b'\n' {
let result = Request::parse(&mut self.buf.iter());
self.buf.clear();
return result;
}
}
}
Err(Error::NeedsMoreData { bytes_left: 0 })
}
}
impl DataReceiver {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
buf: Vec::with_capacity(1024),
crlf_dot: false,
last_ch: 0,
prev_last_ch: 0,
}
}
pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool {
for &ch in bytes {
match ch {
b'.' if self.last_ch == b'\n' && self.prev_last_ch == b'\r' => {
self.crlf_dot = true;
}
b'\n' if self.crlf_dot && self.last_ch == b'\r' => {
self.buf.truncate(self.buf.len() - 3);
return true;
}
b'\r' => {
self.buf.push(ch);
}
_ => {
self.buf.push(ch);
self.crlf_dot = false;
}
}
self.prev_last_ch = self.last_ch;
self.last_ch = ch;
}
false
}
}
impl BdatReceiver {
pub fn new(bytes_left: usize) -> Self {
Self {
buf: Vec::with_capacity(bytes_left),
bytes_left,
}
}
pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool {
for &ch in bytes {
self.buf.push(ch);
if self.buf.len() == self.bytes_left {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use crate::{request::receiver::RequestReceiver, Error, Request};
use super::DataReceiver;
#[test]
fn data_receiver() {
'outer: for (data, message) in [
(
vec!["hi\r\n", "..\r\n", ".a\r\n", "\r\n.\r\n"],
"hi\r\n.\r\na\r\n",
),
(
vec!["\r\na\rb\nc\r\n.d\r\n..\r\n", "\r\n.\r\n"],
"\r\na\rb\nc\r\nd\r\n.\r\n",
),
] {
let mut r = DataReceiver::new();
for data in &data {
if r.ingest(&mut data.as_bytes().iter()) {
assert_eq!(message, String::from_utf8(r.buf).unwrap());
continue 'outer;
}
}
panic!("Failed for {:?}", data);
}
}
#[test]
fn request_receiver() {
for (data, expected_requests) in [
(
vec![
"data\n",
"start",
"tls\n",
"quit\nnoop",
" hello\nehlo test\nvrfy name\n",
],
vec![
Request::Data,
Request::StartTls,
Request::Quit,
Request::Noop {
value: "hello".to_string(),
},
Request::Ehlo {
host: "test".to_string(),
},
Request::Vrfy {
value: "name".to_string(),
},
],
),
(
vec!["d", "a", "t", "a", "\n", "quit", "\n"],
vec![Request::Data, Request::Quit],
),
] {
let mut requests = Vec::new();
let mut r = RequestReceiver::new();
for data in &data {
let mut bytes = data.as_bytes().iter();
loop {
match r.ingest(&mut bytes, data.as_bytes()) {
Ok(request) => {
requests.push(request);
continue;
}
Err(Error::NeedsMoreData { .. }) => {
break;
}
err => panic!("Unexpected error {:?}", err),
}
}
}
assert_eq!(expected_requests, requests);
}
}
}

View file

@ -0,0 +1,191 @@
pub mod parser;
pub(crate) const _8BITMIME: u128 = (b'8' as u128)
| (b'B' as u128) << 8
| (b'I' as u128) << 16
| (b'T' as u128) << 24
| (b'M' as u128) << 32
| (b'I' as u128) << 40
| (b'M' as u128) << 48
| (b'E' as u128) << 56;
pub(crate) const ATRN: u128 =
(b'A' as u128) | (b'T' as u128) << 8 | (b'R' as u128) << 16 | (b'N' as u128) << 24;
pub(crate) const AUTH: u128 =
(b'A' as u128) | (b'U' as u128) << 8 | (b'T' as u128) << 16 | (b'H' as u128) << 24;
pub(crate) const BINARYMIME: u128 = (b'B' as u128)
| (b'I' as u128) << 8
| (b'N' as u128) << 16
| (b'A' as u128) << 24
| (b'R' as u128) << 32
| (b'Y' as u128) << 40
| (b'M' as u128) << 48
| (b'I' as u128) << 56
| (b'M' as u128) << 64
| (b'E' as u128) << 72;
pub(crate) const BURL: u128 =
(b'B' as u128) | (b'U' as u128) << 8 | (b'R' as u128) << 16 | (b'L' as u128) << 24;
pub(crate) const CHECKPOINT: u128 = (b'C' as u128)
| (b'H' as u128) << 8
| (b'E' as u128) << 16
| (b'C' as u128) << 24
| (b'K' as u128) << 32
| (b'P' as u128) << 40
| (b'O' as u128) << 48
| (b'I' as u128) << 56
| (b'N' as u128) << 64
| (b'T' as u128) << 72;
pub(crate) const CHUNKING: u128 = (b'C' as u128)
| (b'H' as u128) << 8
| (b'U' as u128) << 16
| (b'N' as u128) << 24
| (b'K' as u128) << 32
| (b'I' as u128) << 40
| (b'N' as u128) << 48
| (b'G' as u128) << 56;
pub(crate) const CONNEG: u128 = (b'C' as u128)
| (b'O' as u128) << 8
| (b'N' as u128) << 16
| (b'N' as u128) << 24
| (b'E' as u128) << 32
| (b'G' as u128) << 40;
pub(crate) const CONPERM: u128 = (b'C' as u128)
| (b'O' as u128) << 8
| (b'N' as u128) << 16
| (b'P' as u128) << 24
| (b'E' as u128) << 32
| (b'R' as u128) << 40
| (b'M' as u128) << 48;
pub(crate) const DELIVERBY: u128 = (b'D' as u128)
| (b'E' as u128) << 8
| (b'L' as u128) << 16
| (b'I' as u128) << 24
| (b'V' as u128) << 32
| (b'E' as u128) << 40
| (b'R' as u128) << 48
| (b'B' as u128) << 56
| (b'Y' as u128) << 64;
pub(crate) const DSN: u128 = (b'D' as u128) | (b'S' as u128) << 8 | (b'N' as u128) << 16;
pub(crate) const ENHANCEDSTATUSCO: u128 = (b'E' as u128)
| (b'N' as u128) << 8
| (b'H' as u128) << 16
| (b'A' as u128) << 24
| (b'N' as u128) << 32
| (b'C' as u128) << 40
| (b'E' as u128) << 48
| (b'D' as u128) << 56
| (b'S' as u128) << 64
| (b'T' as u128) << 72
| (b'A' as u128) << 80
| (b'T' as u128) << 88
| (b'U' as u128) << 96
| (b'S' as u128) << 104
| (b'C' as u128) << 112
| (b'O' as u128) << 120;
pub(crate) const ETRN: u128 =
(b'E' as u128) | (b'T' as u128) << 8 | (b'R' as u128) << 16 | (b'N' as u128) << 24;
pub(crate) const EXPN: u128 =
(b'E' as u128) | (b'X' as u128) << 8 | (b'P' as u128) << 16 | (b'N' as u128) << 24;
pub(crate) const FUTURERELEASE: u128 = (b'F' as u128)
| (b'U' as u128) << 8
| (b'T' as u128) << 16
| (b'U' as u128) << 24
| (b'R' as u128) << 32
| (b'E' as u128) << 40
| (b'R' as u128) << 48
| (b'E' as u128) << 56
| (b'L' as u128) << 64
| (b'E' as u128) << 72
| (b'A' as u128) << 80
| (b'S' as u128) << 88
| (b'E' as u128) << 96;
pub(crate) const HELP: u128 =
(b'H' as u128) | (b'E' as u128) << 8 | (b'L' as u128) << 16 | (b'P' as u128) << 24;
pub(crate) const MT_PRIORITY: u128 = (b'M' as u128)
| (b'T' as u128) << 8
| (b'-' as u128) << 16
| (b'P' as u128) << 24
| (b'R' as u128) << 32
| (b'I' as u128) << 40
| (b'O' as u128) << 48
| (b'R' as u128) << 56
| (b'I' as u128) << 64
| (b'T' as u128) << 72
| (b'Y' as u128) << 80;
pub(crate) const MTRK: u128 =
(b'M' as u128) | (b'T' as u128) << 8 | (b'R' as u128) << 16 | (b'K' as u128) << 24;
pub(crate) const NO_SOLICITING: u128 = (b'N' as u128)
| (b'O' as u128) << 8
| (b'-' as u128) << 16
| (b'S' as u128) << 24
| (b'O' as u128) << 32
| (b'L' as u128) << 40
| (b'I' as u128) << 48
| (b'C' as u128) << 56
| (b'I' as u128) << 64
| (b'T' as u128) << 72
| (b'I' as u128) << 80
| (b'N' as u128) << 88
| (b'G' as u128) << 96;
pub(crate) const ONEX: u128 =
(b'O' as u128) | (b'N' as u128) << 8 | (b'E' as u128) << 16 | (b'X' as u128) << 24;
pub(crate) const PIPELINING: u128 = (b'P' as u128)
| (b'I' as u128) << 8
| (b'P' as u128) << 16
| (b'E' as u128) << 24
| (b'L' as u128) << 32
| (b'I' as u128) << 40
| (b'N' as u128) << 48
| (b'I' as u128) << 56
| (b'N' as u128) << 64
| (b'G' as u128) << 72;
pub(crate) const REQUIRETLS: u128 = (b'R' as u128)
| (b'E' as u128) << 8
| (b'Q' as u128) << 16
| (b'U' as u128) << 24
| (b'I' as u128) << 32
| (b'R' as u128) << 40
| (b'E' as u128) << 48
| (b'T' as u128) << 56
| (b'L' as u128) << 64
| (b'S' as u128) << 72;
pub(crate) const RRVS: u128 =
(b'R' as u128) | (b'R' as u128) << 8 | (b'V' as u128) << 16 | (b'S' as u128) << 24;
pub(crate) const SIZE: u128 =
(b'S' as u128) | (b'I' as u128) << 8 | (b'Z' as u128) << 16 | (b'E' as u128) << 24;
pub(crate) const SMTPUTF8: u128 = (b'S' as u128)
| (b'M' as u128) << 8
| (b'T' as u128) << 16
| (b'P' as u128) << 24
| (b'U' as u128) << 32
| (b'T' as u128) << 40
| (b'F' as u128) << 48
| (b'8' as u128) << 56;
pub(crate) const STARTTLS: u128 = (b'S' as u128)
| (b'T' as u128) << 8
| (b'A' as u128) << 16
| (b'R' as u128) << 24
| (b'T' as u128) << 32
| (b'T' as u128) << 40
| (b'L' as u128) << 48
| (b'S' as u128) << 56;
pub(crate) const VERB: u128 =
(b'V' as u128) | (b'E' as u128) << 8 | (b'R' as u128) << 16 | (b'B' as u128) << 24;
// Priorities
pub(crate) const MIXER: u128 = (b'M' as u128)
| (b'I' as u128) << 8
| (b'X' as u128) << 16
| (b'E' as u128) << 24
| (b'R' as u128) << 32;
pub(crate) const STANAG4406: u128 = (b'S' as u128)
| (b'T' as u128) << 8
| (b'A' as u128) << 16
| (b'N' as u128) << 24
| (b'A' as u128) << 32
| (b'G' as u128) << 40
| (b'4' as u128) << 48
| (b'4' as u128) << 56
| (b'0' as u128) << 64
| (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;

589
src/response/parser.rs Normal file
View file

@ -0,0 +1,589 @@
use std::slice::Iter;
use crate::{
request::parser::Rfc5321Parser, Capability, EhloResponse, Error, IntoString, MtPriority,
Response, LF,
};
use super::*;
impl EhloResponse {
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<EhloResponse, 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 code = u16::MAX;
let mut is_first_line = true;
while !eol {
code = parser.size()? as u16;
match parser.stop_char {
b' ' => {
eol = true;
}
b'-' => (),
b'\n' if code < 600 => {
break;
}
_ => {
return Err(Error::SyntaxError {
syntax: "unexpected token",
});
}
}
if !is_first_line && code == 250 {
response
.capabilities
.push(match parser.hashed_value_long()? {
_8BITMIME => Capability::EightBitMime,
ATRN => Capability::Atrn,
AUTH => {
let mut mechanisms = Vec::new();
while parser.stop_char != LF {
if let Some(mechanism) = parser.mechanism()? {
mechanisms.push(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
},
}
}
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,
}
} 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
}
} 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 {
0
},
},
SMTPUTF8 => Capability::SmtpUtf8,
STARTTLS => Capability::StartTls,
VERB => Capability::Verb,
_ => {
parser.seek_lf()?;
continue;
}
});
parser.seek_lf()?;
} else {
let is_hostname = code == 250;
if is_first_line {
is_first_line = false;
} else if !buf.is_empty() && !matches!(buf.last(), Some(b' ')) {
buf.push(b' ');
}
loop {
match parser.read_char()? {
b'\n' => break,
b'\r' => (),
b' ' if is_hostname => {
parser.seek_lf()?;
break;
}
ch => {
buf.push(ch);
}
}
}
if is_hostname {
response.hostname = buf.into_string();
buf = Vec::new();
}
}
}
if code == 250 {
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<Response, Error> {
let mut parser = Rfc5321Parser::new(bytes);
let mut code = 0;
let mut message = Vec::with_capacity(32);
let mut esc = [0u8; 3];
let mut eol = false;
'outer: while !eol {
code = match parser.size()? {
val @ 100..=999 => val as u16,
_ => 0,
};
match parser.stop_char {
b' ' => {
eol = true;
}
b'-' => (),
b'\n' if code < 600 => {
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(),
})
}
}
impl Capability {
pub fn parse(value: &[u8]) -> Option<Capability> {
if value.eq_ignore_ascii_case(b"8BITMIME") {
Capability::EightBitMime.into()
} else if value.eq_ignore_ascii_case(b"ATRN") {
Capability::Atrn.into()
} else if value.eq_ignore_ascii_case(b"AUTH") {
Capability::Auth {
mechanisms: Vec::new(),
}
.into()
} else if value.eq_ignore_ascii_case(b"BINARYMIME") {
Capability::BinaryMime.into()
} else if value.eq_ignore_ascii_case(b"BURL") {
Capability::Burl.into()
} else if value.eq_ignore_ascii_case(b"CHECKPOINT") {
Capability::Checkpoint.into()
} else if value.eq_ignore_ascii_case(b"CHUNKING") {
Capability::Chunking.into()
} else if value.eq_ignore_ascii_case(b"CONNEG") {
Capability::Conneg.into()
} else if value.eq_ignore_ascii_case(b"CONPERM") {
Capability::Conperm.into()
} else if value.eq_ignore_ascii_case(b"DELIVERBY") {
Capability::DeliverBy { min: 0 }.into()
} else if value.eq_ignore_ascii_case(b"DSN") {
Capability::Dsn.into()
} else if value.eq_ignore_ascii_case(b"ENHANCEDSTATUSCODES") {
Capability::EnhancedStatusCodes.into()
} else if value.eq_ignore_ascii_case(b"ETRN") {
Capability::Etrn.into()
} else if value.eq_ignore_ascii_case(b"EXPN") {
Capability::Expn.into()
} else if value.eq_ignore_ascii_case(b"FUTURERELEASE") {
Capability::FutureRelease {
max_interval: 0,
max_datetime: 0,
}
.into()
} else if value.eq_ignore_ascii_case(b"HELP") {
Capability::Help.into()
} else if value.eq_ignore_ascii_case(b"MT-PRIORITY") {
Capability::MtPriority {
priority: MtPriority::Mixer,
}
.into()
} else if value.eq_ignore_ascii_case(b"MTRK") {
Capability::Mtrk.into()
} else if value.eq_ignore_ascii_case(b"NO-SOLICITING") {
Capability::NoSoliciting { keywords: None }.into()
} else if value.eq_ignore_ascii_case(b"ONEX") {
Capability::Onex.into()
} else if value.eq_ignore_ascii_case(b"PIPELINING") {
Capability::Pipelining.into()
} else if value.eq_ignore_ascii_case(b"REQUIRETLS") {
Capability::RequireTls.into()
} else if value.eq_ignore_ascii_case(b"RRVS") {
Capability::Rrvs.into()
} else if value.eq_ignore_ascii_case(b"SIZE") {
Capability::Size { size: 0 }.into()
} else if value.eq_ignore_ascii_case(b"SMTPUTF8") {
Capability::SmtpUtf8.into()
} else if value.eq_ignore_ascii_case(b"STARTTLS") {
Capability::StartTls.into()
} else if value.eq_ignore_ascii_case(b"VERB") {
Capability::Verb.into()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use crate::{Capability, EhloResponse, Error, Mechanism, MtPriority, Response};
#[test]
fn parse_ehlo() {
for item in [
(
concat!(
"250-dbc.mtview.ca.us says hello\n",
"250-8BITMIME\n",
"250-ATRN\n",
"250-AUTH GSSAPI DIGEST-MD5 PLAIN\n",
"250-BINARYMIME\n",
"250-BURL imap\n",
"250-CHECKPOINT\n",
"250-CHUNKING\n",
"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: vec![
Mechanism::Gssapi,
Mechanism::DigestMd5,
Mechanism::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,
],
}),
),
(
concat!("523-Massive\n", "523-Error\n", "523 Message\n"),
Err(Error::InvalidResponse {
response: Response {
code: 523,
esc: [0, 0, 0],
message: "Massive Error Message".to_string(),
},
}),
),
] {
let (response, parsed_response): (&str, Result<EhloResponse, Error>) = item;
for replacement in ["", "\r\n", " \n", " \r\n"] {
let response = if !replacement.is_empty() {
response.replace('\n', replacement)
} else {
response.to_string()
};
assert_eq!(
parsed_response,
EhloResponse::parse(&mut response.as_bytes().iter()).map_err(|err| match err {
Error::InvalidResponse { response } => Error::InvalidResponse {
response: Response {
code: response.code,
esc: response.esc,
message: response.message.trim_end().to_string()
}
},
err => err,
}),
"failed for {:?}",
response
);
}
}
}
#[test]
fn parse_response() {
for (response, parsed_response, has_esc) in [
(
"250 2.1.1 Originator <ned@ymir.claremont.edu> ok\n",
Response {
code: 250,
esc: [2, 1, 1],
message: "Originator <ned@ymir.claremont.edu> ok".to_string(),
},
true,
),
(
concat!(
"551-5.7.1 Forwarding to remote hosts disabled\n",
"551 5.7.1 Select another host to act as your forwarder\n"
),
Response {
code: 551,
esc: [5, 7, 1],
message: concat!(
"Forwarding to remote hosts disabled ",
"Select another host to act as your forwarder"
)
.to_string(),
},
true,
),
(
concat!(
"550-mailbox unavailable\n",
"550 user has moved with no forwarding address\n"
),
Response {
code: 550,
esc: [0, 0, 0],
message: "mailbox unavailable user has moved with no forwarding address"
.to_string(),
},
false,
),
(
concat!(
"550-mailbox unavailable\n",
"550 user has moved with no forwarding address\n"
),
Response {
code: 550,
esc: [0, 0, 0],
message: "mailbox unavailable user has moved with no forwarding address"
.to_string(),
},
true,
),
(
concat!(
"432-6.8.9\n",
"432-6.8.9 Hello\n",
"432-6.8.9 \n",
"432-6.8.9 ,\n",
"432-\n",
"432-6\n",
"432-6.\n",
"432-6.8\n",
"432-6.8.9\n",
"432 6.8.9 World!\n"
),
Response {
code: 432,
esc: [6, 8, 9],
message: "Hello , World!".to_string(),
},
true,
),
(
concat!("250-Missing space\n", "250\n", "250 Ignore this"),
Response {
code: 250,
esc: [0, 0, 0],
message: "Missing space".to_string(),
},
true,
),
] {
assert_eq!(
parsed_response,
Response::parse(&mut response.as_bytes().iter(), has_esc).unwrap(),
"failed for {:?}",
response
);
}
}
}