use crate::ConnectionKind;
use vsmtp_common::{auth::Mechanism, ClientName};
extern crate alloc;
#[non_exhaustive]
pub struct UnparsedArgs(pub Vec<u8>);
pub type Command<Verb, Args> = (Verb, Args);
#[non_exhaustive]
pub struct AcceptArgs {
pub client_addr: std::net::SocketAddr,
pub server_addr: std::net::SocketAddr,
pub timestamp: time::OffsetDateTime,
pub uuid: uuid::Uuid,
pub kind: ConnectionKind,
}
impl AcceptArgs {
#[inline]
#[must_use]
pub const fn new(
client_addr: std::net::SocketAddr,
server_addr: std::net::SocketAddr,
timestamp: time::OffsetDateTime,
uuid: uuid::Uuid,
kind: ConnectionKind,
) -> Self {
Self {
client_addr,
server_addr,
timestamp,
uuid,
kind,
}
}
}
#[non_exhaustive]
pub struct HeloArgs {
pub client_name: String,
}
#[non_exhaustive]
pub struct EhloArgs {
pub client_name: ClientName,
}
#[derive(strum::EnumVariantNames, strum::EnumString)]
pub enum MimeBodyType {
#[strum(serialize = "7BIT")]
SevenBit,
#[strum(serialize = "8BITMIME")]
EightBitMime,
}
#[non_exhaustive]
pub struct MailFromArgs {
pub reverse_path: Option<String>,
pub mime_body_type: Option<MimeBodyType>,
}
#[non_exhaustive]
pub struct RcptToArgs {
pub forward_path: String,
}
#[non_exhaustive]
pub struct AuthArgs {
pub mechanism: Mechanism,
pub initial_response: Option<Vec<u8>>,
}
#[non_exhaustive]
pub enum ParseArgsError {
InvalidUtf8(alloc::string::FromUtf8Error),
BadTypeAddr(std::net::AddrParseError),
BufferTooLong {
expected: usize,
got: usize,
},
InvalidArgs,
}
impl TryFrom<UnparsedArgs> for HeloArgs {
type Error = ParseArgsError;
#[inline]
fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
let value = value
.0
.strip_suffix(b"\r\n")
.ok_or(ParseArgsError::InvalidArgs)?
.to_vec();
Ok(Self {
client_name: addr::parse_domain_name(
&String::from_utf8(value).map_err(ParseArgsError::InvalidUtf8)?,
)
.map_err(|_err| ParseArgsError::InvalidArgs)?
.to_string(),
})
}
}
impl TryFrom<UnparsedArgs> for EhloArgs {
type Error = ParseArgsError;
#[inline]
fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
let value = String::from_utf8(
value
.0
.strip_suffix(b"\r\n")
.ok_or(ParseArgsError::InvalidArgs)?
.to_vec(),
)
.map_err(ParseArgsError::InvalidUtf8)?;
let client_name = match &value {
ipv6 if ipv6.to_lowercase().starts_with("[ipv6:") && ipv6.ends_with(']') => {
match ipv6.get("[IPv6:".len()..ipv6.len() - 1) {
Some(ipv6) => ClientName::Ip6(
ipv6.parse::<std::net::Ipv6Addr>()
.map_err(ParseArgsError::BadTypeAddr)?,
),
None => return Err(ParseArgsError::InvalidArgs),
}
}
ipv4 if ipv4.starts_with('[') && ipv4.ends_with(']') => {
match ipv4.get(1..ipv4.len() - 1) {
Some(ipv4) => ClientName::Ip4(
ipv4.parse::<std::net::Ipv4Addr>()
.map_err(ParseArgsError::BadTypeAddr)?,
),
None => return Err(ParseArgsError::InvalidArgs),
}
}
domain => ClientName::Domain(
addr::parse_domain_name(domain)
.map_err(|_err| ParseArgsError::InvalidArgs)?
.to_string(),
),
};
Ok(Self { client_name })
}
}
impl TryFrom<UnparsedArgs> for AuthArgs {
type Error = ParseArgsError;
#[inline]
fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
let value = value
.0
.strip_suffix(b"\r\n")
.ok_or(ParseArgsError::InvalidArgs)?;
let (mechanism, initial_response) = if let Some((idx, _)) = value
.iter()
.copied()
.enumerate()
.find(|&(_, c)| c.is_ascii_whitespace())
{
let (mechanism, initial_response) = value.split_at(idx);
(
mechanism.to_vec(),
Some(
initial_response
.get(1..)
.ok_or(ParseArgsError::InvalidArgs)?
.to_vec(),
),
)
} else {
(value.to_vec(), None)
};
let mechanism = String::from_utf8(mechanism)
.map_err(ParseArgsError::InvalidUtf8)?
.parse()
.map_err(|_err| ParseArgsError::InvalidArgs)?;
Ok(Self {
mechanism,
initial_response,
})
}
}
impl TryFrom<UnparsedArgs> for MailFromArgs {
type Error = ParseArgsError;
#[inline]
fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
let value = value
.0
.strip_suffix(b"\r\n")
.ok_or(ParseArgsError::InvalidArgs)?;
let mut words = value
.split(u8::is_ascii_whitespace)
.filter(|s| !s.is_empty());
let mailbox = if let Some(s) = words.next() {
let mailbox = s
.strip_prefix(b"<")
.ok_or(ParseArgsError::InvalidArgs)?
.strip_suffix(b">")
.ok_or(ParseArgsError::InvalidArgs)?;
if mailbox.is_empty() {
None
} else {
Some(String::from_utf8(mailbox.to_vec()).map_err(ParseArgsError::InvalidUtf8)?)
}
} else {
return Err(ParseArgsError::InvalidArgs);
};
let mut mime_body_type = None;
#[allow(clippy::expect_used)]
for args in words {
match args.strip_prefix(b"BODY=") {
Some(args_mime_body_type) if mime_body_type.is_none() => {
mime_body_type = <MimeBodyType as strum::VariantNames>::VARIANTS
.iter()
.find(|i| {
args_mime_body_type.len() >= i.len()
&& args_mime_body_type
.get(..i.len())
.expect("range checked above")
.eq_ignore_ascii_case(i.as_bytes())
})
.map(|body| body.parse().expect("body found above"));
}
_ => return Err(ParseArgsError::InvalidArgs),
}
}
Ok(Self {
reverse_path: mailbox,
mime_body_type,
})
}
}
impl TryFrom<UnparsedArgs> for RcptToArgs {
type Error = ParseArgsError;
#[inline]
fn try_from(value: UnparsedArgs) -> Result<Self, Self::Error> {
let value = value
.0
.strip_suffix(b"\r\n")
.ok_or(ParseArgsError::InvalidArgs)?;
let mut word = value
.split(u8::is_ascii_whitespace)
.filter(|s| !s.is_empty());
let mailbox = if let Some(s) = word.next() {
String::from_utf8(
s.strip_prefix(b"<")
.ok_or(ParseArgsError::InvalidArgs)?
.strip_suffix(b">")
.ok_or(ParseArgsError::InvalidArgs)?
.to_vec(),
)
.map_err(ParseArgsError::InvalidUtf8)?
} else {
return Err(ParseArgsError::InvalidArgs);
};
Ok(Self {
forward_path: mailbox,
})
}
}
#[derive(Debug, strum::AsRefStr, strum::EnumString, strum::EnumVariantNames)]
#[non_exhaustive]
pub enum Verb {
#[strum(serialize = "HELO ")]
Helo,
#[strum(serialize = "EHLO ")]
Ehlo,
#[strum(serialize = "MAIL FROM:")]
MailFrom,
#[strum(serialize = "RCPT TO:")]
RcptTo,
#[strum(serialize = "DATA\r\n")]
Data,
#[strum(serialize = "QUIT\r\n")]
Quit,
#[strum(serialize = "RSET\r\n")]
Rset,
#[strum(serialize = "HELP")]
Help,
#[strum(serialize = "NOOP\r\n")]
Noop,
#[strum(serialize = "STARTTLS\r\n")]
StartTls,
#[strum(serialize = "AUTH ")]
Auth,
Unknown,
}