diff --git a/Cargo.toml b/Cargo.toml index 4bf979e..00717de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[dev-dependencies] +csv = "1.1" diff --git a/src/lib.rs b/src/lib.rs index 4932ae3..9c0576d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + pub mod request; pub mod response; @@ -220,16 +222,36 @@ pub enum MtPriority { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct EhloResponse { - pub hostname: String, +pub struct EhloResponse { + pub hostname: T, pub capabilities: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Response { - pub code: u16, +pub struct Response { + pub code: [u8; 3], pub esc: [u8; 3], - pub message: String, + pub message: T, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + PositiveCompletion = 2, + PositiveIntermediate = 3, + TransientNegativeCompletion = 4, + PermanentNegativeCompletion = 5, + Invalid = 0, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Category { + Syntax = 0, + Information = 1, + Connections = 2, + Unspecified3 = 3, + Unspecified4 = 4, + MailSystem = 5, + Invalid = 6, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -240,7 +262,7 @@ pub enum Error { SyntaxError { syntax: &'static str }, InvalidParameter { param: &'static str }, UnsupportedParameter { param: String }, - InvalidResponse { response: Response }, + InvalidResponse { response: Response }, } pub(crate) const LF: u8 = b'\n'; @@ -256,3 +278,41 @@ impl IntoString for Vec { .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) } } + +#[cfg(test)] +mod tests { + + #[test] + fn csv() { + // Build the CSV reader and iterate over each record. + let mut rdr = csv::Reader::from_path("smtp-enhanced-status-codes-1.csv").unwrap(); + for result in rdr.records() { + // The iterator yields Result, so we check the + // error here. + let record = result.unwrap(); + let codes = record.get(0).unwrap().split('.').collect::>(); + let title = record.get(1).unwrap().replace('\n', " "); + let desc = record + .get(2) + .unwrap() + .replace('\n', " ") + .replace('"', "\\\"") + .replace("This is useful only as a persistent transient error.", "") + .replace( + "This is useful for both permanent and persistent transient errors.", + "", + ) + .replace("This is useful only as a permanent error.", "") + .trim() + .replace(" ", " ") + .chars() + .collect::>() + .chunks(50) + .map(|s| format!("\"{}\"", s.iter().collect::())) + .collect::>() + .join(", "); + + println!("{} => (\"{}\", concat!({})).into(),", codes[0], title, desc); + } + } +} diff --git a/src/request/parser.rs b/src/request/parser.rs index 6e157ce..b2cd0c1 100644 --- a/src/request/parser.rs +++ b/src/request/parser.rs @@ -5,10 +5,10 @@ use crate::{ NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS, SP, }; -use super::*; +use super::{receiver::ReceiverParser, *}; -impl Request { - pub fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { +impl ReceiverParser for Request { + fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { let mut parser = Rfc5321Parser::new(bytes); let command = parser.hashed_value()?; if !parser.stop_char.is_ascii_whitespace() { @@ -1137,8 +1137,8 @@ impl TryFrom for Body { #[cfg(test)] mod tests { use crate::{ - Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, NOTIFY_DELAY, - NOTIFY_FAILURE, NOTIFY_SUCCESS, + request::receiver::ReceiverParser, Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter, + Request, Ret, Rrvs, NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS, }; #[test] diff --git a/src/request/receiver.rs b/src/request/receiver.rs index 7a92087..b627212 100644 --- a/src/request/receiver.rs +++ b/src/request/receiver.rs @@ -1,9 +1,14 @@ -use std::slice::Iter; +use std::{marker::PhantomData, slice::Iter}; -use crate::{Error, Request}; +use crate::Error; -pub struct RequestReceiver { +pub struct Receiver { pub buf: Vec, + _p: PhantomData, +} + +pub trait ReceiverParser: Sized { + fn parse(bytes: &mut Iter<'_, u8>) -> Result; } pub struct DataReceiver { @@ -18,21 +23,19 @@ pub struct BdatReceiver { bytes_left: usize, } -impl RequestReceiver { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { +impl Default for Receiver { + fn default() -> Self { Self { buf: Vec::with_capacity(0), + _p: Default::default(), } } +} - pub fn ingest( - &mut self, - bytes: &mut Iter<'_, u8>, - buf: &[u8], - ) -> Result, Error> { +impl Receiver { + pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &[u8]) -> Result { if self.buf.is_empty() { - match Request::parse(bytes) { + match T::parse(bytes) { Err(Error::NeedsMoreData { bytes_left }) if bytes_left > 0 => { self.buf = buf[buf.len() - bytes_left..].to_vec(); } @@ -42,7 +45,7 @@ impl RequestReceiver { for &ch in bytes { self.buf.push(ch); if ch == b'\n' { - let result = Request::parse(&mut self.buf.iter()); + let result = T::parse(&mut self.buf.iter()); self.buf.clear(); return result; } @@ -112,7 +115,7 @@ impl BdatReceiver { #[cfg(test)] mod tests { - use crate::{request::receiver::RequestReceiver, Error, Request}; + use crate::{request::receiver::Receiver, Error, Request}; use super::DataReceiver; @@ -171,7 +174,7 @@ mod tests { ), ] { let mut requests = Vec::new(); - let mut r = RequestReceiver::new(); + let mut r = Receiver::default(); for data in &data { let mut bytes = data.as_bytes().iter(); loop { diff --git a/src/response/generate.rs b/src/response/generate.rs new file mode 100644 index 0000000..cb66790 --- /dev/null +++ b/src/response/generate.rs @@ -0,0 +1,1170 @@ +use std::{ + fmt::Display, + io::{self, Write}, +}; + +use crate::{Capability, Category, EhloResponse, Mechanism, MtPriority, Response, Severity}; + +impl EhloResponse { + pub fn write(&self, mut writer: impl Write) -> io::Result<()> { + write!(writer, "250-{} says hello\r\n", self.hostname)?; + let len = self.capabilities.len(); + for (pos, capability) in self.capabilities.iter().enumerate() { + writer.write_all(b"250")?; + writer.write_all(if pos < len - 1 { b"-" } else { b" " })?; + match capability { + Capability::EightBitMime => write!(writer, "8BITMIME\r\n"), + Capability::Atrn => write!(writer, "ATRN\r\n"), + Capability::Auth { mechanisms } => { + writer.write_all(b"AUTH")?; + for mechanism in mechanisms { + write!(writer, " {}", mechanism)?; + } + writer.write_all(b"\r\n") + } + Capability::BinaryMime => write!(writer, "BINARYMIME\r\n"), + Capability::Burl => write!(writer, "BURL\r\n"), + Capability::Checkpoint => write!(writer, "CHECKPOINT\r\n"), + Capability::Chunking => write!(writer, "CHUNKING\r\n"), + Capability::Conneg => write!(writer, "CONNEG\r\n"), + Capability::Conperm => write!(writer, "CONPERM\r\n"), + Capability::DeliverBy { min } => { + if *min > 0 { + write!(writer, "DELIVERBY {}\r\n", min) + } else { + write!(writer, "DELIVERBY\r\n") + } + } + Capability::Dsn => write!(writer, "DSN\r\n"), + Capability::EnhancedStatusCodes => write!(writer, "ENHANCEDSTATUSCODES\r\n"), + Capability::Etrn => write!(writer, "ETRN\r\n"), + Capability::Expn => write!(writer, "EXPN\r\n"), + Capability::FutureRelease { + max_interval, + max_datetime, + } => write!( + writer, + "FUTURERELEASE {} {}\r\n", + max_interval, max_datetime + ), + Capability::Help => write!(writer, "HELP\r\n"), + Capability::MtPriority { priority } => write!( + writer, + "MT-PRIORITY {}\r\n", + match priority { + MtPriority::Mixer => "MIXER", + MtPriority::Stanag4406 => "STANAG4406", + MtPriority::Nsep => "NSEP", + } + ), + Capability::Mtrk => write!(writer, "MTRK\r\n"), + Capability::NoSoliciting { keywords } => { + if let Some(keywords) = keywords { + write!(writer, "NO-SOLICITING {}\r\n", keywords) + } else { + write!(writer, "NO-SOLICITING\r\n") + } + } + Capability::Onex => write!(writer, "ONEX\r\n"), + Capability::Pipelining => write!(writer, "PIPELINING\r\n"), + Capability::RequireTls => write!(writer, "REQUIRETLS\r\n"), + Capability::Rrvs => write!(writer, "RRVS\r\n"), + Capability::Size { size } => { + if *size > 0 { + write!(writer, "SIZE {}\r\n", size) + } else { + write!(writer, "SIZE\r\n") + } + } + Capability::SmtpUtf8 => write!(writer, "SMTPUTF8\r\n"), + Capability::StartTls => write!(writer, "STARTTLS\r\n"), + Capability::Verb => write!(writer, "VERB\r\n"), + }?; + } + + Ok(()) + } +} + +impl Response { + pub fn write(&self, mut writer: impl Write) -> io::Result<()> { + write!( + writer, + "{}{}{} {}.{}.{} {}\r\n", + self.code[0], + self.code[1], + self.code[2], + self.esc[0], + self.esc[1], + self.esc[2], + self.message + ) + } +} + +impl Display for Mechanism { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Mechanism::_9798MDsaSha1 => "9798-M-DSA-SHA1", + Mechanism::_9798MEcdsaSha1 => "9798-M-ECDSA-SHA1", + Mechanism::_9798MRsaSha1Enc => "9798-M-RSA-SHA1-ENC", + Mechanism::_9798UDsaSha1 => "9798-U-DSA-SHA1", + Mechanism::_9798UEcdsaSha1 => "9798-U-ECDSA-SHA1", + Mechanism::_9798URsaSha1Enc => "9798-U-RSA-SHA1-ENC", + Mechanism::Anonymous => "ANONYMOUS", + Mechanism::CramMd5 => "CRAM-MD5", + Mechanism::DigestMd5 => "DIGEST-MD5", + Mechanism::EapAes128 => "EAP-AES128", + Mechanism::EapAes128Plus => "EAP-AES128-PLUS", + Mechanism::EcdhX25519Challenge => "ECDH-X25519-CHALLENGE", + Mechanism::EcdsaNist256pChallenge => "ECDSA-NIST256P-CHALLENGE", + Mechanism::External => "EXTERNAL", + Mechanism::Gs2Krb5 => "GS2-KRB5", + Mechanism::Gs2Krb5Plus => "GS2-KRB5-PLUS", + Mechanism::GssSpnego => "GSS-SPNEGO", + Mechanism::Gssapi => "GSSAPI", + Mechanism::KerberosV4 => "KERBEROS_V4", + Mechanism::KerberosV5 => "KERBEROS_V5", + Mechanism::Login => "LOGIN", + Mechanism::NmasSambaAuth => "NMAS-SAMBA-AUTH", + Mechanism::NmasAuthen => "NMAS_AUTHEN", + Mechanism::NmasLogin => "NMAS_LOGIN", + Mechanism::Ntlm => "NTLM", + Mechanism::Oauth10a => "OAUTH10A", + Mechanism::Oauthbearer => "OAUTHBEARER", + Mechanism::Openid20 => "OPENID20", + Mechanism::Otp => "OTP", + Mechanism::Plain => "PLAIN", + Mechanism::Saml20 => "SAML20", + Mechanism::ScramSha1 => "SCRAM-SHA-1", + Mechanism::ScramSha1Plus => "SCRAM-SHA-1-PLUS", + Mechanism::ScramSha256 => "SCRAM-SHA-256", + Mechanism::ScramSha256Plus => "SCRAM-SHA-256-PLUS", + Mechanism::Securid => "SECURID", + Mechanism::Skey => "SKEY", + Mechanism::Spnego => "SPNEGO", + Mechanism::SpnegoPlus => "SPNEGO-PLUS", + Mechanism::SxoverPlus => "SXOVER-PLUS", + Mechanism::Xoauth => "XOAUTH", + Mechanism::Xoauth2 => "XOAUTH2", + Mechanism::Unknown => "", + }) + } +} + +impl Response { + pub fn new(c0: u8, c1: u8, c2: u8, e0: u8, e1: u8, e2: u8, message: T) -> Self { + Self { + code: [c0, c1, c2], + esc: [e0, e1, e2], + message, + } + } + + /// Returns the reply's numeric status. + pub fn code(&self) -> &[u8] { + &self.code + } + + /// Returns the message included in the reply. + pub fn message(&self) -> &T { + &self.message + } + + /// Returns the status severity (first digit of the status code). + pub fn severity(&self) -> Severity { + match self.code[0] / 100 { + 2 => Severity::PositiveCompletion, + 3 => Severity::PositiveIntermediate, + 4 => Severity::TransientNegativeCompletion, + 5 => Severity::PermanentNegativeCompletion, + _ => Severity::Invalid, + } + } + + /// Returns the status category (second digit of the status code). + pub fn category(&self) -> Category { + match self.code[1] { + 0 => Category::Syntax, + 1 => Category::Information, + 2 => Category::Connections, + 3 => Category::Unspecified3, + 4 => Category::Unspecified4, + 5 => Category::MailSystem, + _ => Category::Invalid, + } + } + + /// Returns the status details (third digit of the status code). + pub fn details(&self) -> u8 { + self.code[2] + } + + /// Returns `true` if the reply is a positive completion. + pub fn is_positive_completion(&self) -> bool { + self.severity() == Severity::PositiveCompletion + } + + pub fn explain_class_code(&self) -> Option<(&'static str, &'static str)> { + match self.esc[0] { + 2 => ( + "Success", + concat!( + "Success specifies that the DSN is reporting a posi", + "tive delivery action. Detail sub-codes may provide", + " notification of transformations required for deli", + "very." + ), + ) + .into(), + 4 => ( + "Persistent Transient Failure", + concat!( + "A persistent transient failure is one in which the", + " message as sent is valid, but persistence of some", + " temporary condition has caused abandonment or del", + "ay of attempts to send the message. If this code a", + "ccompanies a delivery failure report, sending in t", + "he future may be successful." + ), + ) + .into(), + 5 => ( + "Permanent Failure", + concat!( + "A permanent failure is one which is not likely to ", + "be resolved by resending the message in the curren", + "t form. Some change to the message or the destinat", + "ion must be made for successful delivery." + ), + ) + .into(), + _ => None, + } + } + + pub fn explain_subject_code(&self) -> Option<(&'static str, &'static str)> { + match self.esc[1] { + 0 => ( + "Other or Undefined Status", + concat!("There is no additional subject information availab", "le."), + ) + .into(), + 1 => ( + "Addressing Status", + concat!( + "The address status reports on the originator or de", + "stination address. It may include address syntax o", + "r validity. These errors can generally be correcte", + "d by the sender and retried." + ), + ) + .into(), + 2 => ( + "Mailbox Status", + concat!( + "Mailbox status indicates that something having to ", + "do with the mailbox has caused this DSN. Mailbox i", + "ssues are assumed to be under the general control ", + "of the recipient." + ), + ) + .into(), + 3 => ( + "Mail System Status", + concat!( + "Mail system status indicates that something having", + " to do with the destination system has caused this", + " DSN. System issues are assumed to be under the ge", + "neral control of the destination system administra", + "tor." + ), + ) + .into(), + 4 => ( + "Network and Routing Status", + concat!( + "The networking or routing codes report status abou", + "t the delivery system itself. These system compone", + "nts include any necessary infrastructure such as d", + "irectory and routing services. Network issues are ", + "assumed to be under the control of the destination", + " or intermediate system administrator." + ), + ) + .into(), + 5 => ( + "Mail Delivery Protocol Status", + concat!( + "The mail delivery protocol status codes report fai", + "lures involving the message delivery protocol. The", + "se failures include the full range of problems res", + "ulting from implementation errors or an unreliable", + " connection." + ), + ) + .into(), + 6 => ( + "Message Content or Media Status", + concat!( + "The message content or media status codes report f", + "ailures involving the content of the message. Thes", + "e codes report failures due to translation, transc", + "oding, or otherwise unsupported message media. Mes", + "sage content or media issues are under the control", + " of both the sender and the receiver, both of whic", + "h must support a common set of supported content-t", + "ypes." + ), + ) + .into(), + 7 => ( + "Security or Policy Status", + concat!( + "The security or policy status codes report failure", + "s involving policies such as per-recipient or per-", + "host filtering and cryptographic operations. Secur", + "ity and policy status issues are assumed to be und", + "er the control of either or both the sender and re", + "cipient. Both the sender and recipient must permit", + " the exchange of messages and arrange the exchange", + " of necessary keys and certificates for cryptograp", + "hic operations." + ), + ) + .into(), + + _ => None, + } + } + + pub fn explain_status_code(&self) -> Option<(&'static str, &'static str)> { + match (self.esc[1], self.esc[2]) { + (0, 0) => ( + "Other undefined Status", + concat!( + "Other undefined status is the only undefined error", + " code." + ), + ) + .into(), + (1, 0) => ( + "Other address status", + concat!( + "Something about the address specified in the messa", + "ge caused this DSN." + ), + ) + .into(), + (1, 1) => ( + "Bad destination mailbox address", + concat!( + "The mailbox specified in the address does not exis", + "t. For Internet mail names, this means the address", + " portion to the left of the \"@\" sign is invalid.", + " This code is only useful for permanent failures." + ), + ) + .into(), + (1, 2) => ( + "Bad destination system address", + concat!( + "The destination system specified in the address do", + "es not exist or is incapable of accepting mail. Fo", + "r Internet mail names, this means the address port", + "ion to the right of the \"@\" is invalid for mail.", + " This code is only useful for permanent failures." + ), + ) + .into(), + (1, 3) => ( + "Bad destination mailbox address syntax", + concat!( + "The destination address was syntactically invalid.", + " This can apply to any field in the address. This ", + "code is only useful for permanent failures." + ), + ) + .into(), + (1, 4) => ( + "Destination mailbox address ambiguous", + concat!( + "The mailbox address as specified matches one or mo", + "re recipients on the destination system. This may ", + "result if a heuristic address mapping algorithm is", + " used to map the specified address to a local mail", + "box name." + ), + ) + .into(), + (1, 5) => ( + "Destination address valid", + concat!( + "This mailbox address as specified was valid. This ", + "status code should be used for positive delivery r", + "eports." + ), + ) + .into(), + (1, 6) => ( + "Destination mailbox has moved, No forwarding address", + concat!( + "The mailbox address provided was at one time valid", + ", but mail is no longer being accepted for that ad", + "dress. This code is only useful for permanent fail", + "ures." + ), + ) + .into(), + (1, 7) => ( + "Bad sender's mailbox address syntax", + concat!( + "The sender's address was syntactically invalid. Th", + "is can apply to any field in the address." + ), + ) + .into(), + (1, 8) => ( + "Bad sender's system address", + concat!( + "The sender's system specified in the address does ", + "not exist or is incapable of accepting return mail", + ". For domain names, this means the address portion", + " to the right of the \"@\" is invalid for mail." + ), + ) + .into(), + (1, 9) => ( + "Message relayed to non-compliant mailer", + concat!( + "The mailbox address specified was valid, but the m", + "essage has been relayed to a system that does not ", + "speak this protocol; no further information can be", + " provided." + ), + ) + .into(), + (1, 10) => ( + "Recipient address has null MX", + concat!( + "This status code is returned when the associated a", + "ddress is marked as invalid using a null MX." + ), + ) + .into(), + (2, 0) => ( + "Other or undefined mailbox status", + concat!( + "The mailbox exists, but something about the destin", + "ation mailbox has caused the sending of this DSN." + ), + ) + .into(), + (2, 1) => ( + "Mailbox disabled, not accepting messages", + concat!( + "The mailbox exists, but is not accepting messages.", + " This may be a permanent error if the mailbox will", + " never be re-enabled or a transient error if the m", + "ailbox is only temporarily disabled." + ), + ) + .into(), + (2, 2) => ( + "Mailbox full", + concat!( + "The mailbox is full because the user has exceeded ", + "a per-mailbox administrative quota or physical cap", + "acity. The general semantics implies that the reci", + "pient can delete messages to make more space avail", + "able. This code should be used as a persistent tra", + "nsient failure." + ), + ) + .into(), + (2, 3) => ( + "Message length exceeds administrative limit", + concat!( + "A per-mailbox administrative message length limit ", + "has been exceeded. This status code should be used", + " when the per-mailbox message length limit is less", + " than the general system limit. This code should b", + "e used as a permanent failure." + ), + ) + .into(), + (2, 4) => ( + "Mailing list expansion problem", + concat!( + "The mailbox is a mailing list address and the mail", + "ing list was unable to be expanded. This code may ", + "represent a permanent failure or a persistent tran", + "sient failure." + ), + ) + .into(), + (3, 0) => ( + "Other or undefined mail system status", + concat!( + "The destination system exists and normally accepts", + " mail, but something about the system has caused t", + "he generation of this DSN." + ), + ) + .into(), + (3, 1) => ( + "Mail system full", + concat!( + "Mail system storage has been exceeded. The general", + " semantics imply that the individual recipient may", + " not be able to delete material to make room for a", + "dditional messages." + ), + ) + .into(), + (3, 2) => ( + "System not accepting network messages", + concat!( + "The host on which the mailbox is resident is not a", + "ccepting messages. Examples of such conditions inc", + "lude an imminent shutdown, excessive load, or syst", + "em maintenance." + ), + ) + .into(), + (3, 3) => ( + "System not capable of selected features", + concat!( + "Selected features specified for the message are no", + "t supported by the destination system. This can oc", + "cur in gateways when features from one domain cann", + "ot be mapped onto the supported feature in another", + "." + ), + ) + .into(), + (3, 4) => ( + "Message too big for system", + concat!( + "The message is larger than per-message size limit.", + " This limit may either be for physical or administ", + "rative reasons." + ), + ) + .into(), + (3, 5) => ( + "System incorrectly configured", + concat!( + "The system is not configured in a manner that will", + " permit it to accept this message." + ), + ) + .into(), + (3, 6) => ( + "Requested priority was changed", + concat!( + "The message was accepted for relay/delivery, but t", + "he requested priority (possibly the implied defaul", + "t) was not honoured. The human readable text after", + " the status code contains the new priority, follow", + "ed by SP (space) and explanatory human readable te", + "xt." + ), + ) + .into(), + (4, 0) => ( + "Other or undefined network or routing status", + concat!( + "Something went wrong with the networking, but it i", + "s not clear what the problem is, or the problem ca", + "nnot be well expressed with any of the other provi", + "ded detail codes." + ), + ) + .into(), + (4, 1) => ( + "No answer from host", + concat!( + "The outbound connection attempt was not answered, ", + "because either the remote system was busy, or was ", + "unable to take a call." + ), + ) + .into(), + (4, 2) => ( + "Bad connection", + concat!( + "The outbound connection was established, but was u", + "nable to complete the message transaction, either ", + "because of time-out, or inadequate connection qual", + "ity." + ), + ) + .into(), + (4, 3) => ( + "Directory server failure", + concat!( + "The network system was unable to forward the messa", + "ge, because a directory server was unavailable. Th", + "e inability to connect to an Internet DNS server i", + "s one example of the directory server failure erro", + "r." + ), + ) + .into(), + (4, 4) => ( + "Unable to route", + concat!( + "The mail system was unable to determine the next h", + "op for the message because the necessary routing i", + "nformation was unavailable from the directory serv", + "er. A DNS lookup returning only an SOA (Start of A", + "dministration) record for a domain name is one exa", + "mple of the unable to route error." + ), + ) + .into(), + (4, 5) => ( + "Mail system congestion", + concat!( + "The mail system was unable to deliver the message ", + "because the mail system was congested." + ), + ) + .into(), + (4, 6) => ( + "Routing loop detected", + concat!( + "A routing loop caused the message to be forwarded ", + "too many times, either because of incorrect routin", + "g tables or a user- forwarding loop." + ), + ) + .into(), + (4, 7) => ( + "Delivery time expired", + concat!( + "The message was considered too old by the rejectin", + "g system, either because it remained on that host ", + "too long or because the time-to-live value specifi", + "ed by the sender of the message was exceeded. If p", + "ossible, the code for the actual problem found whe", + "n delivery was attempted should be returned rather", + " than this code." + ), + ) + .into(), + (5, 0) => ( + "Other or undefined protocol status", + concat!( + "Something was wrong with the protocol necessary to", + " deliver the message to the next hop and the probl", + "em cannot be well expressed with any of the other ", + "provided detail codes." + ), + ) + .into(), + (5, 1) => ( + "Invalid command", + concat!( + "A mail transaction protocol command was issued whi", + "ch was either out of sequence or unsupported." + ), + ) + .into(), + (5, 2) => ( + "Syntax error", + concat!( + "A mail transaction protocol command was issued whi", + "ch could not be interpreted, either because the sy", + "ntax was wrong or the command is unrecognized." + ), + ) + .into(), + (5, 3) => ( + "Too many recipients", + concat!( + "More recipients were specified for the message tha", + "n could have been delivered by the protocol. This ", + "error should normally result in the segmentation o", + "f the message into two, the remainder of the recip", + "ients to be delivered on a subsequent delivery att", + "empt. It is included in this list in the event tha", + "t such segmentation is not possible." + ), + ) + .into(), + (5, 4) => ( + "Invalid command arguments", + concat!( + "A valid mail transaction protocol command was issu", + "ed with invalid arguments, either because the argu", + "ments were out of range or represented unrecognize", + "d features." + ), + ) + .into(), + (5, 5) => ( + "Wrong protocol version", + concat!( + "A protocol version mis-match existed which could n", + "ot be automatically resolved by the communicating ", + "parties." + ), + ) + .into(), + (5, 6) => ( + "Authentication Exchange line is too long", + concat!( + "This enhanced status code SHOULD be returned when ", + "the server fails the AUTH command due to the clien", + "t sending a [BASE64] response which is longer than", + " the maximum buffer size available for the current", + "ly selected SASL mechanism." + ), + ) + .into(), + (6, 0) => ( + "Other or undefined media error", + concat!( + "Something about the content of a message caused it", + " to be considered undeliverable and the problem ca", + "nnot be well expressed with any of the other provi", + "ded detail codes." + ), + ) + .into(), + (6, 1) => ( + "Media not supported", + concat!( + "The media of the message is not supported by eithe", + "r the delivery protocol or the next system in the ", + "forwarding path." + ), + ) + .into(), + (6, 2) => ( + "Conversion required and prohibited", + concat!( + "The content of the message must be converted befor", + "e it can be delivered and such conversion is not p", + "ermitted. Such prohibitions may be the expression ", + "of the sender in the message itself or the policy ", + "of the sending host." + ), + ) + .into(), + (6, 3) => ( + "Conversion required but not supported", + concat!( + "The message content must be converted in order to ", + "be forwarded but such conversion is not possible o", + "r is not practical by a host in the forwarding pat", + "h. This condition may result when an ESMTP gateway", + " supports 8bit transport but is not able to downgr", + "ade the message to 7 bit as required for the next ", + "hop." + ), + ) + .into(), + (6, 4) => ( + "Conversion with loss performed", + concat!( + "This is a warning sent to the sender when message ", + "delivery was successfully but when the delivery re", + "quired a conversion in which some data was lost. T", + "his may also be a permanent error if the sender ha", + "s indicated that conversion with loss is prohibite", + "d for the message." + ), + ) + .into(), + (6, 5) => ( + "Conversion Failed", + concat!( + "A conversion was required but was unsuccessful. Th", + "is may be useful as a permanent or persistent temp", + "orary notification." + ), + ) + .into(), + (6, 6) => ( + "Message content not available", + concat!( + "The message content could not be fetched from a re", + "mote system. This may be useful as a permanent or ", + "persistent temporary notification." + ), + ) + .into(), + (6, 7) => ( + "Non-ASCII addresses not permitted for that sender/recipient", + concat!( + "This indicates the reception of a MAIL or RCPT com", + "mand that non-ASCII addresses are not permitted" + ), + ) + .into(), + (6, 8 | 10) => ( + "UTF-8 string reply is required, but not permitted by the SMTP client", + concat!( + "This indicates that a reply containing a UTF-8 str", + "ing is required to show the mailbox name, but that", + " form of response is not permitted by the SMTP cli", + "ent." + ), + ) + .into(), + (6, 9) => ( + concat!( + "UTF-8 header message cannot be transferred to ", + "one or more recipients, so the message must be rejected" + ), + concat!( + "This indicates that transaction failed after the f", + "inal \".\" of the DATA command." + ), + ) + .into(), + (7, 0) => ( + "Other or undefined security status", + concat!( + "Something related to security caused the message t", + "o be returned, and the problem cannot be well expr", + "essed with any of the other provided detail codes.", + " This status code may also be used when the condit", + "ion cannot be further described because of securit", + "y policies in force." + ), + ) + .into(), + (7, 1) => ( + "Delivery not authorized, message refused", + concat!( + "The sender is not authorized to send to the destin", + "ation. This can be the result of per-host or per-r", + "ecipient filtering. This memo does not discuss the", + " merits of any such filtering, but provides a mech", + "anism to report such." + ), + ) + .into(), + (7, 2) => ( + "Mailing list expansion prohibited", + concat!( + "The sender is not authorized to send a message to ", + "the intended mailing list." + ), + ) + .into(), + (7, 3) => ( + "Security conversion required but not possible", + concat!( + "A conversion from one secure messaging protocol to", + " another was required for delivery and such conver", + "sion was not possible." + ), + ) + .into(), + (7, 4) => ( + "Security features not supported", + concat!( + "A message contained security features such as secu", + "re authentication that could not be supported on t", + "he delivery protocol." + ), + ) + .into(), + (7, 5) => ( + "Cryptographic failure", + concat!( + "A transport system otherwise authorized to validat", + "e or decrypt a message in transport was unable to ", + "do so because necessary information such as key wa", + "s not available or such information was invalid." + ), + ) + .into(), + (7, 6) => ( + "Cryptographic algorithm not supported", + concat!( + "A transport system otherwise authorized to validat", + "e or decrypt a message was unable to do so because", + " the necessary algorithm was not supported." + ), + ) + .into(), + (7, 7) => ( + "Message integrity failure", + concat!( + "A transport system otherwise authorized to validat", + "e a message was unable to do so because the messag", + "e was corrupted or altered. This may be useful as ", + "a permanent, transient persistent, or successful d", + "elivery code." + ), + ) + .into(), + (7, 8) => ( + "Authentication credentials invalid", + concat!( + "This response to the AUTH command indicates that t", + "he authentication failed due to invalid or insuffi", + "cient authentication credentials. In this case, th", + "e client SHOULD ask the user to supply new credent", + "ials (such as by presenting a password dialog box)", + "." + ), + ) + .into(), + (7, 9) => ( + "Authentication mechanism is too weak", + concat!( + "This response to the AUTH command indicates that t", + "he selected authentication mechanism is weaker tha", + "n server policy permits for that user. The client ", + "SHOULD retry with a new authentication mechanism." + ), + ) + .into(), + (7, 10) => ( + "Encryption Needed", + concat!( + "This indicates that external strong privacy layer ", + "is needed in order to use the requested authentica", + "tion mechanism. This is primarily intended for use", + " with clear text authentication mechanisms. A clie", + "nt which receives this may activate a security lay", + "er such as TLS prior to authenticating, or attempt", + " to use a stronger mechanism." + ), + ) + .into(), + (7, 11) => ( + "Encryption required for requested authentication mechanism", + concat!( + "This response to the AUTH command indicates that t", + "he selected authentication mechanism may only be u", + "sed when the underlying SMTP connection is encrypt", + "ed. Note that this response code is documented her", + "e for historical purposes only. Modern implementat", + "ions SHOULD NOT advertise mechanisms that are not ", + "permitted due to lack of encryption, unless an enc", + "ryption layer of sufficient strength is currently ", + "being employed." + ), + ) + .into(), + (7, 12) => ( + "A password transition is needed", + concat!( + "This response to the AUTH command indicates that t", + "he user needs to transition to the selected authen", + "tication mechanism. This is typically done by auth", + "enticating once using the [PLAIN] authentication m", + "echanism. The selected mechanism SHOULD then work ", + "for authentications in subsequent sessions." + ), + ) + .into(), + (7, 13) => ( + "User Account Disabled", + concat!( + "Sometimes a system administrator will have to disa", + "ble a user's account (e.g., due to lack of payment", + ", abuse, evidence of a break-in attempt, etc). Thi", + "s error code occurs after a successful authenticat", + "ion to a disabled account. This informs the client", + " that the failure is permanent until the user cont", + "acts their system administrator to get the account", + " re-enabled. It differs from a generic authenticat", + "ion failure where the client's best option is to p", + "resent the passphrase entry dialog in case the use", + "r simply mistyped their passphrase." + ), + ) + .into(), + (7, 14) => ( + "Trust relationship required", + concat!( + "The submission server requires a configured trust ", + "relationship with a third-party server in order to", + " access the message content. This value replaces t", + "he prior use of X.7.8 for this error condition. th", + "ereby updating [RFC4468]." + ), + ) + .into(), + (7, 15) => ( + "Priority Level is too low", + concat!( + "The specified priority level is below the lowest p", + "riority acceptable for the receiving SMTP server. ", + "This condition might be temporary, for example the", + " server is operating in a mode where only higher p", + "riority messages are accepted for transfer and del", + "ivery, while lower priority messages are rejected." + ), + ) + .into(), + (7, 16) => ( + "Message is too big for the specified priority", + concat!( + "The message is too big for the specified priority.", + " This condition might be temporary, for example th", + "e server is operating in a mode where only higher ", + "priority messages below certain size are accepted ", + "for transfer and delivery." + ), + ) + .into(), + (7, 17) => ( + "Mailbox owner has changed", + concat!( + "This status code is returned when a message is rec", + "eived with a Require-Recipient-Valid-Since field o", + "r RRVS extension and the receiving system is able ", + "to determine that the intended recipient mailbox h", + "as not been under continuous ownership since the s", + "pecified date-time." + ), + ) + .into(), + (7, 18) => ( + "Domain owner has changed", + concat!( + "This status code is returned when a message is rec", + "eived with a Require-Recipient-Valid-Since field o", + "r RRVS extension and the receiving system wishes t", + "o disclose that the owner of the domain name of th", + "e recipient has changed since the specified date-t", + "ime." + ), + ) + .into(), + (7, 19) => ( + "RRVS test cannot be completed", + concat!( + "This status code is returned when a message is rec", + "eived with a Require-Recipient-Valid-Since field o", + "r RRVS extension and the receiving system cannot c", + "omplete the requested evaluation because the requi", + "red timestamp was not recorded. The message origin", + "ator needs to decide whether to reissue the messag", + "e without RRVS protection." + ), + ) + .into(), + (7, 20) => ( + "No passing DKIM signature found", + concat!( + "This status code is returned when a message did no", + "t contain any passing DKIM signatures. (This viola", + "tes the advice of Section 6.1 of [RFC6376].)" + ), + ) + .into(), + (7, 21) => ( + "No acceptable DKIM signature found", + concat!( + "This status code is returned when a message contai", + "ns one or more passing DKIM signatures, but none a", + "re acceptable. (This violates the advice of Sectio", + "n 6.1 of [RFC6376].)" + ), + ) + .into(), + (7, 22) => ( + "No valid author-matched DKIM signature found", + concat!( + "This status code is returned when a message contai", + "ns one or more passing DKIM signatures, but none a", + "re acceptable because none have an identifier(s) t", + "hat matches the author address(es) found in the Fr", + "om header field. This is a special case of X.7.21.", + " (This violates the advice of Section 6.1 of [RFC6", + "376].)" + ), + ) + .into(), + (7, 23) => ( + "SPF validation failed", + concat!( + "This status code is returned when a message comple", + "ted an SPF check that produced a \"fail\" result, ", + "contrary to local policy requirements. Used in pla", + "ce of 5.7.1 as described in Section 8.4 of [RFC720", + "8]." + ), + ) + .into(), + (7, 24) => ( + "SPF validation error", + concat!( + "This status code is returned when evaluation of SP", + "F relative to an arriving message resulted in an e", + "rror. Used in place of 4.4.3 or 5.5.2 as described", + " in Sections 8.6 and 8.7 of [RFC7208]." + ), + ) + .into(), + (7, 25) => ( + "Reverse DNS validation failed", + concat!( + "This status code is returned when an SMTP client's", + " IP address failed a reverse DNS validation check,", + " contrary to local policy requirements." + ), + ) + .into(), + (7, 26) => ( + "Multiple authentication checks failed", + concat!( + "This status code is returned when a message failed", + " more than one message authentication check, contr", + "ary to local policy requirements. The particular m", + "echanisms that failed are not specified." + ), + ) + .into(), + (7, 27) => ( + "Sender address has null MX", + concat!( + "This status code is returned when the associated s", + "ender address has a null MX, and the SMTP receiver", + " is configured to reject mail from such sender (e.", + "g., because it could not return a DSN)." + ), + ) + .into(), + (7, 28) => ( + "Mail flood detected", + concat!( + "The message appears to be part of a mail flood of ", + "similar abusive messages." + ), + ) + .into(), + (7, 29) => ( + "ARC validation failure", + concat!( + "This status code may be returned when a message fa", + "ils ARC validation." + ), + ) + .into(), + (7, 30) => ( + "REQUIRETLS support required", + concat!( + "This indicates that the message was not able to be", + " forwarded because it was received with a REQUIRET", + "LS requirement and none of the SMTP servers to whi", + "ch the message should be forwarded provide this su", + "pport." + ), + ) + .into(), + _ => None, + } + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs index 5d06867..67f5f14 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,3 +1,4 @@ +pub mod generate; pub mod parser; pub(crate) const _8BITMIME: u128 = (b'8' as u128) diff --git a/src/response/parser.rs b/src/response/parser.rs index f0480e1..7f73b40 100644 --- a/src/response/parser.rs +++ b/src/response/parser.rs @@ -1,14 +1,14 @@ use std::slice::Iter; use crate::{ - request::parser::Rfc5321Parser, Capability, EhloResponse, Error, IntoString, MtPriority, - Response, LF, + request::{parser::Rfc5321Parser, receiver::ReceiverParser}, + Capability, EhloResponse, Error, IntoString, MtPriority, Response, LF, }; use super::*; -impl EhloResponse { - pub fn parse(bytes: &mut Iter<'_, u8>) -> Result { +impl ReceiverParser for EhloResponse { + fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { let mut parser = Rfc5321Parser::new(bytes); let mut response = EhloResponse { hostname: String::new(), @@ -16,17 +16,29 @@ impl EhloResponse { }; let mut eol = false; let mut buf = Vec::with_capacity(32); - let mut code = u16::MAX; + let mut code = [0u8; 3]; let mut is_first_line = true; + let mut did_success = false; while !eol { - code = parser.size()? as u16; - match parser.stop_char { + for code in code.iter_mut() { + match parser.read_char()? { + ch @ b'0'..=b'9' => { + *code = ch - b'0'; + } + _ => { + return Err(Error::SyntaxError { + syntax: "unexpected token", + }); + } + } + } + match parser.read_char()? { b' ' => { eol = true; } b'-' => (), - b'\n' if code < 600 => { + b'\n' if code[0] < 6 => { break; } _ => { @@ -36,7 +48,9 @@ impl EhloResponse { } } - if !is_first_line && code == 250 { + did_success = code[0] == 2 && code[1] == 5 && code[2] == 0; + + if !is_first_line && did_success { response .capabilities .push(match parser.hashed_value_long()? { @@ -157,18 +171,17 @@ impl EhloResponse { }); 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' '); + } else if !buf.is_empty() { + buf.extend_from_slice(b"\r\n"); } loop { match parser.read_char()? { b'\n' => break, b'\r' => (), - b' ' if is_hostname => { + b' ' if did_success => { parser.seek_lf()?; break; } @@ -178,14 +191,14 @@ impl EhloResponse { } } - if is_hostname { + if did_success { response.hostname = buf.into_string(); buf = Vec::new(); } } } - if code == 250 { + if did_success { Ok(response) } else { Err(Error::InvalidResponse { @@ -199,25 +212,33 @@ impl EhloResponse { } } -impl Response { - pub fn parse(bytes: &mut Iter<'_, u8>, has_esc: bool) -> Result { +impl Response { + pub fn parse(bytes: &mut Iter<'_, u8>, has_esc: bool) -> Result, Error> { let mut parser = Rfc5321Parser::new(bytes); - let mut code = 0; + let mut code = [0u8; 3]; 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 { + for code in code.iter_mut() { + match parser.read_char()? { + ch @ b'0'..=b'9' => { + *code = ch - b'0'; + } + _ => { + return Err(Error::SyntaxError { + syntax: "unexpected token", + }) + } + } + } + match parser.read_char()? { b' ' => { eol = true; } b'-' => (), - b'\n' if code < 600 => { + b'\n' if code[0] < 6 => { break; } _ => { @@ -352,7 +373,10 @@ impl Capability { #[cfg(test)] mod tests { - use crate::{Capability, EhloResponse, Error, Mechanism, MtPriority, Response}; + use crate::{ + request::receiver::ReceiverParser, Capability, EhloResponse, Error, Mechanism, MtPriority, + Response, + }; #[test] fn parse_ehlo() { @@ -460,33 +484,24 @@ mod tests { concat!("523-Massive\n", "523-Error\n", "523 Message\n"), Err(Error::InvalidResponse { response: Response { - code: 523, + code: [5, 2, 3], esc: [0, 0, 0], - message: "Massive Error Message".to_string(), + message: "Massive\r\nError\r\nMessage".to_string(), }, }), ), ] { - let (response, parsed_response): (&str, Result) = item; + let (response, parsed_response): (&str, Result, Error>) = item; for replacement in ["", "\r\n", " \n", " \r\n"] { - let response = if !replacement.is_empty() { + let response = if !replacement.is_empty() && parsed_response.is_ok() { 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, - }), + EhloResponse::parse(&mut response.as_bytes().iter()), "failed for {:?}", response ); @@ -500,7 +515,7 @@ mod tests { ( "250 2.1.1 Originator ok\n", Response { - code: 250, + code: [2, 5, 0], esc: [2, 1, 1], message: "Originator ok".to_string(), }, @@ -512,7 +527,7 @@ mod tests { "551 5.7.1 Select another host to act as your forwarder\n" ), Response { - code: 551, + code: [5, 5, 1], esc: [5, 7, 1], message: concat!( "Forwarding to remote hosts disabled ", @@ -528,7 +543,7 @@ mod tests { "550 user has moved with no forwarding address\n" ), Response { - code: 550, + code: [5, 5, 0], esc: [0, 0, 0], message: "mailbox unavailable user has moved with no forwarding address" .to_string(), @@ -541,7 +556,7 @@ mod tests { "550 user has moved with no forwarding address\n" ), Response { - code: 550, + code: [5, 5, 0], esc: [0, 0, 0], message: "mailbox unavailable user has moved with no forwarding address" .to_string(), @@ -562,7 +577,7 @@ mod tests { "432 6.8.9 World!\n" ), Response { - code: 432, + code: [4, 3, 2], esc: [6, 8, 9], message: "Hello , World!".to_string(), }, @@ -571,7 +586,7 @@ mod tests { ( concat!("250-Missing space\n", "250\n", "250 Ignore this"), Response { - code: 250, + code: [2, 5, 0], esc: [0, 0, 0], message: "Missing space".to_string(), },