pub(crate) mod validated;
pub use validated::{
AddressLiteral, Domain, DomainOrLiteral, EnvidValue, ForwardPath, Mailbox, ReversePath,
ValidationError, XtextSafe,
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Protocol {
Smtp,
Lmtp,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RecipientResult {
pub recipient: ForwardPath,
pub response: SmtpResponse,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RejectedRecipient {
pub recipient: ForwardPath,
pub response: SmtpResponse,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SendResult {
pub rejected_recipients: Vec<RejectedRecipient>,
}
impl SendResult {
pub fn all_accepted(&self) -> bool {
self.rejected_recipients.is_empty()
}
pub fn has_rejections(&self) -> bool {
!self.rejected_recipients.is_empty()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LmtpSendResult {
pub results: Vec<RecipientResult>,
pub rejected_recipients: Vec<RejectedRecipient>,
}
impl LmtpSendResult {
pub fn all_accepted(&self) -> bool {
self.rejected_recipients.is_empty()
}
pub fn has_rejections(&self) -> bool {
!self.rejected_recipients.is_empty()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SmtpResponse {
pub code: u16,
pub enhanced_code: Option<EnhancedStatusCode>,
pub lines: Vec<String>,
}
impl SmtpResponse {
pub fn is_success(&self) -> bool {
(200..300).contains(&self.code)
}
pub fn is_intermediate(&self) -> bool {
(300..400).contains(&self.code)
}
pub fn is_data_ready(&self) -> bool {
self.code == 354
}
pub fn is_transient_error(&self) -> bool {
(400..500).contains(&self.code)
}
pub fn is_permanent_error(&self) -> bool {
(500..600).contains(&self.code)
}
pub fn text(&self) -> String {
self.lines.join("\n")
}
}
impl std::fmt::Display for SmtpResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, line) in self.lines.iter().enumerate() {
if i > 0 {
f.write_str("\n")?;
}
write!(f, "{} {}", self.code, line)?;
}
if self.lines.is_empty() {
write!(f, "{}", self.code)?;
}
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EnhancedStatusCode {
pub class: u8,
pub subject: u16,
pub detail: u16,
}
impl std::fmt::Display for EnhancedStatusCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.class, self.subject, self.detail)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SmtpExtension {
EightBitMime,
Pipelining,
Size(Option<u64>),
StartTls,
Auth(Vec<AuthMechanism>),
Chunking,
BinaryMime,
SmtpUtf8,
EnhancedStatusCodes,
SaslIr,
Dsn,
RequireTls,
FutureRelease {
max_interval: Option<u64>,
max_datetime: Option<String>,
},
DeliverBy(Option<u64>),
MtPriority,
Vrfy,
Expn,
NoSoliciting(Option<String>),
Other(String),
}
impl std::fmt::Display for SmtpExtension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EightBitMime => f.write_str("8BITMIME"),
Self::Pipelining => f.write_str("PIPELINING"),
Self::Size(Some(n)) => write!(f, "SIZE {n}"),
Self::Size(None) => f.write_str("SIZE"),
Self::StartTls => f.write_str("STARTTLS"),
Self::Auth(mechs) => {
f.write_str("AUTH")?;
for m in mechs {
write!(f, " {m}")?;
}
Ok(())
}
Self::Chunking => f.write_str("CHUNKING"),
Self::BinaryMime => f.write_str("BINARYMIME"),
Self::SmtpUtf8 => f.write_str("SMTPUTF8"),
Self::EnhancedStatusCodes => f.write_str("ENHANCEDSTATUSCODES"),
Self::SaslIr => f.write_str("SASL-IR"),
Self::Dsn => f.write_str("DSN"),
Self::RequireTls => f.write_str("REQUIRETLS"),
Self::FutureRelease {
max_interval,
max_datetime,
} => {
f.write_str("FUTURERELEASE")?;
if let Some(interval) = max_interval {
write!(f, " {interval}")?;
}
if let Some(datetime) = max_datetime {
write!(f, " {datetime}")?;
}
Ok(())
}
Self::DeliverBy(Some(n)) => write!(f, "DELIVERBY {n}"),
Self::DeliverBy(None) => f.write_str("DELIVERBY"),
Self::MtPriority => f.write_str("MT-PRIORITY"),
Self::Vrfy => f.write_str("VRFY"),
Self::Expn => f.write_str("EXPN"),
Self::NoSoliciting(Some(kw)) => write!(f, "NO-SOLICITING {kw}"),
Self::NoSoliciting(None) => f.write_str("NO-SOLICITING"),
Self::Other(s) => f.write_str(s),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AuthMechanism {
Plain,
Login,
OAuthBearer,
XOAuth2,
Other(String),
}
impl AuthMechanism {
fn as_mechanism_name(&self) -> &str {
match self {
Self::Plain => "PLAIN",
Self::Login => "LOGIN",
Self::OAuthBearer => "OAUTHBEARER",
Self::XOAuth2 => "XOAUTH2",
Self::Other(name) => name,
}
}
pub(crate) fn eq_mechanism(&self, other: &Self) -> bool {
self.as_mechanism_name()
.eq_ignore_ascii_case(other.as_mechanism_name())
}
}
impl PartialEq for AuthMechanism {
fn eq(&self, other: &Self) -> bool {
self.eq_mechanism(other)
}
}
impl Eq for AuthMechanism {}
impl std::hash::Hash for AuthMechanism {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for byte in self.as_mechanism_name().as_bytes() {
byte.to_ascii_lowercase().hash(state);
}
}
}
impl std::fmt::Display for AuthMechanism {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_mechanism_name())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ServerCapabilities {
pub(crate) greeting_name: String,
pub(crate) extensions: Vec<SmtpExtension>,
}
impl ServerCapabilities {
pub fn greeting_name(&self) -> &str {
&self.greeting_name
}
pub fn extensions(&self) -> &[SmtpExtension] {
&self.extensions
}
pub fn supports_auth(&self, mechanism: &AuthMechanism) -> bool {
self.extensions.iter().any(|ext| {
if let SmtpExtension::Auth(mechs) = ext {
mechs.iter().any(|m| m.eq_mechanism(mechanism))
} else {
false
}
})
}
pub fn supports_auth_extension(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Auth(_)))
}
fn has_extension(&self, predicate: fn(&SmtpExtension) -> bool) -> bool {
self.extensions.iter().any(predicate)
}
pub fn supports_starttls(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::StartTls))
}
pub fn supports_chunking(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Chunking))
}
pub fn supports_size(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Size(_)))
}
pub fn size_limit(&self) -> Option<u64> {
self.extensions.iter().find_map(|ext| {
if let SmtpExtension::Size(limit) = ext {
*limit
} else {
None
}
})
}
pub fn supports_8bitmime(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::EightBitMime))
}
pub fn supports_binarymime(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::BinaryMime))
}
pub fn supports_8bit_or_binary(&self) -> bool {
self.supports_8bitmime() || self.supports_binarymime()
}
pub fn supports_pipelining(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Pipelining))
}
pub fn supports_smtputf8(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::SmtpUtf8))
}
pub fn supports_sasl_ir(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::SaslIr))
}
pub fn supports_dsn(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Dsn))
}
pub fn supports_requiretls(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::RequireTls))
}
pub fn supports_future_release(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::FutureRelease { .. }))
}
pub fn future_release_max_interval(&self) -> Option<u64> {
self.extensions.iter().find_map(|ext| {
if let SmtpExtension::FutureRelease { max_interval, .. } = ext {
*max_interval
} else {
None
}
})
}
pub fn future_release_max_datetime(&self) -> Option<&str> {
self.extensions.iter().find_map(|ext| {
if let SmtpExtension::FutureRelease { max_datetime, .. } = ext {
max_datetime.as_deref()
} else {
None
}
})
}
pub fn supports_deliver_by(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::DeliverBy(_)))
}
pub fn deliver_by_min(&self) -> Option<u64> {
self.extensions.iter().find_map(|ext| {
if let SmtpExtension::DeliverBy(min) = ext {
*min
} else {
None
}
})
}
pub fn supports_mt_priority(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::MtPriority))
}
pub fn supports_vrfy(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Vrfy))
}
pub fn supports_expn(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::Expn))
}
pub fn supports_enhanced_status_codes(&self) -> bool {
self.has_extension(|ext| matches!(ext, SmtpExtension::EnhancedStatusCodes))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SmtpAuthParam {
Mailbox(Mailbox),
Empty,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MailFromParams {
pub size: Option<u64>,
pub body: Option<BodyType>,
pub smtputf8: bool,
pub requiretls: bool,
pub ret: Option<DsnRet>,
pub envid: Option<EnvidValue>,
pub hold_for: Option<u64>,
pub hold_until: Option<String>,
pub deliver_by: Option<DeliverBy>,
pub mt_priority: Option<i8>,
pub auth: Option<SmtpAuthParam>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RcptToParams {
pub notify: Option<Vec<DsnNotify>>,
pub orcpt: Option<String>,
}
impl RcptToParams {
pub fn is_empty(&self) -> bool {
let notify_is_empty = self.notify.as_ref().map_or(true, Vec::is_empty);
notify_is_empty && self.orcpt.is_none()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeliverBy {
pub seconds: i64,
pub mode: DeliverByMode,
pub trace: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DeliverByMode {
Notify,
Return,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DsnRet {
Full,
Hdrs,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DsnNotify {
Success,
Failure,
Delay,
Never,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BodyType {
SevenBit,
EightBitMime,
BinaryMime,
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;