Response parsing
This commit is contained in:
parent
24e0230bab
commit
ee947670bd
6 changed files with 1592 additions and 518 deletions
70
src/lib.rs
70
src/lib.rs
|
@ -120,7 +120,7 @@ pub const NOTIFY_SUCCESS: u8 = 0x01;
|
||||||
pub const NOTIFY_FAILURE: u8 = 0x02;
|
pub const NOTIFY_FAILURE: u8 = 0x02;
|
||||||
pub const NOTIFY_DELAY: u8 = 0x04;
|
pub const NOTIFY_DELAY: u8 = 0x04;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Mechanism {
|
pub enum Mechanism {
|
||||||
_9798MDsaSha1,
|
_9798MDsaSha1,
|
||||||
_9798MEcdsaSha1,
|
_9798MEcdsaSha1,
|
||||||
|
@ -168,15 +168,79 @@ pub enum Mechanism {
|
||||||
Unknown,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
NeedsMoreData,
|
NeedsMoreData { bytes_left: usize },
|
||||||
UnknownCommand,
|
UnknownCommand,
|
||||||
InvalidAddress,
|
InvalidAddress,
|
||||||
SyntaxError { syntax: &'static str },
|
SyntaxError { syntax: &'static str },
|
||||||
InvalidParameter { param: &'static str },
|
InvalidParameter { param: &'static str },
|
||||||
UnsupportedParameter { param: String },
|
UnsupportedParameter { param: String },
|
||||||
UnexpectedChar { char: u8 },
|
InvalidResponse { response: Response },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const LF: u8 = b'\n';
|
pub(crate) const LF: u8 = b'\n';
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,8 +8,8 @@ use crate::{
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl Request<String> {
|
impl Request<String> {
|
||||||
pub fn parse<'x>(bytes: &'x mut Iter<'x, u8>) -> Result<Request<String>, Error> {
|
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
|
||||||
let mut parser = RequestParser::new(bytes);
|
let mut parser = Rfc5321Parser::new(bytes);
|
||||||
let command = parser.hashed_value()?;
|
let command = parser.hashed_value()?;
|
||||||
if !parser.stop_char.is_ascii_whitespace() {
|
if !parser.stop_char.is_ascii_whitespace() {
|
||||||
return Err(Error::UnknownCommand);
|
return Err(Error::UnknownCommand);
|
||||||
|
@ -289,15 +289,18 @@ impl Request<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RequestParser<'x> {
|
pub(crate) struct Rfc5321Parser<'x, 'y> {
|
||||||
bytes: &'x mut Iter<'x, u8>,
|
bytes: &'x mut Iter<'y, u8>,
|
||||||
stop_char: u8,
|
pub(crate) stop_char: u8,
|
||||||
|
bytes_left: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'x> RequestParser<'x> {
|
impl<'x, 'y> Rfc5321Parser<'x, 'y> {
|
||||||
pub fn new(bytes: &'x mut Iter<'x, u8>) -> Self {
|
pub fn new(bytes: &'x mut Iter<'y, u8>) -> Self {
|
||||||
RequestParser {
|
let (bytes_left, _) = bytes.size_hint();
|
||||||
|
Rfc5321Parser {
|
||||||
bytes,
|
bytes,
|
||||||
|
bytes_left,
|
||||||
stop_char: 0,
|
stop_char: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,12 +312,12 @@ impl<'x> RequestParser<'x> {
|
||||||
|
|
||||||
while let Some(&ch) = self.bytes.next() {
|
while let Some(&ch) = self.bytes.next() {
|
||||||
match ch {
|
match ch {
|
||||||
b'A'..=b'Z' if shift < 64 => {
|
b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 64 => {
|
||||||
value |= ((ch - b'A' + b'a') as u64) << shift;
|
value |= (ch as u64) << shift;
|
||||||
shift += 8;
|
shift += 8;
|
||||||
}
|
}
|
||||||
b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 64 => {
|
b'a'..=b'z' if shift < 64 => {
|
||||||
value |= (ch as u64) << shift;
|
value |= ((ch - b'a' + b'A') as u64) << shift;
|
||||||
shift += 8;
|
shift += 8;
|
||||||
}
|
}
|
||||||
b'\r' => (),
|
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)]
|
#[allow(clippy::while_let_on_iterator)]
|
||||||
|
@ -341,12 +346,12 @@ impl<'x> RequestParser<'x> {
|
||||||
|
|
||||||
while let Some(&ch) = self.bytes.next() {
|
while let Some(&ch) = self.bytes.next() {
|
||||||
match ch {
|
match ch {
|
||||||
b'A'..=b'Z' if shift < 128 => {
|
b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 128 => {
|
||||||
value |= ((ch - b'A' + b'a') as u128) << shift;
|
value |= (ch as u128) << shift;
|
||||||
shift += 8;
|
shift += 8;
|
||||||
}
|
}
|
||||||
b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 128 => {
|
b'a'..=b'z' if shift < 128 => {
|
||||||
value |= (ch as u128) << shift;
|
value |= ((ch - b'a' + b'A') as u128) << shift;
|
||||||
shift += 8;
|
shift += 8;
|
||||||
}
|
}
|
||||||
b' ' => {
|
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> {
|
pub fn address(&mut self) -> Result<Option<String>, Error> {
|
||||||
|
@ -470,7 +477,9 @@ impl<'x> RequestParser<'x> {
|
||||||
last_ch = ch;
|
last_ch = ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::NeedsMoreData)
|
Err(Error::NeedsMoreData {
|
||||||
|
bytes_left: self.bytes_left,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string(&mut self) -> Result<String, Error> {
|
pub fn string(&mut self) -> Result<String, Error> {
|
||||||
|
@ -503,7 +512,9 @@ impl<'x> RequestParser<'x> {
|
||||||
last_ch = ch;
|
last_ch = ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::NeedsMoreData)
|
Err(Error::NeedsMoreData {
|
||||||
|
bytes_left: self.bytes_left,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::while_let_on_iterator)]
|
#[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)]
|
#[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)]
|
#[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)]
|
#[inline(always)]
|
||||||
|
@ -613,7 +630,9 @@ impl<'x> RequestParser<'x> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::NeedsMoreData)
|
Err(Error::NeedsMoreData {
|
||||||
|
bytes_left: self.bytes_left,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
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> {
|
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> {
|
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> {
|
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> {
|
pub fn parameters(&mut self) -> Result<Vec<Parameter<String>>, Error> {
|
||||||
|
@ -1012,7 +1046,7 @@ impl<'x> RequestParser<'x> {
|
||||||
Ok(params)
|
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 trailing_chars = [0u8; 8];
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
let mechanism = self.hashed_value_long()?;
|
let mechanism = self.hashed_value_long()?;
|
||||||
|
@ -1031,7 +1065,9 @@ impl<'x> RequestParser<'x> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.stop_char.is_ascii_whitespace() {
|
if !self.stop_char.is_ascii_whitespace() {
|
||||||
return Err(Error::NeedsMoreData);
|
return Err(Error::NeedsMoreData {
|
||||||
|
bytes_left: self.bytes_left,
|
||||||
|
});
|
||||||
} else if pos > 8 {
|
} else if pos > 8 {
|
||||||
return Ok(Mechanism::Unknown.into());
|
return Ok(Mechanism::Unknown.into());
|
||||||
}
|
}
|
||||||
|
|
193
src/request/receiver.rs
Normal file
193
src/request/receiver.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
589
src/response/parser.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue