diff --git a/.gitignore b/.gitignore index 088ba6b..b7e28fc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + + +# Added by cargo + +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4bf979e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "smtp-proto" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0db3ec9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,194 @@ +pub mod request; +pub mod response; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Request { + Ehlo { + host: T, + }, + Lhlo { + host: T, + }, + Helo { + host: T, + }, + Mail { + from: T, + parameters: Vec>, + }, + Rcpt { + to: T, + parameters: Vec>, + }, + Bdat { + chunk_size: usize, + is_last: bool, + }, + Auth { + mechanism: Mechanism, + initial_response: T, + }, + Noop { + value: T, + }, + Vrfy { + value: T, + }, + Expn { + value: T, + }, + Help { + value: T, + }, + Etrn { + name: T, + }, + Atrn { + domains: Vec, + }, + Burl { + uri: T, + is_last: bool, + }, + StartTls, + Data, + Rset, + Quit, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Parameter { + Body(Body), + Size(usize), + TransId(T), + By(By), + Notify(u8), + Orcpt(Orcpt), + Ret(Ret), + EnvId(T), + Solicit(T), + Mtrk(Mtrk), + Auth(T), + HoldFor(u64), + HoldUntil(u64), + MtPriority(i64), + Rrvs(Rrvs), + SmtpUtf8, + RequireTls, + ConPerm, + ConNeg, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Body { + SevenBit, + EightBitMime, + BinaryMime, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Orcpt { + pub addr_type: T, + pub addr: T, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ret { + Full, + Hdrs, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Mtrk { + pub certifier: T, + pub timeout: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum By { + Notify { time: i64, trace: bool }, + Return { time: i64, trace: bool }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Rrvs { + Reject(i64), + Continue(i64), +} + +pub const NOTIFY_SUCCESS: u8 = 0x01; +pub const NOTIFY_FAILURE: u8 = 0x02; +pub const NOTIFY_DELAY: u8 = 0x04; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Mechanism { + _9798MDsaSha1, + _9798MEcdsaSha1, + _9798MRsaSha1Enc, + _9798UDsaSha1, + _9798UEcdsaSha1, + _9798URsaSha1Enc, + Anonymous, + CramMd5, + DigestMd5, + EapAes128, + EapAes128Plus, + EcdhX25519Challenge, + EcdsaNist256pChallenge, + External, + Gs2Krb5, + Gs2Krb5Plus, + GssSpnego, + Gssapi, + KerberosV4, + KerberosV5, + Login, + NmasSambaAuth, + NmasAuthen, + NmasLogin, + Ntlm, + Oauth10a, + Oauthbearer, + Openid20, + Otp, + Plain, + Saml20, + ScramSha1, + ScramSha1Plus, + ScramSha256, + ScramSha256Plus, + Securid, + Skey, + Spnego, + SpnegoPlus, + SxoverPlus, + Xoauth, + Xoauth2, + // Unknown + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + NeedsMoreData, + UnknownCommand, + InvalidAddress, + SyntaxError { syntax: &'static str }, + InvalidParameter { param: &'static str }, + UnsupportedParameter { param: String }, + UnexpectedChar { char: u8 }, +} + +pub(crate) const LF: u8 = b'\n'; +pub(crate) const SP: u8 = b' '; + +pub(crate) trait IntoString: Sized { + fn into_string(self) -> String; +} + +impl IntoString for Vec { + fn into_string(self) -> String { + String::from_utf8(self) + .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) + } +} diff --git a/src/request/mod.rs b/src/request/mod.rs new file mode 100644 index 0000000..d568f5f --- /dev/null +++ b/src/request/mod.rs @@ -0,0 +1,664 @@ +pub mod parser; + +// SMTP commands +pub(crate) const EHLO: u64 = + (b'e' as u64) | (b'h' as u64) << 8 | (b'l' as u64) << 16 | (b'o' as u64) << 24; +pub(crate) const HELO: u64 = + (b'h' as u64) | (b'e' as u64) << 8 | (b'l' as u64) << 16 | (b'o' as u64) << 24; +pub(crate) const LHLO: u64 = + (b'l' as u64) | (b'h' as u64) << 8 | (b'l' as u64) << 16 | (b'o' as u64) << 24; +pub(crate) const MAIL: u64 = + (b'm' as u64) | (b'a' as u64) << 8 | (b'i' as u64) << 16 | (b'l' as u64) << 24; +pub(crate) const RCPT: u64 = + (b'r' as u64) | (b'c' as u64) << 8 | (b'p' as u64) << 16 | (b't' as u64) << 24; +pub(crate) const DATA: u64 = + (b'd' as u64) | (b'a' as u64) << 8 | (b't' as u64) << 16 | (b'a' as u64) << 24; +pub(crate) const BDAT: u64 = + (b'b' as u64) | (b'd' as u64) << 8 | (b'a' as u64) << 16 | (b't' as u64) << 24; +pub(crate) const RSET: u64 = + (b'r' as u64) | (b's' as u64) << 8 | (b'e' as u64) << 16 | (b't' as u64) << 24; +pub(crate) const VRFY: u64 = + (b'v' as u64) | (b'r' as u64) << 8 | (b'f' as u64) << 16 | (b'y' as u64) << 24; +pub(crate) const EXPN: u64 = + (b'e' as u64) | (b'x' as u64) << 8 | (b'p' as u64) << 16 | (b'n' as u64) << 24; +pub(crate) const HELP: u64 = + (b'h' as u64) | (b'e' as u64) << 8 | (b'l' as u64) << 16 | (b'p' as u64) << 24; +pub(crate) const NOOP: u64 = + (b'n' as u64) | (b'o' as u64) << 8 | (b'o' as u64) << 16 | (b'p' as u64) << 24; +pub(crate) const QUIT: u64 = + (b'q' as u64) | (b'u' as u64) << 8 | (b'i' as u64) << 16 | (b't' as u64) << 24; +pub(crate) const ETRN: u64 = + (b'e' as u64) | (b't' as u64) << 8 | (b'r' as u64) << 16 | (b'n' as u64) << 24; +pub(crate) const ATRN: u64 = + (b'a' as u64) | (b't' as u64) << 8 | (b'r' as u64) << 16 | (b'n' as u64) << 24; +pub(crate) const AUTH: u64 = + (b'a' as u64) | (b'u' as u64) << 8 | (b't' as u64) << 16 | (b'h' as u64) << 24; +pub(crate) const BURL: u64 = + (b'b' as u64) | (b'u' as u64) << 8 | (b'r' as u64) << 16 | (b'l' as u64) << 24; +pub(crate) const STARTTLS: u64 = (b's' as u64) + | (b't' as u64) << 8 + | (b'a' as u64) << 16 + | (b'r' as u64) << 24 + | (b't' as u64) << 32 + | (b't' as u64) << 40 + | (b'l' as u64) << 48 + | (b's' as u64) << 56; + +// Arguments +pub(crate) const FROM: u64 = + (b'f' as u64) | (b'r' as u64) << 8 | (b'o' as u64) << 16 | (b'm' as u64) << 24; +pub(crate) const TO: u64 = (b't' as u64) | (b'o' as u64) << 8; +pub(crate) const LAST: u64 = + (b'l' as u64) | (b'a' as u64) << 8 | (b's' as u64) << 16 | (b't' as u64) << 24; + +// Parameters +pub(crate) const BODY: u128 = + (b'b' as u128) | (b'o' as u128) << 8 | (b'd' as u128) << 16 | (b'y' as u128) << 24; +pub(crate) const SEVENBIT: u128 = + (b'7' as u128) | (b'b' as u128) << 8 | (b'i' as u128) << 16 | (b't' as u128) << 24; +pub(crate) const EIGHBITMIME: 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 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 SIZE: u128 = + (b's' as u128) | (b'i' as u128) << 8 | (b'z' as u128) << 16 | (b'e' as u128) << 24; +pub(crate) const TRANSID: u128 = (b't' as u128) + | (b'r' as u128) << 8 + | (b'a' as u128) << 16 + | (b'n' as u128) << 24 + | (b's' as u128) << 32 + | (b'i' as u128) << 40 + | (b'd' as u128) << 48; +pub(crate) const BY: u128 = (b'b' as u128) | (b'y' as u128) << 8; + +pub(crate) const N: u64 = b'n' as u64; +pub(crate) const NT: u64 = (b'n' as u64) | (b't' as u64) << 8; +pub(crate) const C: u64 = b'c' as u64; +pub(crate) const R: u64 = b'r' as u64; +pub(crate) const RT: u64 = (b'r' as u64) | (b't' as u64) << 8; + +pub(crate) const NOTIFY: u128 = (b'n' as u128) + | (b'o' as u128) << 8 + | (b't' as u128) << 16 + | (b'i' as u128) << 24 + | (b'f' as u128) << 32 + | (b'y' as u128) << 40; +pub(crate) const ORCPT: u128 = (b'o' as u128) + | (b'r' as u128) << 8 + | (b'c' as u128) << 16 + | (b'p' as u128) << 24 + | (b't' as u128) << 32; +pub(crate) const RET: u128 = (b'r' as u128) | (b'e' as u128) << 8 | (b't' as u128) << 16; +pub(crate) const ENVID: u128 = (b'e' as u128) + | (b'n' as u128) << 8 + | (b'v' as u128) << 16 + | (b'i' as u128) << 24 + | (b'd' as u128) << 32; +pub(crate) const NEVER: u128 = (b'n' as u128) + | (b'e' as u128) << 8 + | (b'v' as u128) << 16 + | (b'e' as u128) << 24 + | (b'r' as u128) << 32; +pub(crate) const SUCCESS: u128 = (b's' as u128) + | (b'u' as u128) << 8 + | (b'c' as u128) << 16 + | (b'c' as u128) << 24 + | (b'e' as u128) << 32 + | (b's' as u128) << 40 + | (b's' as u128) << 48; +pub(crate) const FAILURE: u128 = (b'f' as u128) + | (b'a' as u128) << 8 + | (b'i' as u128) << 16 + | (b'l' as u128) << 24 + | (b'u' as u128) << 32 + | (b'r' as u128) << 40 + | (b'e' as u128) << 48; +pub(crate) const DELAY: u128 = (b'd' as u128) + | (b'e' as u128) << 8 + | (b'l' as u128) << 16 + | (b'a' as u128) << 24 + | (b'y' as u128) << 32; +pub(crate) const FULL: u64 = + (b'f' as u64) | (b'u' as u64) << 8 | (b'l' as u64) << 16 | (b'l' as u64) << 24; +pub(crate) const HDRS: u64 = + (b'h' as u64) | (b'd' as u64) << 8 | (b'r' as u64) << 16 | (b's' as u64) << 24; +pub(crate) const SOLICIT: u128 = (b's' as u128) + | (b'o' as u128) << 8 + | (b'l' as u128) << 16 + | (b'i' as u128) << 24 + | (b'c' as u128) << 32 + | (b'i' as u128) << 40 + | (b't' as u128) << 48; +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 AUTH_: u128 = + (b'a' as u128) | (b'u' as u128) << 8 | (b't' as u128) << 16 | (b'h' as u128) << 24; +pub(crate) const HOLDFOR: u128 = (b'h' as u128) + | (b'o' as u128) << 8 + | (b'l' as u128) << 16 + | (b'd' as u128) << 24 + | (b'f' as u128) << 32 + | (b'o' as u128) << 40 + | (b'r' as u128) << 48; +pub(crate) const HOLDUNTIL: u128 = (b'h' as u128) + | (b'o' as u128) << 8 + | (b'l' as u128) << 16 + | (b'd' as u128) << 24 + | (b'u' as u128) << 32 + | (b'n' as u128) << 40 + | (b't' as u128) << 48 + | (b'i' as u128) << 56 + | (b'l' as u128) << 64; +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 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 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 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 RRVS: u128 = + (b'r' as u128) | (b'r' as u128) << 8 | (b'v' as u128) << 16 | (b's' as u128) << 24; +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; + +// SASL Mechanisms +pub(crate) const _9798_M_DSA_SHA1: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'm' as u128) << 40 + | (b'-' as u128) << 48 + | (b'd' as u128) << 56 + | (b's' as u128) << 64 + | (b'a' as u128) << 72 + | (b'-' as u128) << 80 + | (b's' as u128) << 88 + | (b'h' as u128) << 96 + | (b'a' as u128) << 104 + | (b'1' as u128) << 112; +pub(crate) const _9798_M_ECDSA_SHA: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'm' as u128) << 40 + | (b'-' as u128) << 48 + | (b'e' as u128) << 56 + | (b'c' as u128) << 64 + | (b'd' as u128) << 72 + | (b's' as u128) << 80 + | (b'a' as u128) << 88 + | (b'-' as u128) << 96 + | (b's' as u128) << 104 + | (b'h' as u128) << 112 + | (b'a' as u128) << 120; +pub(crate) const _9798_M_RSA_SHA1_: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'm' as u128) << 40 + | (b'-' as u128) << 48 + | (b'r' as u128) << 56 + | (b's' as u128) << 64 + | (b'a' as u128) << 72 + | (b'-' as u128) << 80 + | (b's' as u128) << 88 + | (b'h' as u128) << 96 + | (b'a' as u128) << 104 + | (b'1' as u128) << 112 + | (b'-' as u128) << 120; +pub(crate) const _9798_U_DSA_SHA1: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'u' as u128) << 40 + | (b'-' as u128) << 48 + | (b'd' as u128) << 56 + | (b's' as u128) << 64 + | (b'a' as u128) << 72 + | (b'-' as u128) << 80 + | (b's' as u128) << 88 + | (b'h' as u128) << 96 + | (b'a' as u128) << 104 + | (b'1' as u128) << 112; +pub(crate) const _9798_U_ECDSA_SHA: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'u' as u128) << 40 + | (b'-' as u128) << 48 + | (b'e' as u128) << 56 + | (b'c' as u128) << 64 + | (b'd' as u128) << 72 + | (b's' as u128) << 80 + | (b'a' as u128) << 88 + | (b'-' as u128) << 96 + | (b's' as u128) << 104 + | (b'h' as u128) << 112 + | (b'a' as u128) << 120; +pub(crate) const _9798_U_RSA_SHA1_: u128 = (b'9' as u128) + | (b'7' as u128) << 8 + | (b'9' as u128) << 16 + | (b'8' as u128) << 24 + | (b'-' as u128) << 32 + | (b'u' as u128) << 40 + | (b'-' as u128) << 48 + | (b'r' as u128) << 56 + | (b's' as u128) << 64 + | (b'a' as u128) << 72 + | (b'-' as u128) << 80 + | (b's' as u128) << 88 + | (b'h' as u128) << 96 + | (b'a' as u128) << 104 + | (b'1' as u128) << 112 + | (b'-' as u128) << 120; +pub(crate) const ANONYMOUS: u128 = (b'a' as u128) + | (b'n' as u128) << 8 + | (b'o' as u128) << 16 + | (b'n' as u128) << 24 + | (b'y' as u128) << 32 + | (b'm' as u128) << 40 + | (b'o' as u128) << 48 + | (b'u' as u128) << 56 + | (b's' as u128) << 64; +pub(crate) const CRAM_MD5: u128 = (b'c' as u128) + | (b'r' as u128) << 8 + | (b'a' as u128) << 16 + | (b'm' as u128) << 24 + | (b'-' as u128) << 32 + | (b'm' as u128) << 40 + | (b'd' as u128) << 48 + | (b'5' as u128) << 56; +pub(crate) const DIGEST_MD5: u128 = (b'd' as u128) + | (b'i' as u128) << 8 + | (b'g' as u128) << 16 + | (b'e' as u128) << 24 + | (b's' as u128) << 32 + | (b't' as u128) << 40 + | (b'-' as u128) << 48 + | (b'm' as u128) << 56 + | (b'd' as u128) << 64 + | (b'5' as u128) << 72; +pub(crate) const EAP_AES128: u128 = (b'e' as u128) + | (b'a' as u128) << 8 + | (b'p' as u128) << 16 + | (b'-' as u128) << 24 + | (b'a' as u128) << 32 + | (b'e' as u128) << 40 + | (b's' as u128) << 48 + | (b'1' as u128) << 56 + | (b'2' as u128) << 64 + | (b'8' as u128) << 72; +pub(crate) const EAP_AES128_PLUS: u128 = (b'e' as u128) + | (b'a' as u128) << 8 + | (b'p' as u128) << 16 + | (b'-' as u128) << 24 + | (b'a' as u128) << 32 + | (b'e' as u128) << 40 + | (b's' as u128) << 48 + | (b'1' as u128) << 56 + | (b'2' as u128) << 64 + | (b'8' as u128) << 72 + | (b'-' as u128) << 80 + | (b'p' as u128) << 88 + | (b'l' as u128) << 96 + | (b'u' as u128) << 104 + | (b's' as u128) << 112; +pub(crate) const ECDH_X25519_CHAL: u128 = (b'e' as u128) + | (b'c' as u128) << 8 + | (b'd' as u128) << 16 + | (b'h' as u128) << 24 + | (b'-' as u128) << 32 + | (b'x' as u128) << 40 + | (b'2' as u128) << 48 + | (b'5' as u128) << 56 + | (b'5' as u128) << 64 + | (b'1' as u128) << 72 + | (b'9' as u128) << 80 + | (b'-' as u128) << 88 + | (b'c' as u128) << 96 + | (b'h' as u128) << 104 + | (b'a' as u128) << 112 + | (b'l' as u128) << 120; +pub(crate) const ECDSA_NIST256P_C: u128 = (b'e' as u128) + | (b'c' as u128) << 8 + | (b'd' as u128) << 16 + | (b's' as u128) << 24 + | (b'a' as u128) << 32 + | (b'-' as u128) << 40 + | (b'n' as u128) << 48 + | (b'i' as u128) << 56 + | (b's' as u128) << 64 + | (b't' as u128) << 72 + | (b'2' as u128) << 80 + | (b'5' as u128) << 88 + | (b'6' as u128) << 96 + | (b'p' as u128) << 104 + | (b'-' as u128) << 112 + | (b'c' as u128) << 120; +pub(crate) const EXTERNAL: u128 = (b'e' as u128) + | (b'x' as u128) << 8 + | (b't' as u128) << 16 + | (b'e' as u128) << 24 + | (b'r' as u128) << 32 + | (b'n' as u128) << 40 + | (b'a' as u128) << 48 + | (b'l' as u128) << 56; +pub(crate) const GS2_KRB5: u128 = (b'g' as u128) + | (b's' as u128) << 8 + | (b'2' as u128) << 16 + | (b'-' as u128) << 24 + | (b'k' as u128) << 32 + | (b'r' as u128) << 40 + | (b'b' as u128) << 48 + | (b'5' as u128) << 56; +pub(crate) const GS2_KRB5_PLUS: u128 = (b'g' as u128) + | (b's' as u128) << 8 + | (b'2' as u128) << 16 + | (b'-' as u128) << 24 + | (b'k' as u128) << 32 + | (b'r' as u128) << 40 + | (b'b' as u128) << 48 + | (b'5' as u128) << 56 + | (b'-' as u128) << 64 + | (b'p' as u128) << 72 + | (b'l' as u128) << 80 + | (b'u' as u128) << 88 + | (b's' as u128) << 96; +pub(crate) const GSS_SPNEGO: u128 = (b'g' as u128) + | (b's' as u128) << 8 + | (b's' as u128) << 16 + | (b'-' as u128) << 24 + | (b's' as u128) << 32 + | (b'p' as u128) << 40 + | (b'n' as u128) << 48 + | (b'e' as u128) << 56 + | (b'g' as u128) << 64 + | (b'o' as u128) << 72; +pub(crate) const GSSAPI: u128 = (b'g' as u128) + | (b's' as u128) << 8 + | (b's' as u128) << 16 + | (b'a' as u128) << 24 + | (b'p' as u128) << 32 + | (b'i' as u128) << 40; +pub(crate) const KERBEROS_V4: u128 = (b'k' as u128) + | (b'e' as u128) << 8 + | (b'r' as u128) << 16 + | (b'b' as u128) << 24 + | (b'e' as u128) << 32 + | (b'r' as u128) << 40 + | (b'o' as u128) << 48 + | (b's' as u128) << 56 + | (b'_' as u128) << 64 + | (b'v' as u128) << 72 + | (b'4' as u128) << 80; +pub(crate) const KERBEROS_V5: u128 = (b'k' as u128) + | (b'e' as u128) << 8 + | (b'r' as u128) << 16 + | (b'b' as u128) << 24 + | (b'e' as u128) << 32 + | (b'r' as u128) << 40 + | (b'o' as u128) << 48 + | (b's' as u128) << 56 + | (b'_' as u128) << 64 + | (b'v' as u128) << 72 + | (b'5' as u128) << 80; +pub(crate) const LOGIN: u128 = (b'l' as u128) + | (b'o' as u128) << 8 + | (b'g' as u128) << 16 + | (b'i' as u128) << 24 + | (b'n' as u128) << 32; +pub(crate) const NMAS_SAMBA_AUTH: u128 = (b'n' as u128) + | (b'm' as u128) << 8 + | (b'a' as u128) << 16 + | (b's' as u128) << 24 + | (b'-' as u128) << 32 + | (b's' as u128) << 40 + | (b'a' as u128) << 48 + | (b'm' as u128) << 56 + | (b'b' as u128) << 64 + | (b'a' as u128) << 72 + | (b'-' as u128) << 80 + | (b'a' as u128) << 88 + | (b'u' as u128) << 96 + | (b't' as u128) << 104 + | (b'h' as u128) << 112; +pub(crate) const NMAS_AUTHEN: u128 = (b'n' as u128) + | (b'm' as u128) << 8 + | (b'a' as u128) << 16 + | (b's' as u128) << 24 + | (b'_' as u128) << 32 + | (b'a' as u128) << 40 + | (b'u' as u128) << 48 + | (b't' as u128) << 56 + | (b'h' as u128) << 64 + | (b'e' as u128) << 72 + | (b'n' as u128) << 80; +pub(crate) const NMAS_LOGIN: u128 = (b'n' as u128) + | (b'm' as u128) << 8 + | (b'a' as u128) << 16 + | (b's' as u128) << 24 + | (b'_' as u128) << 32 + | (b'l' as u128) << 40 + | (b'o' as u128) << 48 + | (b'g' as u128) << 56 + | (b'i' as u128) << 64 + | (b'n' as u128) << 72; +pub(crate) const NTLM: u128 = + (b'n' as u128) | (b't' as u128) << 8 | (b'l' as u128) << 16 | (b'm' as u128) << 24; +pub(crate) const OAUTH10A: u128 = (b'o' as u128) + | (b'a' as u128) << 8 + | (b'u' as u128) << 16 + | (b't' as u128) << 24 + | (b'h' as u128) << 32 + | (b'1' as u128) << 40 + | (b'0' as u128) << 48 + | (b'a' as u128) << 56; +pub(crate) const OAUTHBEARER: u128 = (b'o' as u128) + | (b'a' as u128) << 8 + | (b'u' as u128) << 16 + | (b't' as u128) << 24 + | (b'h' as u128) << 32 + | (b'b' as u128) << 40 + | (b'e' as u128) << 48 + | (b'a' as u128) << 56 + | (b'r' as u128) << 64 + | (b'e' as u128) << 72 + | (b'r' as u128) << 80; +pub(crate) const OPENID20: u128 = (b'o' as u128) + | (b'p' as u128) << 8 + | (b'e' as u128) << 16 + | (b'n' as u128) << 24 + | (b'i' as u128) << 32 + | (b'd' as u128) << 40 + | (b'2' as u128) << 48 + | (b'0' as u128) << 56; +pub(crate) const OTP: u128 = (b'o' as u128) | (b't' as u128) << 8 | (b'p' as u128) << 16; +pub(crate) const PLAIN: u128 = (b'p' as u128) + | (b'l' as u128) << 8 + | (b'a' as u128) << 16 + | (b'i' as u128) << 24 + | (b'n' as u128) << 32; +pub(crate) const SAML20: u128 = (b's' as u128) + | (b'a' as u128) << 8 + | (b'm' as u128) << 16 + | (b'l' as u128) << 24 + | (b'2' as u128) << 32 + | (b'0' as u128) << 40; +pub(crate) const SCRAM_SHA_1: u128 = (b's' as u128) + | (b'c' as u128) << 8 + | (b'r' as u128) << 16 + | (b'a' as u128) << 24 + | (b'm' as u128) << 32 + | (b'-' as u128) << 40 + | (b's' as u128) << 48 + | (b'h' as u128) << 56 + | (b'a' as u128) << 64 + | (b'-' as u128) << 72 + | (b'1' as u128) << 80; +pub(crate) const SCRAM_SHA_1_PLUS: u128 = (b's' as u128) + | (b'c' as u128) << 8 + | (b'r' as u128) << 16 + | (b'a' as u128) << 24 + | (b'm' as u128) << 32 + | (b'-' as u128) << 40 + | (b's' as u128) << 48 + | (b'h' as u128) << 56 + | (b'a' as u128) << 64 + | (b'-' as u128) << 72 + | (b'1' as u128) << 80 + | (b'-' as u128) << 88 + | (b'p' as u128) << 96 + | (b'l' as u128) << 104 + | (b'u' as u128) << 112 + | (b's' as u128) << 120; +pub(crate) const SCRAM_SHA_256: u128 = (b's' as u128) + | (b'c' as u128) << 8 + | (b'r' as u128) << 16 + | (b'a' as u128) << 24 + | (b'm' as u128) << 32 + | (b'-' as u128) << 40 + | (b's' as u128) << 48 + | (b'h' as u128) << 56 + | (b'a' as u128) << 64 + | (b'-' as u128) << 72 + | (b'2' as u128) << 80 + | (b'5' as u128) << 88 + | (b'6' as u128) << 96; +pub(crate) const SCRAM_SHA_256_PL: u128 = (b's' as u128) + | (b'c' as u128) << 8 + | (b'r' as u128) << 16 + | (b'a' as u128) << 24 + | (b'm' as u128) << 32 + | (b'-' as u128) << 40 + | (b's' as u128) << 48 + | (b'h' as u128) << 56 + | (b'a' as u128) << 64 + | (b'-' as u128) << 72 + | (b'2' as u128) << 80 + | (b'5' as u128) << 88 + | (b'6' as u128) << 96 + | (b'-' as u128) << 104 + | (b'p' as u128) << 112 + | (b'l' as u128) << 120; +pub(crate) const SECURID: u128 = (b's' as u128) + | (b'e' as u128) << 8 + | (b'c' as u128) << 16 + | (b'u' as u128) << 24 + | (b'r' as u128) << 32 + | (b'i' as u128) << 40 + | (b'd' as u128) << 48; +pub(crate) const SKEY: u128 = + (b's' as u128) | (b'k' as u128) << 8 | (b'e' as u128) << 16 | (b'y' as u128) << 24; +pub(crate) const SPNEGO: u128 = (b's' as u128) + | (b'p' as u128) << 8 + | (b'n' as u128) << 16 + | (b'e' as u128) << 24 + | (b'g' as u128) << 32 + | (b'o' as u128) << 40; +pub(crate) const SPNEGO_PLUS: u128 = (b's' as u128) + | (b'p' as u128) << 8 + | (b'n' as u128) << 16 + | (b'e' as u128) << 24 + | (b'g' as u128) << 32 + | (b'o' as u128) << 40 + | (b'-' as u128) << 48 + | (b'p' as u128) << 56 + | (b'l' as u128) << 64 + | (b'u' as u128) << 72 + | (b's' as u128) << 80; +pub(crate) const SXOVER_PLUS: u128 = (b's' as u128) + | (b'x' as u128) << 8 + | (b'o' as u128) << 16 + | (b'v' as u128) << 24 + | (b'e' as u128) << 32 + | (b'r' as u128) << 40 + | (b'-' as u128) << 48 + | (b'p' as u128) << 56 + | (b'l' as u128) << 64 + | (b'u' as u128) << 72 + | (b's' as u128) << 80; +pub(crate) const XOAUTH: u128 = (b'x' as u128) + | (b'o' as u128) << 8 + | (b'a' as u128) << 16 + | (b'u' as u128) << 24 + | (b't' as u128) << 32 + | (b'h' as u128) << 40; +pub(crate) const XOAUTH2: u128 = (b'x' as u128) + | (b'o' as u128) << 8 + | (b'a' as u128) << 16 + | (b'u' as u128) << 24 + | (b't' as u128) << 32 + | (b'h' as u128) << 40 + | (b'2' as u128) << 48; + +/* + * Adapted from Daniel Lemire's source: + * https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2019/04/17/hexparse.cpp + * + */ + +pub(crate) static HEX_MAP: &[i8] = &[ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, + 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; diff --git a/src/request/parser.rs b/src/request/parser.rs new file mode 100644 index 0000000..0d9ac69 --- /dev/null +++ b/src/request/parser.rs @@ -0,0 +1,1895 @@ +use std::slice::Iter; + +use crate::{ + Body, By, Error, IntoString, Mechanism, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, LF, + NOTIFY_DELAY, NOTIFY_FAILURE, NOTIFY_SUCCESS, SP, +}; + +use super::*; + +impl Request { + pub fn parse<'x>(bytes: &'x mut Iter<'x, u8>) -> Result, Error> { + let mut parser = RequestParser::new(bytes); + let command = parser.hashed_value()?; + if !parser.stop_char.is_ascii_whitespace() { + return Err(Error::UnknownCommand); + } + + match command { + RCPT => { + if !(parser.stop_char == LF + || parser.hashed_value()? != TO + || parser.stop_char != b':' && parser.next_char()? != b':') + && parser.next_char()? == b'<' + { + if let Some(to) = parser.address()? { + if parser.stop_char == b'>' { + return Ok(Request::Rcpt { + to, + parameters: parser.parameters()?, + }); + } + } else { + parser.seek_lf()?; + return Err(Error::InvalidAddress); + } + } + parser.seek_lf()?; + Err(Error::SyntaxError { + syntax: "RCPT TO: [parameters]", + }) + } + MAIL => { + if !(parser.stop_char == LF + || parser.hashed_value()? != FROM + || parser.stop_char != b':' && parser.next_char()? != b':') + && parser.next_char()? == b'<' + { + if let Some(from) = parser.address()? { + if parser.stop_char == b'>' { + return Ok(Request::Mail { + from, + parameters: parser.parameters()?, + }); + } + } else { + parser.seek_lf()?; + return Err(Error::InvalidAddress); + } + } + + parser.seek_lf()?; + Err(Error::SyntaxError { + syntax: "MAIL FROM: [parameters]", + }) + } + DATA => { + parser.seek_lf()?; + Ok(Request::Data) + } + EHLO => { + if parser.stop_char != LF { + let host = parser.text()?; + parser.seek_lf()?; + if !host.is_empty() { + return Ok(Request::Ehlo { host }); + } + } + Err(Error::SyntaxError { + syntax: "EHLO domain", + }) + } + BDAT => { + if parser.stop_char != LF { + let chunk_size = parser.size()?; + if chunk_size != usize::MAX && parser.stop_char.is_ascii_whitespace() { + if parser.stop_char != LF { + match parser.hashed_value()? { + LAST => { + parser.seek_lf()?; + return Ok(Request::Bdat { + chunk_size, + is_last: true, + }); + } + 0 => { + parser.seek_lf()?; + return Ok(Request::Bdat { + chunk_size, + is_last: false, + }); + } + _ => (), + } + } else { + return Ok(Request::Bdat { + chunk_size, + is_last: false, + }); + } + } + } + parser.seek_lf()?; + Err(Error::SyntaxError { + syntax: "BDAT chunk-size [LAST]", + }) + } + AUTH => { + if parser.stop_char != LF { + if let Some(mechanism) = parser.mechanism()? { + let initial_response = if parser.stop_char != LF { + parser.text()? + } else { + String::new() + }; + parser.seek_lf()?; + return Ok(Request::Auth { + mechanism, + initial_response, + }); + } + } + Err(Error::SyntaxError { + syntax: "AUTH mechanism [initial-response]", + }) + } + EXPN => { + if parser.stop_char != LF { + let value = parser.string()?; + parser.seek_lf()?; + if !value.is_empty() { + return Ok(Request::Expn { value }); + } + } + Err(Error::SyntaxError { + syntax: "EXPN string", + }) + } + NOOP => { + if parser.stop_char != LF { + let value = parser.string()?; + parser.seek_lf()?; + Ok(Request::Noop { value }) + } else { + Ok(Request::Noop { + value: String::new(), + }) + } + } + QUIT => { + parser.seek_lf()?; + Ok(Request::Quit) + } + LHLO => { + if parser.stop_char != LF { + let host = parser.text()?; + parser.seek_lf()?; + if !host.is_empty() { + return Ok(Request::Lhlo { host }); + } + } + Err(Error::SyntaxError { + syntax: "LHLO domain", + }) + } + RSET => { + parser.seek_lf()?; + Ok(Request::Rset) + } + VRFY => { + if parser.stop_char != LF { + let value = parser.string()?; + parser.seek_lf()?; + if !value.is_empty() { + return Ok(Request::Vrfy { value }); + } + } + Err(Error::SyntaxError { + syntax: "VRFY string", + }) + } + HELP => { + if parser.stop_char != LF { + let value = parser.string()?; + parser.seek_lf()?; + Ok(Request::Help { value }) + } else { + Ok(Request::Help { + value: String::new(), + }) + } + } + STARTTLS => { + parser.seek_lf()?; + Ok(Request::StartTls) + } + ETRN => { + if parser.stop_char != LF { + let name = parser.string()?; + parser.seek_lf()?; + if !name.is_empty() { + return Ok(Request::Etrn { name }); + } + } + Err(Error::SyntaxError { + syntax: "ETRN name", + }) + } + ATRN => { + if parser.stop_char != LF { + let mut domains = Vec::new(); + loop { + let domain = parser.seek_char(b',')?; + if !domain.is_empty() { + domains.push(domain); + } + if parser.stop_char != b',' { + parser.seek_lf()?; + if !domains.is_empty() { + return Ok(Request::Atrn { domains }); + } else { + break; + } + } + } + } + Err(Error::SyntaxError { + syntax: "ATRN domain[,domain]", + }) + } + BURL => { + if parser.stop_char != LF { + let uri = parser.text()?; + if !uri.is_empty() && parser.stop_char.is_ascii_whitespace() { + if parser.stop_char != LF { + match parser.hashed_value()? { + LAST => { + parser.seek_lf()?; + return Ok(Request::Burl { uri, is_last: true }); + } + 0 => { + parser.seek_lf()?; + return Ok(Request::Burl { + uri, + is_last: false, + }); + } + _ => (), + } + } else { + return Ok(Request::Burl { + uri, + is_last: false, + }); + } + } + } + parser.seek_lf()?; + Err(Error::SyntaxError { + syntax: "BURL absolute-uri [LAST]", + }) + } + HELO => { + if parser.stop_char != LF { + let host = parser.text()?; + parser.seek_lf()?; + if !host.is_empty() { + return Ok(Request::Helo { host }); + } + } + Err(Error::SyntaxError { + syntax: "HELO domain", + }) + } + _ => { + parser.seek_lf()?; + Err(Error::UnknownCommand) + } + } + } +} + +struct RequestParser<'x> { + bytes: &'x mut Iter<'x, u8>, + stop_char: u8, +} + +impl<'x> RequestParser<'x> { + pub fn new(bytes: &'x mut Iter<'x, u8>) -> Self { + RequestParser { + bytes, + stop_char: 0, + } + } + + #[allow(clippy::while_let_on_iterator)] + pub fn hashed_value(&mut self) -> Result { + let mut value: u64 = 0; + let mut shift = 0; + + 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; + shift += 8; + } + b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 64 => { + value |= (ch as u64) << shift; + shift += 8; + } + b'\r' => (), + b' ' => { + if value != 0 { + self.stop_char = ch; + return Ok(value); + } + } + _ => { + self.stop_char = ch; + return Ok(value); + } + } + } + + Err(Error::NeedsMoreData) + } + + #[allow(clippy::while_let_on_iterator)] + pub fn hashed_value_long(&mut self) -> Result { + let mut value: u128 = 0; + let mut shift = 0; + + 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; + shift += 8; + } + b'a'..=b'z' | b'0'..=b'9' | b'-' if shift < 128 => { + value |= (ch as u128) << shift; + shift += 8; + } + b' ' => { + if value != 0 { + self.stop_char = b' '; + return Ok(value); + } + } + b'\r' => (), + _ => { + self.stop_char = ch; + return Ok(value); + } + } + } + + Err(Error::NeedsMoreData) + } + + pub fn address(&mut self) -> Result, Error> { + let mut value = Vec::with_capacity(32); + let mut last_ch = 0; + let mut in_quote = false; + let mut at_count = 0; + let mut lp_len = 0; + + for &ch in &mut self.bytes { + match ch { + b'0'..=b'9' + | b'a'..=b'z' + | b'A'..=b'Z' + | b'!' + | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'/' + | b'=' + | b'?' + | b'^' + | b'_' + | b'`' + | b'{' + | b'|' + | b'}' + | b'~' + | 0x7f..=u8::MAX => { + value.push(ch); + } + b'.' if !in_quote => { + if last_ch != b'.' && last_ch != b'@' && !value.is_empty() { + value.push(ch); + } else { + self.stop_char = ch; + return Ok(None); + } + } + b'@' if !in_quote => { + at_count += 1; + lp_len = value.len(); + value.push(ch); + } + b'>' if !in_quote => { + self.stop_char = ch; + let value = value.into_string(); + let len = value.chars().count(); + return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }); + } + b'\r' => (), + b':' if !in_quote && matches!(value.first(), Some(b'@')) => { + // Remove source route + value.clear(); + at_count = 0; + lp_len = 0; + } + b',' if !in_quote && matches!(value.first(), Some(b'@')) => (), + b' ' if !in_quote => { + if !value.is_empty() { + self.stop_char = b' '; + let value = value.into_string(); + let len = value.chars().count(); + return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }); + } + } + b'\n' => { + self.stop_char = b'\n'; + let value = value.into_string(); + let len = value.chars().count(); + return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }); + } + b'\"' if !in_quote || last_ch != b'\\' => { + in_quote = !in_quote; + } + b'\\' if in_quote && last_ch != b'\\' => (), + _ => { + if in_quote { + value.push(ch); + } else { + self.stop_char = ch; + return Ok(None); + } + } + } + + last_ch = ch; + } + + Err(Error::NeedsMoreData) + } + + pub fn string(&mut self) -> Result { + let mut in_quote = false; + let mut value = Vec::with_capacity(32); + let mut last_ch = 0; + + for &ch in &mut self.bytes { + match ch { + b' ' if !in_quote => { + if !value.is_empty() { + self.stop_char = b' '; + return Ok(value.into_string()); + } + } + b'\n' => { + self.stop_char = b'\n'; + return Ok(value.into_string()); + } + b'\"' if !in_quote || last_ch != b'\\' => { + in_quote = !in_quote; + } + b'\\' if in_quote && last_ch != b'\\' => (), + b'\r' => (), + _ => { + value.push(ch); + } + } + + last_ch = ch; + } + + Err(Error::NeedsMoreData) + } + + #[allow(clippy::while_let_on_iterator)] + pub fn text(&mut self) -> Result { + let mut value = Vec::with_capacity(32); + while let Some(&ch) = self.bytes.next() { + match ch { + b'\n' => { + self.stop_char = b'\n'; + return Ok(value.into_string()); + } + b' ' => { + self.stop_char = b' '; + return Ok(value.into_string()); + } + b'\r' => (), + _ => { + value.push(ch); + } + } + } + + Err(Error::NeedsMoreData) + } + + #[allow(clippy::while_let_on_iterator)] + pub fn xtext(&mut self) -> Result { + let mut value = Vec::with_capacity(32); + while let Some(&ch) = self.bytes.next() { + match ch { + b'\n' => { + self.stop_char = b'\n'; + return Ok(value.into_string()); + } + b'+' => { + let mut hex1 = 0; + + while let Some(&ch) = self.bytes.next() { + if ch.is_ascii_hexdigit() { + if hex1 != 0 { + let hex1 = HEX_MAP[hex1 as usize]; + let hex2 = HEX_MAP[ch as usize]; + if hex1 != -1 && hex2 != -1 { + value.push(((hex1 as u8) << 4) | hex2 as u8); + } + break; + } else { + hex1 = ch; + } + } else if ch == LF { + self.stop_char = b'\n'; + return Ok(value.into_string()); + } else { + break; + } + } + } + b' ' => { + self.stop_char = b' '; + return Ok(value.into_string()); + } + b'\r' => (), + _ => { + value.push(ch); + } + } + } + + Err(Error::NeedsMoreData) + } + + #[allow(clippy::while_let_on_iterator)] + pub fn seek_char(&mut self, stop_char: u8) -> Result { + let mut value = Vec::with_capacity(32); + while let Some(&ch) = self.bytes.next() { + match ch { + b'\n' => { + self.stop_char = b'\n'; + return Ok(value.into_string()); + } + b' ' => { + if !value.is_empty() { + self.stop_char = b' '; + return Ok(value.into_string()); + } + } + b'\r' => (), + _ => { + if ch != stop_char { + value.push(ch); + } else { + self.stop_char = ch; + return Ok(value.into_string()); + } + } + } + } + + Err(Error::NeedsMoreData) + } + + #[inline(always)] + pub fn seek_lf(&mut self) -> Result<(), Error> { + if self.stop_char != LF { + for &ch in &mut self.bytes { + if ch == LF { + return Ok(()); + } + } + Err(Error::NeedsMoreData) + } else { + Ok(()) + } + } + + #[inline(always)] + pub fn next_char(&mut self) -> Result { + for &ch in &mut self.bytes { + match ch { + b' ' | b'\r' => (), + _ => { + self.stop_char = ch; + return Ok(ch); + } + } + } + Err(Error::NeedsMoreData) + } + + pub fn size(&mut self) -> Result { + let mut value = usize::MAX; + for &ch in &mut self.bytes { + match ch { + b'0'..=b'9' => { + value = if value != usize::MAX { + value + .saturating_mul(10) + .saturating_add((ch - b'0') as usize) + } else { + (ch - b'0') as usize + }; + } + b'\r' => (), + b' ' => { + if value != usize::MAX { + self.stop_char = b' '; + return Ok(value); + } + } + _ => { + self.stop_char = ch; + return Ok(value); + } + } + } + Err(Error::NeedsMoreData) + } + + pub fn integer(&mut self) -> Result { + let mut value = i64::MAX; + let mut multiplier = 1; + + for &ch in &mut self.bytes { + match ch { + b'0'..=b'9' => { + value = if value != i64::MAX { + value.saturating_mul(10).saturating_add((ch - b'0') as i64) + } else { + (ch - b'0') as i64 + }; + } + b' ' => { + if value != i64::MAX { + self.stop_char = b' '; + return Ok(value * multiplier); + } + } + b'-' if value == i64::MAX => { + multiplier = -1; + } + b'+' if value == i64::MAX => (), + b'\r' => (), + _ => { + self.stop_char = ch; + return Ok(if value != i64::MAX { + value * multiplier + } else { + i64::MAX + }); + } + } + } + Err(Error::NeedsMoreData) + } + + pub fn timestamp(&mut self) -> Result { + let mut dt = [0u32; 8]; + let mut zone_multiplier = 1; + let mut pos = 0; + + for &ch in &mut self.bytes { + match ch { + b'0'..=b'9' if pos < 8 => { + dt[pos] = dt[pos] + .saturating_mul(10) + .saturating_add((ch - b'0') as u32); + } + b'-' if pos <= 1 || pos == 5 => { + pos += 1; + } + b'+' if pos == 5 => { + zone_multiplier = -1; + pos += 1; + } + b'T' if pos == 2 => { + pos += 1; + } + b':' if pos == 3 || pos == 4 || pos == 6 => { + pos += 1; + } + b'Z' if pos == 5 => { + pos = 8; + } + _ => { + self.stop_char = ch; + return Ok(if pos >= 7 { + // Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992 + let month = dt[1]; + let year_base = 4800; /* Before min year, multiple of 400. */ + let m_adj = month.wrapping_sub(3); /* March-based month. */ + let carry = i64::from(m_adj > month); + let adjust = if carry > 0 { 12 } else { 0 }; + let y_adj = dt[0] as i64 + year_base - carry; + let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048; + let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400; + (y_adj * 365 + leap_days + month_days as i64 + (dt[2] as i64 - 1) - 2472632) + * 86400 + + dt[3] as i64 * 3600 + + dt[4] as i64 * 60 + + dt[5] as i64 + + ((dt[6] as i64 * 3600 + dt[7] as i64 * 60) * zone_multiplier) + } else { + i64::MAX + }); + } + } + } + + Err(Error::NeedsMoreData) + } + + pub fn parameters(&mut self) -> Result>, Error> { + let mut params = Vec::new(); + while self.stop_char != LF { + let key = self.hashed_value_long()?; + match key { + SMTPUTF8 if self.stop_char.is_ascii_whitespace() => { + params.push(Parameter::SmtpUtf8); + } + BODY if self.stop_char == b'=' => match Body::try_from(self.hashed_value_long()?) { + Ok(body) if self.stop_char.is_ascii_whitespace() => { + params.push(Parameter::Body(body)); + } + _ => { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "BODY" }); + } + }, + SIZE if self.stop_char == b'=' => { + let size = self.size()?; + if size != usize::MAX && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::Size(size)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "SIZE" }); + } + } + BY if self.stop_char == b'=' => { + let time = self.integer()?; + if time != i64::MAX && self.stop_char == b';' { + let (is_notify, trace) = match self.hashed_value()? { + N if self.stop_char.is_ascii_whitespace() => (true, false), + NT if self.stop_char.is_ascii_whitespace() => (true, true), + R if self.stop_char.is_ascii_whitespace() => (false, false), + RT if self.stop_char.is_ascii_whitespace() => (false, true), + _ => { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "BY" }); + } + }; + params.push(Parameter::By(if is_notify { + By::Notify { time, trace } + } else { + By::Return { time, trace } + })); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "BY" }); + } + } + HOLDUNTIL if self.stop_char == b'=' => { + let hold = self.size()?; + if hold != usize::MAX && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::HoldUntil(hold as u64)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "HOLDUNTIL" }); + } + } + HOLDFOR if self.stop_char == b'=' => { + let hold = self.size()?; + if hold != usize::MAX && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::HoldFor(hold as u64)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "HOLDFOR" }); + } + } + NOTIFY if self.stop_char == b'=' => { + let mut notify = 0; + loop { + match self.hashed_value_long()? { + NEVER if notify == 0 => (), + SUCCESS => { + notify |= NOTIFY_SUCCESS; + } + FAILURE => { + notify |= NOTIFY_FAILURE; + } + DELAY => { + notify |= NOTIFY_DELAY; + } + _ => { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "NOTIFY" }); + } + } + if self.stop_char.is_ascii_whitespace() { + break; + } else if self.stop_char != b',' { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "NOTIFY" }); + } + } + params.push(Parameter::Notify(notify)); + } + ORCPT if self.stop_char == b'=' => { + let addr_type = self.seek_char(b';')?; + if self.stop_char != b';' { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "ORCPT" }); + } + let addr = self.xtext()?; + if self.stop_char.is_ascii_whitespace() + && !addr_type.is_empty() + && !addr.is_empty() + { + params.push(Parameter::Orcpt(Orcpt { addr_type, addr })); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "ORCPT" }); + } + } + RET if self.stop_char == b'=' => { + params.push(Parameter::Ret(match self.hashed_value()? { + FULL if self.stop_char.is_ascii_whitespace() => Ret::Full, + HDRS if self.stop_char.is_ascii_whitespace() => Ret::Hdrs, + _ => { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "RET" }); + } + })); + } + ENVID if self.stop_char == b'=' => { + let envid = self.xtext()?; + if self.stop_char.is_ascii_whitespace() && !envid.is_empty() { + params.push(Parameter::EnvId(envid)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "ENVID" }); + } + } + REQUIRETLS if self.stop_char.is_ascii_whitespace() => { + params.push(Parameter::RequireTls); + } + SOLICIT if self.stop_char == b'=' => { + let solicit = self.text()?; + if !solicit.is_empty() && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::Solicit(solicit)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "SOLICIT" }); + } + } + TRANSID if self.stop_char == b'=' => { + if self.next_char()? == b'<' { + let transid = self.seek_char(b'>')?; + if self.stop_char == b'>' && !transid.is_empty() { + params.push(Parameter::TransId(transid)); + self.stop_char = SP; + continue; + } + } + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "TRANSID" }); + } + MTRK if self.stop_char == b'=' => { + let certifier = self.seek_char(b':')?; + let timeout = if self.stop_char == b':' { + self.size()? + } else { + 0 + }; + + if !certifier.is_empty() + && self.stop_char.is_ascii_whitespace() + && timeout != usize::MAX + { + params.push(Parameter::Mtrk(Mtrk { + certifier, + timeout: timeout as u64, + })); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "MTRK" }); + } + } + AUTH_ if self.stop_char == b'=' => { + let mailbox = self.xtext()?; + if !mailbox.is_empty() && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::Auth(mailbox)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "AUTH" }); + } + } + MT_PRIORITY if self.stop_char == b'=' => { + let priority = self.integer()?; + if priority != i64::MAX && self.stop_char.is_ascii_whitespace() { + params.push(Parameter::MtPriority(priority)); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { + param: "MT-PRIORITY", + }); + } + } + RRVS if self.stop_char == b'=' => { + let time = self.timestamp()?; + if time != i64::MAX && self.stop_char.is_ascii_whitespace() + || self.stop_char == b';' + { + let is_reject = self.stop_char != b';' + || match self.hashed_value()? { + C if self.stop_char.is_ascii_whitespace() => false, + R if self.stop_char.is_ascii_whitespace() => true, + _ => { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "RRVS" }); + } + }; + params.push(Parameter::Rrvs(if is_reject { + Rrvs::Reject(time) + } else { + Rrvs::Continue(time) + })); + } else { + self.seek_lf()?; + return Err(Error::InvalidParameter { param: "RRVS" }); + } + } + CONPERM if self.stop_char.is_ascii_whitespace() => { + params.push(Parameter::ConPerm); + } + CONNEG if self.stop_char.is_ascii_whitespace() => { + params.push(Parameter::ConNeg); + } + 0 => (), + unknown => { + let mut param = Vec::with_capacity(16); + for ch in unknown.to_le_bytes() { + if ch != 0 { + param.push(ch.to_ascii_uppercase()); + } + } + if !self.stop_char.is_ascii_whitespace() { + param.push(self.stop_char.to_ascii_uppercase()); + for &ch in &mut self.bytes { + if !ch.is_ascii_whitespace() { + param.push(ch.to_ascii_uppercase()); + } else { + self.stop_char = ch; + break; + } + } + } + + self.seek_lf()?; + return Err(Error::UnsupportedParameter { + param: param.into_string(), + }); + } + } + } + + Ok(params) + } + + fn mechanism(&mut self) -> Result, Error> { + let mut trailing_chars = [0u8; 8]; + let mut pos = 0; + let mechanism = self.hashed_value_long()?; + if !self.stop_char.is_ascii_whitespace() { + trailing_chars[0] = self.stop_char; + pos += 1; + for &ch in &mut self.bytes { + if !ch.is_ascii_whitespace() { + if let Some(tch) = trailing_chars.get_mut(pos) { + *tch = ch.to_ascii_uppercase(); + } + pos += 1; + } else { + self.stop_char = ch; + break; + } + } + if !self.stop_char.is_ascii_whitespace() { + return Err(Error::NeedsMoreData); + } else if pos > 8 { + return Ok(Mechanism::Unknown.into()); + } + } + Ok(match (mechanism, &trailing_chars[..pos]) { + (_9798_M_DSA_SHA1, b"") => Mechanism::_9798MDsaSha1.into(), + (_9798_M_ECDSA_SHA, b"1") => Mechanism::_9798MEcdsaSha1.into(), + (_9798_M_RSA_SHA1_, b"ENC") => Mechanism::_9798MRsaSha1Enc.into(), + (_9798_U_DSA_SHA1, b"") => Mechanism::_9798UDsaSha1.into(), + (_9798_U_ECDSA_SHA, b"1") => Mechanism::_9798UEcdsaSha1.into(), + (_9798_U_RSA_SHA1_, b"ENC") => Mechanism::_9798URsaSha1Enc.into(), + (ANONYMOUS, b"") => Mechanism::Anonymous.into(), + (CRAM_MD5, b"") => Mechanism::CramMd5.into(), + (DIGEST_MD5, b"") => Mechanism::DigestMd5.into(), + (EAP_AES128, b"") => Mechanism::EapAes128.into(), + (EAP_AES128_PLUS, b"") => Mechanism::EapAes128Plus.into(), + (ECDH_X25519_CHAL, b"LENGE") => Mechanism::EcdhX25519Challenge.into(), + (ECDSA_NIST256P_C, b"HALLENGE") => Mechanism::EcdsaNist256pChallenge.into(), + (EXTERNAL, b"") => Mechanism::External.into(), + (GS2_KRB5, b"") => Mechanism::Gs2Krb5.into(), + (GS2_KRB5_PLUS, b"") => Mechanism::Gs2Krb5Plus.into(), + (GSS_SPNEGO, b"") => Mechanism::GssSpnego.into(), + (GSSAPI, b"") => Mechanism::Gssapi.into(), + (KERBEROS_V4, b"") => Mechanism::KerberosV4.into(), + (KERBEROS_V5, b"") => Mechanism::KerberosV5.into(), + (LOGIN, b"") => Mechanism::Login.into(), + (NMAS_SAMBA_AUTH, b"") => Mechanism::NmasSambaAuth.into(), + (NMAS_AUTHEN, b"") => Mechanism::NmasAuthen.into(), + (NMAS_LOGIN, b"") => Mechanism::NmasLogin.into(), + (NTLM, b"") => Mechanism::Ntlm.into(), + (OAUTH10A, b"") => Mechanism::Oauth10a.into(), + (OAUTHBEARER, b"") => Mechanism::Oauthbearer.into(), + (OPENID20, b"") => Mechanism::Openid20.into(), + (OTP, b"") => Mechanism::Otp.into(), + (PLAIN, b"") => Mechanism::Plain.into(), + (SAML20, b"") => Mechanism::Saml20.into(), + (SCRAM_SHA_1, b"") => Mechanism::ScramSha1.into(), + (SCRAM_SHA_1_PLUS, b"") => Mechanism::ScramSha1Plus.into(), + (SCRAM_SHA_256, b"") => Mechanism::ScramSha256.into(), + (SCRAM_SHA_256_PL, b"US") => Mechanism::ScramSha256Plus.into(), + (SECURID, b"") => Mechanism::Securid.into(), + (SKEY, b"") => Mechanism::Skey.into(), + (SPNEGO, b"") => Mechanism::Spnego.into(), + (SPNEGO_PLUS, b"") => Mechanism::SpnegoPlus.into(), + (SXOVER_PLUS, b"") => Mechanism::SxoverPlus.into(), + (XOAUTH, b"") => Mechanism::Xoauth.into(), + (XOAUTH2, b"") => Mechanism::Xoauth2.into(), + (0, b"") => None, + _ => Mechanism::Unknown.into(), + }) + } +} + +impl TryFrom for Body { + type Error = (); + + fn try_from(value: u128) -> Result { + match value { + EIGHBITMIME => Ok(Body::EightBitMime), + BINARYMIME => Ok(Body::BinaryMime), + SEVENBIT => Ok(Body::SevenBit), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Body, By, Error, Mechanism, Mtrk, Orcpt, Parameter, Request, Ret, Rrvs, NOTIFY_DELAY, + NOTIFY_FAILURE, NOTIFY_SUCCESS, + }; + + #[test] + fn parse_request() { + for item in [ + // HELO et al. + ( + "EHLO bar.com", + Ok(Request::Ehlo { + host: "bar.com".to_string(), + }), + ), + ( + "EHLO", + Err(Error::SyntaxError { + syntax: "EHLO domain", + }), + ), + ( + "HELO bar.com", + Ok(Request::Helo { + host: "bar.com".to_string(), + }), + ), + ( + "HELO", + Err(Error::SyntaxError { + syntax: "HELO domain", + }), + ), + ( + "LHLO bar.com", + Ok(Request::Lhlo { + host: "bar.com".to_string(), + }), + ), + ( + "LHLO", + Err(Error::SyntaxError { + syntax: "LHLO domain", + }), + ), + // VRFY + ( + "VRFY Hello", + Ok(Request::Vrfy { + value: "Hello".to_string(), + }), + ), + ( + "VRFY \"Hello\\\" Wo\\\\rld\"", + Ok(Request::Vrfy { + value: "Hello\" Wo\\rld".to_string(), + }), + ), + ( + "VRFY \"\"", + Err(Error::SyntaxError { + syntax: "VRFY string", + }), + ), + ( + "VRFY", + Err(Error::SyntaxError { + syntax: "VRFY string", + }), + ), + // EXPN + ( + "EXPN Hello", + Ok(Request::Expn { + value: "Hello".to_string(), + }), + ), + ( + "EXPN \"Hello\\\" Wo\\\\rld\"", + Ok(Request::Expn { + value: "Hello\" Wo\\rld".to_string(), + }), + ), + ( + "EXPN \"\"", + Err(Error::SyntaxError { + syntax: "EXPN string", + }), + ), + ( + "EXPN", + Err(Error::SyntaxError { + syntax: "EXPN string", + }), + ), + // NOOP + ( + "NOOP", + Ok(Request::Noop { + value: "".to_string(), + }), + ), + ( + "NOOP Hello", + Ok(Request::Noop { + value: "Hello".to_string(), + }), + ), + // HELP + ( + "HELP", + Ok(Request::Help { + value: "".to_string(), + }), + ), + ( + "HELP Hello", + Ok(Request::Help { + value: "Hello".to_string(), + }), + ), + // No param commands + ("DATA", Ok(Request::Data)), + ("QUIT", Ok(Request::Quit)), + ("RSET", Ok(Request::Rset)), + ("STARTTLS", Ok(Request::StartTls)), + // BDAT + ( + "BDAT 0", + Ok(Request::Bdat { + chunk_size: 0, + is_last: false, + }), + ), + ( + "BDAT 123456", + Ok(Request::Bdat { + chunk_size: 123456, + is_last: false, + }), + ), + ( + "BDAT 123456 LAST", + Ok(Request::Bdat { + chunk_size: 123456, + is_last: true, + }), + ), + ( + "BDAT", + Err(Error::SyntaxError { + syntax: "BDAT chunk-size [LAST]", + }), + ), + ( + "BDAT 123LAST", + Err(Error::SyntaxError { + syntax: "BDAT chunk-size [LAST]", + }), + ), + ( + "BDAT 123x LAST", + Err(Error::SyntaxError { + syntax: "BDAT chunk-size [LAST]", + }), + ), + ( + "BDAT LAST", + Err(Error::SyntaxError { + syntax: "BDAT chunk-size [LAST]", + }), + ), + // AUTH + ( + "AUTH GSSAPI", + Ok(Request::Auth { + mechanism: Mechanism::Gssapi, + initial_response: "".to_string(), + }), + ), + ( + "AUTH ECDSA-NIST256P-CHALLENGE =", + Ok(Request::Auth { + mechanism: Mechanism::EcdsaNist256pChallenge, + initial_response: "=".to_string(), + }), + ), + ( + "AUTH SCRAM-SHA-256-PLUS base64_goes_here", + Ok(Request::Auth { + mechanism: Mechanism::ScramSha256Plus, + initial_response: "base64_goes_here".to_string(), + }), + ), + ( + "AUTH ECDSA-NIST256P-CHALLENGE100 abcde", + Ok(Request::Auth { + mechanism: Mechanism::Unknown, + initial_response: "abcde".to_string(), + }), + ), + ( + "AUTH", + Err(Error::SyntaxError { + syntax: "AUTH mechanism [initial-response]", + }), + ), + // ETRN + ( + "ETRN Hello", + Ok(Request::Etrn { + name: "Hello".to_string(), + }), + ), + ( + "ETRN \"Hello\\\" Wo\\\\rld\"", + Ok(Request::Etrn { + name: "Hello\" Wo\\rld".to_string(), + }), + ), + ( + "ETRN \"\"", + Err(Error::SyntaxError { + syntax: "ETRN name", + }), + ), + ( + "ETRN", + Err(Error::SyntaxError { + syntax: "ETRN name", + }), + ), + // ATRN + ( + "ATRN example.org", + Ok(Request::Atrn { + domains: vec!["example.org".to_string()], + }), + ), + ( + "ATRN example.org,example.com,example.net", + Ok(Request::Atrn { + domains: vec![ + "example.org".to_string(), + "example.com".to_string(), + "example.net".to_string(), + ], + }), + ), + ( + "ATRN example.org, example.com, example.net", + Ok(Request::Atrn { + domains: vec![ + "example.org".to_string(), + "example.com".to_string(), + "example.net".to_string(), + ], + }), + ), + ( + "ATRN", + Err(Error::SyntaxError { + syntax: "ATRN domain[,domain]", + }), + ), + // BURL + ( + concat!( + "BURL imap://harry@gryffindor.example.com/outbox", + ";uidvalidity=1078863300/;uid=25;urlauth=submit+harry", + ":internal:91354a473744909de610943775f92038 LAST" + ), + Ok(Request::Burl { + uri: concat!( + "imap://harry@gryffindor.example.com/outbox", + ";uidvalidity=1078863300/;uid=25;urlauth=submit+harry", + ":internal:91354a473744909de610943775f92038" + ) + .to_string(), + is_last: true, + }), + ), + ( + "BURL imap:://test.example.org", + Ok(Request::Burl { + uri: "imap:://test.example.org".to_string(), + is_last: false, + }), + ), + ( + "BURL", + Err(Error::SyntaxError { + syntax: "BURL absolute-uri [LAST]", + }), + ), + // MAIL FROM + ( + "MAIL FROM:", + Ok(Request::Mail { + from: "JQP@bar.com".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM:<@a,@b:user@d>", + Ok(Request::Mail { + from: "user@d".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM:<\"@a,@b:\"@d>", + Ok(Request::Mail { + from: "@a,@b:@d".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM: <\" hi there! \"@d>", + Ok(Request::Mail { + from: " hi there! @d".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM : <>", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM : < >", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![], + }), + ), + ( + "MAIL FROM:", + Ok(Request::Mail { + from: "hi.there@valid.org".to_string(), + parameters: vec![], + }), + ), + ("MAIL FROM:<@invalid>", Err(Error::InvalidAddress)), + ("MAIL FROM:", Err(Error::InvalidAddress)), + ( + "MAIL FROM:", + Err(Error::InvalidAddress), + ), + ( + "MAIL FROM:", + Err(Error::InvalidAddress), + ), + ( + "MAIL FROM:", + Err(Error::InvalidAddress), + ), + ( + "MAIL FROM:<.hi.there@invalid.org>", + Err(Error::InvalidAddress), + ), + ("MAIL FROM:<@>", Err(Error::InvalidAddress)), + ("MAIL FROM:<.@.>", Err(Error::InvalidAddress)), + ( + "RCPT TO:<孫子@áéíóú.org>", + Ok(Request::Rcpt { + to: "孫子@áéíóú.org".to_string(), + parameters: vec![], + }), + ), + // RCPT TO + ( + "RCPT TO:", + Ok(Request::Rcpt { + to: "Jones@XYZ.COM".to_string(), + parameters: vec![], + }), + ), + ( + "RCPT TO:<>", + Ok(Request::Rcpt { + to: "".to_string(), + parameters: vec![], + }), + ), + // Invalid commands + ("", Err(Error::UnknownCommand)), + ("X-SPECIAL", Err(Error::UnknownCommand)), + ("DATA_", Err(Error::UnknownCommand)), + // Invalid parameters + ( + "MAIL FROM:<> HELLO=WORLD", + Err(Error::UnsupportedParameter { + param: "HELLO=WORLD".to_string(), + }), + ), + ( + "MAIL FROM:<> VERY_LONG_AND_INVALID=PARAM", + Err(Error::UnsupportedParameter { + param: "VERY_LONG_AND_INVALID=PARAM".to_string(), + }), + ), + ( + "MAIL FROM:<> SMTPUTF8=YES", + Err(Error::UnsupportedParameter { + param: "SMTPUTF8=YES".to_string(), + }), + ), + ( + "MAIL FROM:<> SMTPUTF8=YES", + Err(Error::UnsupportedParameter { + param: "SMTPUTF8=YES".to_string(), + }), + ), + // Parameters + ( + "MAIL FROM:<> SMTPUTF8", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::SmtpUtf8], + }), + ), + ( + "MAIL FROM:<> SMTPUTF8 REQUIRETLS CONPERM CONNEG", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::SmtpUtf8, + Parameter::RequireTls, + Parameter::ConPerm, + Parameter::ConNeg, + ], + }), + ), + ( + "MAIL FROM:<> BODY=BINARYMIME BODY=7BIT BODY=8BITMIME", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Body(Body::BinaryMime), + Parameter::Body(Body::SevenBit), + Parameter::Body(Body::EightBitMime), + ], + }), + ), + ( + "MAIL FROM:<> BODY=OTHER", + Err(Error::InvalidParameter { param: "BODY" }), + ), + ( + "MAIL FROM:<> SIZE=500000 SIZE=0", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::Size(500000), Parameter::Size(0)], + }), + ), + ( + "MAIL FROM:<> SIZE=ABC", + Err(Error::InvalidParameter { param: "SIZE" }), + ), + ( + "MAIL FROM:<> SIZE=-100", + Err(Error::InvalidParameter { param: "SIZE" }), + ), + ( + "MAIL FROM:<> SIZE=", + Err(Error::InvalidParameter { param: "SIZE" }), + ), + ( + "MAIL FROM:<> BY=120;R BY=0;N BY=-10;RT BY=+22;NT", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::By(By::Return { + time: 120, + trace: false, + }), + Parameter::By(By::Notify { + time: 0, + trace: false, + }), + Parameter::By(By::Return { + time: -10, + trace: true, + }), + Parameter::By(By::Notify { + time: 22, + trace: true, + }), + ], + }), + ), + ( + "MAIL FROM:<> BY=120", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=120;T", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=120;", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=120;0", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=120;;", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=;", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=;R", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> BY=", + Err(Error::InvalidParameter { param: "BY" }), + ), + ( + "MAIL FROM:<> HOLDUNTIL=12345 HOLDFOR=67890", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::HoldUntil(12345), Parameter::HoldFor(67890)], + }), + ), + ( + "MAIL FROM:<> HOLDUNTIL=0ABC", + Err(Error::InvalidParameter { param: "HOLDUNTIL" }), + ), + ( + "MAIL FROM:<> HOLDUNTIL=", + Err(Error::InvalidParameter { param: "HOLDUNTIL" }), + ), + ( + "MAIL FROM:<> HOLDFOR=XYZ", + Err(Error::InvalidParameter { param: "HOLDFOR" }), + ), + ( + "MAIL FROM:<> HOLDFOR=", + Err(Error::InvalidParameter { param: "HOLDFOR" }), + ), + ( + concat!( + "MAIL FROM:<> NOTIFY=FAILURE NOTIFY=FAILURE,DELAY ", + "NOTIFY=SUCCESS,FAILURE,DELAY NOTIFY=NEVER" + ), + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Notify(NOTIFY_FAILURE), + Parameter::Notify(NOTIFY_FAILURE | NOTIFY_DELAY), + Parameter::Notify(NOTIFY_FAILURE | NOTIFY_DELAY | NOTIFY_SUCCESS), + Parameter::Notify(0), + ], + }), + ), + ( + "MAIL FROM:<> NOTIFY=", + Err(Error::InvalidParameter { param: "NOTIFY" }), + ), + ( + "MAIL FROM:<> NOTIFY=FAILURE,NEVER", + Err(Error::InvalidParameter { param: "NOTIFY" }), + ), + ( + "MAIL FROM:<> NOTIFY=CHIMICHANGA", + Err(Error::InvalidParameter { param: "NOTIFY" }), + ), + ( + concat!( + "MAIL FROM:<> ORCPT=rfc822;Bob@Example.COM ", + "ORCPT=rfc822;George+20@Tax-+20ME+20.GOV" + ), + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Orcpt(Orcpt { + addr_type: "rfc822".to_string(), + addr: "Bob@Example.COM".to_string(), + }), + Parameter::Orcpt(Orcpt { + addr_type: "rfc822".to_string(), + addr: "George @Tax- ME .GOV".to_string(), + }), + ], + }), + ), + ( + "MAIL FROM:<> ORCPT=", + Err(Error::InvalidParameter { param: "ORCPT" }), + ), + ( + "MAIL FROM:<> ORCPT=;hello@domain.org", + Err(Error::InvalidParameter { param: "ORCPT" }), + ), + ( + "MAIL FROM:<> ORCPT=rfc822;", + Err(Error::InvalidParameter { param: "ORCPT" }), + ), + ( + "MAIL FROM:<> ORCPT=;", + Err(Error::InvalidParameter { param: "ORCPT" }), + ), + ( + "MAIL FROM:<> RET=HDRS RET=FULL", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::Ret(Ret::Hdrs), Parameter::Ret(Ret::Full)], + }), + ), + ( + "MAIL FROM:<> RET=", + Err(Error::InvalidParameter { param: "RET" }), + ), + ( + "MAIL FROM:<> RET=ENCHILADA", + Err(Error::InvalidParameter { param: "RET" }), + ), + ( + "MAIL FROM:<> ENVID=QQ314159 ENVID=hi+20there", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::EnvId("QQ314159".to_string()), + Parameter::EnvId("hi there".to_string()), + ], + }), + ), + ( + "MAIL FROM:<> ENVID=", + Err(Error::InvalidParameter { param: "ENVID" }), + ), + ( + concat!( + "MAIL FROM:<> SOLICIT=org.example:ADV:ADLT ", + " SOLICIT=net.example:ADV,org.example:ADV:ADLT" + ), + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Solicit("org.example:ADV:ADLT".to_string()), + Parameter::Solicit("net.example:ADV,org.example:ADV:ADLT".to_string()), + ], + }), + ), + ( + "MAIL FROM:<> SOLICIT=", + Err(Error::InvalidParameter { param: "SOLICIT" }), + ), + ( + "MAIL FROM:<> TRANSID=<12345@claremont.edu>", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::TransId("12345@claremont.edu".to_string())], + }), + ), + ( + "MAIL FROM:<> TRANSID=", + Err(Error::InvalidParameter { param: "TRANSID" }), + ), + ( + "MAIL FROM:<> MTRK=my-ceritifier MTRK=other-certifier:1234", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Mtrk(Mtrk { + certifier: "my-ceritifier".to_string(), + timeout: 0, + }), + Parameter::Mtrk(Mtrk { + certifier: "other-certifier".to_string(), + timeout: 1234, + }), + ], + }), + ), + ( + "MAIL FROM:<> MTRK=", + Err(Error::InvalidParameter { param: "MTRK" }), + ), + ( + "MAIL FROM:<> MTRK=:", + Err(Error::InvalidParameter { param: "MTRK" }), + ), + ( + "MAIL FROM:<> MTRK=:998", + Err(Error::InvalidParameter { param: "MTRK" }), + ), + ( + "MAIL FROM:<> MTRK=abc:", + Err(Error::InvalidParameter { param: "MTRK" }), + ), + ( + "MAIL FROM:<> MTRK=abc:abc", + Err(Error::InvalidParameter { param: "MTRK" }), + ), + ( + "MAIL FROM:<> AUTH=<> AUTH=e+3Dmc2@example.com", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Auth("<>".to_string()), + Parameter::Auth("e=mc2@example.com".to_string()), + ], + }), + ), + ( + "MAIL FROM:<> AUTH=", + Err(Error::InvalidParameter { param: "AUTH" }), + ), + ( + "MAIL FROM:<> MT-PRIORITY=3 MT-PRIORITY=-6", + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![Parameter::MtPriority(3), Parameter::MtPriority(-6)], + }), + ), + ( + "MAIL FROM:<> MT-PRIORITY=", + Err(Error::InvalidParameter { + param: "MT-PRIORITY", + }), + ), + ( + "MAIL FROM:<> MT-PRIORITY=ab", + Err(Error::InvalidParameter { + param: "MT-PRIORITY", + }), + ), + ( + "MAIL FROM:<> MT-PRIORITY=-", + Err(Error::InvalidParameter { + param: "MT-PRIORITY", + }), + ), + ( + concat!( + "MAIL FROM:<> RRVS=2014-04-03T23:01:00Z ", + "RRVS=1997-11-24T14:22:01-08:00;C ", + "RRVS=2003-07-01T10:52:37+02:00;R" + ), + Ok(Request::Mail { + from: "".to_string(), + parameters: vec![ + Parameter::Rrvs(Rrvs::Reject(1396566060)), + Parameter::Rrvs(Rrvs::Continue(880410121)), + Parameter::Rrvs(Rrvs::Reject(1057049557)), + ], + }), + ), + ( + "MAIL FROM:<> RRVS=", + Err(Error::InvalidParameter { param: "RRVS" }), + ), + ( + "MAIL FROM:<> RRVS=2022-01-02", + Err(Error::InvalidParameter { param: "RRVS" }), + ), + ( + "MAIL FROM:<> RRVS=2022-01-02T01:01:01", + Err(Error::InvalidParameter { param: "RRVS" }), + ), + ( + "MAIL FROM:<> RRVS=2022-01-02T01:01:01ZZ", + Err(Error::InvalidParameter { param: "RRVS" }), + ), + ( + "MAIL FROM:<> RRVS=ABC", + Err(Error::InvalidParameter { param: "RRVS" }), + ), + ] { + let (request, parsed_request): (&str, Result, Error>) = item; + + for extra in ["\n", "\r\n", " \n", " \r\n"] { + let request = format!("{}{}", request, extra); + assert_eq!( + parsed_request, + Request::parse(&mut request.as_bytes().iter()), + "failed for {:?}", + request + ); + } + } + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..e69de29