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