daaki-smtp 0.2.0

An async SMTP client library
Documentation
//! Error types for SMTP operations.
//!
//! Distinguishes permanent (5xx), transient (4xx), I/O, auth, parse, and timeout errors.
//! Reply code classes are defined in RFC 5321 Section 4.2.1.

use std::sync::Arc;

use crate::types::SmtpResponse;

/// Error type for SMTP client operations.
///
/// Implements `Serialize`/`Deserialize` behind the `serde` feature flag.
/// The [`Io`](Error::Io) variant is serialized as its
/// [`ErrorKind`](std::io::ErrorKind) name and message string; on
/// deserialization an `std::io::Error` is reconstructed from these fields.
#[non_exhaustive]
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
    /// Underlying I/O error (includes TLS transport errors).
    ///
    /// Wrapped in [`Arc`] so that `Error` can implement `Clone`.
    #[error("I/O error: {0}")]
    Io(#[source] Arc<std::io::Error>),

    /// Authentication was rejected by the server (RFC 4954 Section 4).
    ///
    /// The `response` field preserves the full server reply so callers can
    /// distinguish transient failures (454) from permanent ones (535).
    #[error("authentication failed: {message}")]
    Auth {
        message: String,
        response: SmtpResponse,
    },

    /// Permanent failure — 5xx response (RFC 5321 Section 4.2.1). Do not retry.
    #[error("permanent failure ({code}): {message}")]
    Permanent {
        code: u16,
        message: String,
        response: SmtpResponse,
    },

    /// Transient failure — 4xx response (RFC 5321 Section 4.2.1). May succeed on retry.
    #[error("transient failure ({code}): {message}")]
    Transient {
        code: u16,
        message: String,
        response: SmtpResponse,
    },

    /// SMTP protocol violation by the server (RFC 5321 Section 4.2).
    #[error("protocol error: {0}")]
    Protocol(String),

    /// Failed to parse a server response (RFC 5321 Section 4.2).
    #[error("parse error: {0}")]
    Parse(String),

    /// Operation exceeded the caller-supplied timeout (RFC 5321 Section 4.5.3.2).
    #[error("operation timed out")]
    Timeout,

    /// The connection has been closed (RFC 5321 Section 3.8).
    #[error("connection closed")]
    Closed,

    /// STARTTLS was requested but the server does not advertise it (RFC 3207).
    #[error("STARTTLS not supported by server")]
    StartTlsUnavailable,

    /// All recipients were rejected (RFC 5321 Section 3.3 / RFC 1854 Section 3).
    #[error("all {count} recipients were rejected")]
    AllRecipientsFailed {
        count: usize,
        responses: Vec<SmtpResponse>,
    },

    /// The message or envelope requires SMTPUTF8, but the server does not
    /// advertise the SMTPUTF8 extension (RFC 6531 Sections 3.1, 3.3, 3.4).
    #[error(
        "message requires SMTPUTF8 but the server does not advertise it \
         (RFC 6531 Sections 3.1, 3.3, 3.4)"
    )]
    SmtpUtf8Required,
}

/// Compares two SMTP errors for equality.
///
/// The [`Io`](Error::Io) variant compares by [`std::io::ErrorKind`] only, since
/// `std::io::Error` does not implement `PartialEq`. Two `Io` errors with the
/// same `ErrorKind` are considered equal even if their messages differ.
impl PartialEq for Error {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
            (
                Self::Auth {
                    message: m1,
                    response: r1,
                },
                Self::Auth {
                    message: m2,
                    response: r2,
                },
            ) => m1 == m2 && r1 == r2,
            (
                Self::Permanent {
                    code: c1,
                    message: m1,
                    response: r1,
                },
                Self::Permanent {
                    code: c2,
                    message: m2,
                    response: r2,
                },
            )
            | (
                Self::Transient {
                    code: c1,
                    message: m1,
                    response: r1,
                },
                Self::Transient {
                    code: c2,
                    message: m2,
                    response: r2,
                },
            ) => c1 == c2 && m1 == m2 && r1 == r2,
            (Self::Protocol(a), Self::Protocol(b)) | (Self::Parse(a), Self::Parse(b)) => a == b,
            (Self::Timeout, Self::Timeout)
            | (Self::Closed, Self::Closed)
            | (Self::StartTlsUnavailable, Self::StartTlsUnavailable)
            | (Self::SmtpUtf8Required, Self::SmtpUtf8Required) => true,
            (
                Self::AllRecipientsFailed {
                    count: c1,
                    responses: r1,
                },
                Self::AllRecipientsFailed {
                    count: c2,
                    responses: r2,
                },
            ) => c1 == c2 && r1 == r2,
            _ => false,
        }
    }
}

impl Eq for Error {}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Self::Io(Arc::new(e))
    }
}

impl From<crate::types::ValidationError> for Error {
    fn from(e: crate::types::ValidationError) -> Self {
        Self::Protocol(e.to_string())
    }
}

impl Error {
    /// Returns `true` if the error is transient and the operation may succeed on retry.
    ///
    /// Transient errors include 4xx SMTP responses (RFC 5321 Section 4.2.1),
    /// I/O errors, timeouts, and transient auth failures (454, RFC 4954 Section 4).
    pub fn is_transient(&self) -> bool {
        match self {
            Self::Transient { .. } | Self::Io(_) | Self::Timeout => true,
            // RFC 4954 Section 4: 454 is a transient auth failure.
            Self::Auth { response, .. } => response.is_transient_error(),
            _ => false,
        }
    }

    /// Returns `true` if the error is permanent and the operation should not be retried.
    ///
    /// Permanent errors include 5xx SMTP responses (RFC 5321 Section 4.2.1)
    /// and permanent authentication failures (535, RFC 4954 Section 4).
    pub fn is_permanent(&self) -> bool {
        match self {
            Self::Permanent { .. } => true,
            // RFC 4954 Section 4: 535 is a permanent auth failure.
            Self::Auth { response, .. } => response.is_permanent_error(),
            _ => false,
        }
    }
}

// ---------------------------------------------------------------------------
// Serde support — custom Serialize/Deserialize behind the `serde` feature
// ---------------------------------------------------------------------------

#[cfg(feature = "serde")]
mod serde_support {
    use super::{Arc, Error, SmtpResponse};
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    /// Convert an [`std::io::ErrorKind`] to its stable `Debug` name
    /// (e.g., `"ConnectionReset"`) for serialization.
    fn error_kind_to_str(kind: std::io::ErrorKind) -> &'static str {
        match kind {
            std::io::ErrorKind::NotFound => "NotFound",
            std::io::ErrorKind::PermissionDenied => "PermissionDenied",
            std::io::ErrorKind::ConnectionRefused => "ConnectionRefused",
            std::io::ErrorKind::ConnectionReset => "ConnectionReset",
            std::io::ErrorKind::ConnectionAborted => "ConnectionAborted",
            std::io::ErrorKind::NotConnected => "NotConnected",
            std::io::ErrorKind::AddrInUse => "AddrInUse",
            std::io::ErrorKind::AddrNotAvailable => "AddrNotAvailable",
            std::io::ErrorKind::BrokenPipe => "BrokenPipe",
            std::io::ErrorKind::AlreadyExists => "AlreadyExists",
            std::io::ErrorKind::WouldBlock => "WouldBlock",
            std::io::ErrorKind::InvalidInput => "InvalidInput",
            std::io::ErrorKind::InvalidData => "InvalidData",
            std::io::ErrorKind::TimedOut => "TimedOut",
            std::io::ErrorKind::WriteZero => "WriteZero",
            std::io::ErrorKind::Interrupted => "Interrupted",
            std::io::ErrorKind::Unsupported => "Unsupported",
            std::io::ErrorKind::UnexpectedEof => "UnexpectedEof",
            std::io::ErrorKind::OutOfMemory => "OutOfMemory",
            _ => "Other",
        }
    }

    /// Reconstruct an [`std::io::ErrorKind`] from its `Debug` name.
    /// Unrecognised names map to [`std::io::ErrorKind::Other`].
    fn error_kind_from_str(s: &str) -> std::io::ErrorKind {
        match s {
            "NotFound" => std::io::ErrorKind::NotFound,
            "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
            "ConnectionRefused" => std::io::ErrorKind::ConnectionRefused,
            "ConnectionReset" => std::io::ErrorKind::ConnectionReset,
            "ConnectionAborted" => std::io::ErrorKind::ConnectionAborted,
            "NotConnected" => std::io::ErrorKind::NotConnected,
            "AddrInUse" => std::io::ErrorKind::AddrInUse,
            "AddrNotAvailable" => std::io::ErrorKind::AddrNotAvailable,
            "BrokenPipe" => std::io::ErrorKind::BrokenPipe,
            "AlreadyExists" => std::io::ErrorKind::AlreadyExists,
            "WouldBlock" => std::io::ErrorKind::WouldBlock,
            "InvalidInput" => std::io::ErrorKind::InvalidInput,
            "InvalidData" => std::io::ErrorKind::InvalidData,
            "TimedOut" => std::io::ErrorKind::TimedOut,
            "WriteZero" => std::io::ErrorKind::WriteZero,
            "Interrupted" => std::io::ErrorKind::Interrupted,
            "Unsupported" => std::io::ErrorKind::Unsupported,
            "UnexpectedEof" => std::io::ErrorKind::UnexpectedEof,
            "OutOfMemory" => std::io::ErrorKind::OutOfMemory,
            _ => std::io::ErrorKind::Other,
        }
    }

    /// Serializable representation of an [`std::io::Error`].
    #[derive(Serialize, Deserialize)]
    struct IoFields {
        kind: String,
        message: String,
    }

    /// Serde-compatible mirror of [`Error`].
    ///
    /// Uses adjacently-tagged representation (`"type"` + `"data"`) so that
    /// unit variants serialize cleanly and struct variants keep their field names.
    #[derive(Serialize, Deserialize)]
    #[serde(tag = "type", content = "data")]
    enum ErrorRepr {
        Io(IoFields),
        Auth {
            message: String,
            response: SmtpResponse,
        },
        Permanent {
            code: u16,
            message: String,
            response: SmtpResponse,
        },
        Transient {
            code: u16,
            message: String,
            response: SmtpResponse,
        },
        Protocol {
            message: String,
        },
        Parse {
            message: String,
        },
        Timeout,
        Closed,
        StartTlsUnavailable,
        AllRecipientsFailed {
            count: usize,
            responses: Vec<SmtpResponse>,
        },
        SmtpUtf8Required,
    }

    impl From<&Error> for ErrorRepr {
        fn from(err: &Error) -> Self {
            match err {
                Error::Io(e) => Self::Io(IoFields {
                    kind: error_kind_to_str(e.kind()).to_owned(),
                    message: e.to_string(),
                }),
                Error::Auth { message, response } => Self::Auth {
                    message: message.clone(),
                    response: response.clone(),
                },
                Error::Permanent {
                    code,
                    message,
                    response,
                } => Self::Permanent {
                    code: *code,
                    message: message.clone(),
                    response: response.clone(),
                },
                Error::Transient {
                    code,
                    message,
                    response,
                } => Self::Transient {
                    code: *code,
                    message: message.clone(),
                    response: response.clone(),
                },
                Error::Protocol(msg) => Self::Protocol {
                    message: msg.clone(),
                },
                Error::Parse(msg) => Self::Parse {
                    message: msg.clone(),
                },
                Error::Timeout => Self::Timeout,
                Error::Closed => Self::Closed,
                Error::StartTlsUnavailable => Self::StartTlsUnavailable,
                Error::AllRecipientsFailed { count, responses } => Self::AllRecipientsFailed {
                    count: *count,
                    responses: responses.clone(),
                },
                Error::SmtpUtf8Required => Self::SmtpUtf8Required,
            }
        }
    }

    impl From<ErrorRepr> for Error {
        fn from(repr: ErrorRepr) -> Self {
            match repr {
                ErrorRepr::Io(fields) => {
                    let kind = error_kind_from_str(&fields.kind);
                    Self::Io(Arc::new(std::io::Error::new(kind, fields.message)))
                }
                ErrorRepr::Auth { message, response } => Self::Auth { message, response },
                ErrorRepr::Permanent {
                    code,
                    message,
                    response,
                } => Self::Permanent {
                    code,
                    message,
                    response,
                },
                ErrorRepr::Transient {
                    code,
                    message,
                    response,
                } => Self::Transient {
                    code,
                    message,
                    response,
                },
                ErrorRepr::Protocol { message } => Self::Protocol(message),
                ErrorRepr::Parse { message } => Self::Parse(message),
                ErrorRepr::Timeout => Self::Timeout,
                ErrorRepr::Closed => Self::Closed,
                ErrorRepr::StartTlsUnavailable => Self::StartTlsUnavailable,
                ErrorRepr::AllRecipientsFailed { count, responses } => {
                    Self::AllRecipientsFailed { count, responses }
                }
                ErrorRepr::SmtpUtf8Required => Self::SmtpUtf8Required,
            }
        }
    }

    impl Serialize for Error {
        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
            ErrorRepr::from(self).serialize(serializer)
        }
    }

    impl<'de> Deserialize<'de> for Error {
        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
            ErrorRepr::deserialize(deserializer).map(Self::from)
        }
    }
}

#[cfg(test)]
#[path = "error_tests.rs"]
mod tests;