use crate::{Domain, Reply, ReplyCode, Target};
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Envelop {
#[error("the envelop does not contain any recipient")]
NoRecipient,
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum LocalDelivery {
#[error("mailbox `{mailbox}` does not exist")]
MailboxDoNotExist {
mailbox: String,
},
#[error("todo")]
Other(String),
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Lookup {
#[error("record not found")]
NoRecords {},
#[error("null MX record found for '{domain}'")]
ContainsNullMX {
domain: Domain,
},
#[error("timed out")]
TimedOut,
#[error("no connections available")]
NoConnections,
#[error("io error: {0}")]
IO(String),
#[error("dns-proto error: {0}")]
Proto(String),
#[error("message: {0}")]
Message(String),
#[error("not implemented")]
NotImplemented,
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Queuer {
#[error("recipient is still in status waiting")]
StillWaiting,
#[error("max deferred attempt reached")]
MaxDeferredAttemptReached,
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Delivery {
#[error("failed to parse the reply of the server: source={}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
ReplyParsing {
with_source: Option<String>,
},
#[error("permanent error: {reply}: {}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
Permanent {
reply: ReplyCode,
with_source: Option<String>,
},
#[error("transient error: {reply}: {}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
Transient {
reply: ReplyCode,
with_source: Option<String>,
},
#[error("tls: {}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
Tls {
with_source: Option<String>,
},
#[error("client: {}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
Client {
with_source: Option<String>,
},
#[error("connection: {}",
with_source
.as_ref()
.map_or("null", String::as_str)
)]
Connection {
with_source: Option<String>,
},
}
impl From<std::io::Error> for Delivery {
#[inline]
fn from(err: std::io::Error) -> Self {
Self::Connection {
with_source: Some(err.to_string()),
}
}
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Rule {
#[error("denied: {0}")]
Denied(Reply),
}
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "testing", derive(PartialEq, Eq))]
pub enum Variant {
#[error("<local delivery>: {0}")]
LocalDelivery(#[from] LocalDelivery),
#[error("<envelop>: {0}")]
Envelop(#[from] Envelop),
#[error("<dns>: {0}")]
Lookup(#[from] Lookup),
#[error("<queuer>: {0}")]
Queuer(#[from] Queuer),
#[error("<delivery>: {}",
.0.iter()
.map(|(domain, e)| format!("{domain}:{e}"))
.collect::<Vec<_>>()
.join(", ")
)]
Delivery(Vec<(Target, Delivery)>),
#[error("<rules>: {0}")]
Rules(#[from] Rule),
}
impl From<trust_dns_resolver::error::ResolveError> for Lookup {
#[inline]
fn from(error: trust_dns_resolver::error::ResolveError) -> Self {
match error.kind() {
trust_dns_resolver::error::ResolveErrorKind::Message(e) => {
Self::Message((*e).to_owned())
}
trust_dns_resolver::error::ResolveErrorKind::Msg(e) => Self::Message(e.to_string()),
trust_dns_resolver::error::ResolveErrorKind::NoConnections => Self::NoConnections,
trust_dns_resolver::error::ResolveErrorKind::NoRecordsFound { .. } => {
Self::NoRecords {}
}
trust_dns_resolver::error::ResolveErrorKind::Io(io) => Self::IO(io.to_string()),
trust_dns_resolver::error::ResolveErrorKind::Proto(proto) => {
Self::Proto(proto.to_string())
}
trust_dns_resolver::error::ResolveErrorKind::Timeout => Self::TimedOut,
_ => Self::NotImplemented,
}
}
}
impl From<lettre::transport::smtp::Error> for Delivery {
#[inline]
fn from(value: lettre::transport::smtp::Error) -> Self {
let with_source = std::error::Error::source(&value).map(ToString::to_string);
if value.is_client() {
Self::Client { with_source }
} else if value.is_permanent() {
#[allow(clippy::expect_used)]
Self::Permanent {
with_source,
reply: value
.status()
.expect("error is permanent and has code")
.into(),
}
} else if value.is_transient() {
#[allow(clippy::expect_used)]
Self::Transient {
with_source,
reply: value
.status()
.expect("error is transient and has code")
.into(),
}
} else if value.is_response() {
Self::ReplyParsing { with_source }
} else if value.is_tls() {
Self::Tls { with_source }
} else {
Self::Connection { with_source }
}
}
}
impl Delivery {
fn is_permanent(&self) -> bool {
match self {
Self::Permanent { .. } => true,
Self::ReplyParsing { .. }
| Self::Transient { .. }
| Self::Tls { .. }
| Self::Client { .. }
| Self::Connection { .. } => false,
}
}
}
impl Variant {
#[must_use]
#[inline]
pub fn is_permanent(&self) -> bool {
match self {
Self::LocalDelivery(
LocalDelivery::MailboxDoNotExist { .. } | LocalDelivery::Other(_),
)
| Self::Envelop(Envelop::NoRecipient)
| Self::Queuer(Queuer::StillWaiting | Queuer::MaxDeferredAttemptReached) => true,
Self::Lookup(
Lookup::NoRecords {}
| Lookup::TimedOut
| Lookup::NoConnections
| Lookup::IO(_)
| Lookup::Proto(_)
| Lookup::Message(_)
| Lookup::ContainsNullMX { .. }
| Lookup::NotImplemented,
)
| Self::Rules(Rule::Denied(_)) => false,
Self::Delivery(attempts) => attempts.iter().all(|(_, e)| e.is_permanent()),
}
}
}