use crate::{
auth::Credentials, rcpt::Rcpt, status::Status, Address, CipherSuite, ClientName,
ProtocolVersion,
};
use vsmtp_auth::{dkim, spf};
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TransactionType {
Incoming(Option<String>),
Outgoing {
domain: String,
},
Internal,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Stage {
Connect,
Helo,
MailFrom,
RcptTo,
Finished,
}
#[derive(Debug, Default, Clone, serde::Serialize)]
pub enum Context {
#[default]
Empty,
Connect(ContextConnect),
Helo(ContextHelo),
MailFrom(ContextMailFrom),
RcptTo(ContextRcptTo),
Finished(ContextFinished),
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("state is not in correct state")]
BadState,
#[error("bad argument: {0}")]
BadArgument(String), }
impl Context {
#[must_use]
pub fn stage(&self) -> Stage {
match self {
Self::Empty => unreachable!(),
Self::Connect { .. } => Stage::Connect,
Self::Helo { .. } => Stage::Helo,
Self::MailFrom { .. } => Stage::MailFrom,
Self::RcptTo { .. } => Stage::RcptTo,
Self::Finished { .. } => Stage::Finished,
}
}
pub fn reset(&mut self) {
match self {
Self::Empty => unreachable!(),
Self::Connect(_) => (),
Self::Helo(ContextHelo { connect, helo })
| Self::MailFrom(ContextMailFrom { connect, helo, .. })
| Self::RcptTo(ContextRcptTo { connect, helo, .. })
| Self::Finished(ContextFinished { connect, helo, .. }) => {
*self = Self::Helo(ContextHelo {
connect: connect.clone(),
helo: helo.clone(),
});
}
}
}
pub fn to_connect(
&mut self,
client_addr: std::net::SocketAddr,
server_addr: std::net::SocketAddr,
server_name: String,
timestamp: time::OffsetDateTime,
uuid: uuid::Uuid,
) -> Result<&mut Self, Error> {
match self {
Self::Empty => {
*self = Self::Connect(ContextConnect {
connect: ConnectProperties {
connect_timestamp: timestamp,
connect_uuid: uuid,
client_addr,
server_addr,
server_name,
skipped: None,
tls: None,
auth: None,
},
});
Ok(self)
}
_ => Err(Error::BadState),
}
}
pub fn to_helo(
&mut self,
client_name: ClientName,
using_deprecated: bool,
) -> Result<&mut Self, Error> {
match self {
Self::Connect(ContextConnect { connect }) => {
*self = Self::Helo(ContextHelo {
connect: connect.clone(),
helo: HeloProperties {
client_name,
using_deprecated,
},
});
Ok(self)
}
Self::Helo(ContextHelo { helo, .. }) => {
helo.client_name = client_name;
helo.using_deprecated = using_deprecated;
Ok(self)
}
_ => Err(Error::BadState),
}
}
pub fn with_credentials(&mut self, credentials: Credentials) -> Result<(), Error> {
match self {
Self::Connect(ContextConnect { connect }) | Self::Helo(ContextHelo { connect, .. }) => {
connect.auth = Some(AuthProperties {
credentials: Some(credentials),
cancel_count: 0,
authenticated: false,
});
Ok(())
}
Self::Empty | Self::MailFrom(_) | Self::RcptTo(_) | Self::Finished(_) => {
Err(Error::BadState)
}
}
}
pub fn to_mail_from(&mut self, reverse_path: Option<Address>) -> Result<(), Error> {
match self {
Self::Helo(ContextHelo { connect, helo }) => {
let now = time::OffsetDateTime::now_utc();
*self = Self::MailFrom(ContextMailFrom {
connect: connect.clone(),
helo: helo.clone(),
mail_from: MailFromProperties {
reverse_path,
mail_timestamp: now,
message_uuid: uuid::Uuid::new_v4(),
},
});
Ok(())
}
Self::MailFrom(ContextMailFrom { mail_from, .. }) => {
mail_from.reverse_path = reverse_path;
Ok(())
}
_ => Err(Error::BadState),
}
}
pub fn to_finished(&mut self) -> Result<(), Error> {
match self {
Self::RcptTo(ContextRcptTo {
connect,
helo,
mail_from,
rcpt_to,
}) => {
*self = Self::Finished(ContextFinished {
connect: connect.clone(),
helo: helo.clone(),
mail_from: mail_from.clone(),
rcpt_to: rcpt_to.clone(),
finished: FinishedProperties {
dkim: None,
spf: None,
},
});
Ok(())
}
_ => Err(Error::BadState),
}
}
#[must_use]
pub fn skipped(&self) -> &Option<Status> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.skipped,
}
}
pub fn set_skipped(&mut self, status: Status) {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => connect.skipped = Some(status),
}
}
#[must_use]
pub fn connection_timestamp(&self) -> &time::OffsetDateTime {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.connect_timestamp,
}
}
#[must_use]
pub fn client_addr(&self) -> &std::net::SocketAddr {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.client_addr,
}
}
#[must_use]
pub fn server_addr(&self) -> &std::net::SocketAddr {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.server_addr,
}
}
#[must_use]
pub fn server_name(&self) -> &String {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.server_name,
}
}
#[must_use]
pub fn is_secured(&self) -> bool {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => connect.tls.is_some(),
}
}
#[must_use]
pub fn is_authenticated(&self) -> bool {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => connect
.auth
.as_ref()
.map_or(false, |auth| auth.authenticated),
}
}
pub fn to_secured(
&mut self,
sni: Option<String>,
protocol_version: rustls::ProtocolVersion,
cipher_suite: rustls::CipherSuite,
peer_certificates: Option<Vec<rustls::Certificate>>,
alpn_protocol: Option<Vec<u8>>,
) -> Result<(), Error> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect }) | Self::Helo(ContextHelo { connect, .. }) => {
connect.tls = Some(TlsProperties {
protocol_version: ProtocolVersion(protocol_version),
cipher_suite: CipherSuite(cipher_suite),
peer_certificates,
alpn_protocol,
});
if let Some(sni) = sni {
connect.server_name = sni;
}
Ok(())
}
Self::MailFrom(ContextMailFrom { .. })
| Self::RcptTo(ContextRcptTo { .. })
| Self::Finished(ContextFinished { .. }) => Err(Error::BadState),
}
}
pub fn client_name(&self) -> Result<&ClientName, Error> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { .. }) => Err(Error::BadState),
Self::Helo(ContextHelo { helo, .. })
| Self::MailFrom(ContextMailFrom { helo, .. })
| Self::RcptTo(ContextRcptTo { helo, .. })
| Self::Finished(ContextFinished { helo, .. }) => Ok(&helo.client_name),
}
}
#[must_use]
pub fn tls(&self) -> &Option<TlsProperties> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.tls,
}
}
#[must_use]
pub fn auth(&self) -> &Option<AuthProperties> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => &connect.auth,
}
}
#[must_use]
pub fn auth_mut(&mut self) -> Option<&mut AuthProperties> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect })
| Self::Helo(ContextHelo { connect, .. })
| Self::MailFrom(ContextMailFrom { connect, .. })
| Self::RcptTo(ContextRcptTo { connect, .. })
| Self::Finished(ContextFinished { connect, .. }) => connect.auth.as_mut(),
}
}
pub fn to_auth(&mut self) -> Result<&mut AuthProperties, Error> {
match self {
Self::Empty => unreachable!(),
Self::Connect(ContextConnect { connect }) | Self::Helo(ContextHelo { connect, .. }) => {
connect.auth = Some(AuthProperties {
authenticated: false,
cancel_count: 0,
credentials: None,
});
Ok(connect.auth.as_mut().expect("has been set just above"))
}
Self::MailFrom(ContextMailFrom { .. })
| Self::RcptTo(ContextRcptTo { .. })
| Self::Finished(ContextFinished { .. }) => Err(Error::BadState),
}
}
pub const fn reverse_path(&self) -> Result<&Option<Address>, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } => Err(Error::BadState),
Self::MailFrom(ContextMailFrom { mail_from, .. })
| Self::RcptTo(ContextRcptTo { mail_from, .. })
| Self::Finished(ContextFinished { mail_from, .. }) => Ok(&mail_from.reverse_path),
}
}
pub fn set_reverse_path(&mut self, reverse_path: Option<Address>) -> Result<(), Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } => Err(Error::BadState),
Self::MailFrom(ContextMailFrom { mail_from, .. })
| Self::RcptTo(ContextRcptTo { mail_from, .. })
| Self::Finished(ContextFinished { mail_from, .. }) => {
mail_from.reverse_path = reverse_path;
Ok(())
}
}
}
pub const fn mail_timestamp(&self) -> Result<&time::OffsetDateTime, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } => Err(Error::BadState),
Self::MailFrom(ContextMailFrom { mail_from, .. })
| Self::RcptTo(ContextRcptTo { mail_from, .. })
| Self::Finished(ContextFinished { mail_from, .. }) => Ok(&mail_from.mail_timestamp),
}
}
pub const fn message_uuid(&self) -> Result<&uuid::Uuid, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } => Err(Error::BadState),
Self::MailFrom(ContextMailFrom { mail_from, .. })
| Self::RcptTo(ContextRcptTo { mail_from, .. })
| Self::Finished(ContextFinished { mail_from, .. }) => Ok(&mail_from.message_uuid),
}
}
pub fn generate_message_id(&mut self) -> Result<(), Error> {
match self {
Self::Empty | Self::Connect(_) | Self::Helo(_) => Err(Error::BadState),
Self::MailFrom(ContextMailFrom { mail_from, .. })
| Self::RcptTo(ContextRcptTo { mail_from, .. })
| Self::Finished(ContextFinished { mail_from, .. }) => {
mail_from.message_uuid = uuid::Uuid::new_v4();
Ok(())
}
}
}
pub fn add_forward_path(&mut self, forward_path: Address) -> Result<(), Error> {
match self {
Self::Empty | Self::Connect(_) | Self::Helo(_) => Err(Error::BadState),
Self::MailFrom(ContextMailFrom {
connect,
helo,
mail_from,
}) => {
*self = Self::RcptTo(ContextRcptTo {
connect: connect.clone(),
helo: helo.clone(),
mail_from: mail_from.clone(),
rcpt_to: RcptToProperties {
forward_paths: vec![Rcpt::new(forward_path)],
transaction_type: TransactionType::Internal, },
});
Ok(())
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => {
rcpt_to.forward_paths.push(Rcpt::new(forward_path));
Ok(())
}
}
}
pub fn remove_forward_path(&mut self, forward_path: &Address) -> Result<bool, Error> {
match self {
Self::Empty | Self::Connect(_) | Self::Helo(_) | Self::MailFrom(_) => {
Err(Error::BadState)
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => {
if let Some(index) = rcpt_to
.forward_paths
.iter()
.position(|rcpt| rcpt.address == *forward_path)
{
rcpt_to.forward_paths.swap_remove(index);
Ok(true)
} else {
Ok(false)
}
}
}
}
pub const fn forward_paths(&self) -> Result<&Vec<Rcpt>, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } | Self::MailFrom { .. } => {
Err(Error::BadState)
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => Ok(&rcpt_to.forward_paths),
}
}
pub fn forward_paths_mut(&mut self) -> Result<&mut Vec<Rcpt>, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } | Self::MailFrom { .. } => {
Err(Error::BadState)
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => Ok(&mut rcpt_to.forward_paths),
}
}
pub const fn transaction_type(&self) -> Result<&TransactionType, Error> {
match self {
Self::Empty | Self::Connect { .. } | Self::Helo { .. } | Self::MailFrom { .. } => {
Err(Error::BadState)
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => Ok(&rcpt_to.transaction_type),
}
}
pub fn set_transaction_type(&mut self, transaction_type: TransactionType) -> Result<(), Error> {
match self {
Self::Empty | Self::Connect(_) | Self::Helo(_) => Err(Error::BadState),
Self::MailFrom(ContextMailFrom {
connect,
helo,
mail_from,
}) => {
*self = Self::RcptTo(ContextRcptTo {
connect: connect.clone(),
helo: helo.clone(),
mail_from: mail_from.clone(),
rcpt_to: RcptToProperties {
transaction_type,
forward_paths: Vec::new(),
},
});
Ok(())
}
Self::RcptTo(ContextRcptTo { rcpt_to, .. })
| Self::Finished(ContextFinished { rcpt_to, .. }) => {
rcpt_to.transaction_type = transaction_type;
Ok(())
}
}
}
pub const fn spf(&self) -> Result<Option<&spf::Result>, Error> {
match self {
Self::Empty
| Self::Connect(_)
| Self::Helo(_)
| Self::MailFrom(_)
| Self::RcptTo(_) => Err(Error::BadState),
Self::Finished(ContextFinished { finished, .. }) => Ok(finished.spf.as_ref()),
}
}
pub fn set_spf(&mut self, spf: spf::Result) -> Result<(), Error> {
match self {
Self::Empty
| Self::Connect(_)
| Self::Helo(_)
| Self::MailFrom(_)
| Self::RcptTo(_) => Err(Error::BadState),
Self::Finished(ContextFinished { finished, .. }) => {
finished.spf = Some(spf);
Ok(())
}
}
}
pub const fn dkim(&self) -> Result<Option<&dkim::VerificationResult>, Error> {
match self {
Self::Empty
| Self::Connect(_)
| Self::Helo(_)
| Self::MailFrom(_)
| Self::RcptTo(_) => Err(Error::BadState),
Self::Finished(ContextFinished { finished, .. }) => Ok(finished.dkim.as_ref()),
}
}
pub fn set_dkim(&mut self, result: dkim::VerificationResult) -> Result<(), Error> {
match self {
Self::Empty
| Self::Connect(_)
| Self::Helo(_)
| Self::MailFrom(_)
| Self::RcptTo(_) => Err(Error::BadState),
Self::Finished(ContextFinished { finished, .. }) => {
finished.dkim = Some(result);
Ok(())
}
}
}
pub fn unwrap_finished(self) -> Result<ContextFinished, Error> {
match self {
Self::Finished(finished) => Ok(finished),
_ => Err(Error::BadState),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TlsProperties {
pub protocol_version: crate::ProtocolVersion,
pub cipher_suite: crate::CipherSuite,
#[serde(
serialize_with = "serde_with::As::<Option<Vec<serde_with::base64::Base64>>>::serialize",
deserialize_with = "de_peer_certificates"
)]
pub peer_certificates: Option<Vec<rustls::Certificate>>,
pub alpn_protocol: Option<Vec<u8>>,
}
fn de_peer_certificates<'de, D>(
deserializer: D,
) -> Result<Option<Vec<rustls::Certificate>>, D::Error>
where
D: serde::Deserializer<'de>,
{
<Option<Vec<String>> as serde::Deserialize>::deserialize(deserializer)?
.map(|certs| {
match certs
.into_iter()
.map(|i| rustls_pemfile::certs(&mut i.as_bytes()))
.collect::<Result<Vec<Vec<Vec<u8>>>, _>>()
{
Ok(certs) => Ok(certs
.into_iter()
.flatten()
.map(rustls::Certificate)
.collect()),
Err(e) => Err(serde::de::Error::custom(e)),
}
})
.transpose()
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct AuthProperties {
pub authenticated: bool,
pub cancel_count: usize,
pub credentials: Option<Credentials>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ConnectProperties {
#[serde(with = "time::serde::iso8601")]
pub connect_timestamp: time::OffsetDateTime,
pub connect_uuid: uuid::Uuid,
pub client_addr: std::net::SocketAddr,
pub server_addr: std::net::SocketAddr,
pub server_name: String,
pub skipped: Option<Status>,
pub tls: Option<TlsProperties>,
pub auth: Option<AuthProperties>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HeloProperties {
pub client_name: ClientName,
pub using_deprecated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MailFromProperties {
pub reverse_path: Option<Address>,
#[serde(with = "time::serde::iso8601")]
pub mail_timestamp: time::OffsetDateTime,
pub message_uuid: uuid::Uuid,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RcptToProperties {
pub forward_paths: Vec<Rcpt>,
pub transaction_type: TransactionType,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct FinishedProperties {
pub dkim: Option<dkim::VerificationResult>,
pub spf: Option<spf::Result>,
}
#[doc(hidden)]
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct ContextConnect {
pub connect: ConnectProperties,
}
#[doc(hidden)]
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct ContextHelo {
pub connect: ConnectProperties,
pub helo: HeloProperties,
}
#[doc(hidden)]
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct ContextMailFrom {
pub connect: ConnectProperties,
pub helo: HeloProperties,
pub mail_from: MailFromProperties,
}
#[doc(hidden)]
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, serde::Serialize)]
pub struct ContextRcptTo {
pub connect: ConnectProperties,
pub helo: HeloProperties,
pub mail_from: MailFromProperties,
pub rcpt_to: RcptToProperties,
}
#[doc(hidden)]
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ContextFinished {
#[serde(flatten)]
pub connect: ConnectProperties,
#[serde(flatten)]
pub helo: HeloProperties,
#[serde(flatten)]
pub mail_from: MailFromProperties,
#[serde(flatten)]
pub rcpt_to: RcptToProperties,
#[serde(flatten)]
pub finished: FinishedProperties,
}