Documentation
//! Error and result type for SMTP clients

use std::{error::Error as StdError, fmt};

use crate::{
    BoxError,
    transport::smtp::response::{Code, Severity},
};

// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs

/// The Errors that may occur when sending an email over SMTP
pub struct Error {
    inner: Box<Inner>,
}

struct Inner {
    kind: Kind,
    source: Option<BoxError>,
}

impl Error {
    pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
    where
        E: Into<BoxError>,
    {
        Error {
            inner: Box::new(Inner {
                kind,
                source: source.map(Into::into),
            }),
        }
    }

    /// Returns true if the error is from response
    pub fn is_response(&self) -> bool {
        matches!(self.inner.kind, Kind::Response)
    }

    /// Returns true if the error is from client
    pub fn is_client(&self) -> bool {
        matches!(self.inner.kind, Kind::Client)
    }

    /// Returns true if the error is a transient SMTP error
    pub fn is_transient(&self) -> bool {
        matches!(self.inner.kind, Kind::Transient(_))
    }

    /// Returns true if the error is a permanent SMTP error
    pub fn is_permanent(&self) -> bool {
        matches!(self.inner.kind, Kind::Permanent(_))
    }

    /// Returns true if the error is caused by a timeout
    pub fn is_timeout(&self) -> bool {
        let mut source = self.source();

        while let Some(err) = source {
            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
                return io_err.kind() == std::io::ErrorKind::TimedOut;
            }

            source = err.source();
        }

        false
    }

    /// Returns true if the error is from TLS
    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
    #[cfg_attr(
        docsrs,
        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
    )]
    pub fn is_tls(&self) -> bool {
        matches!(self.inner.kind, Kind::Tls)
    }

    /// Returns true if the error is because the transport was shut down
    pub fn is_transport_shutdown(&self) -> bool {
        matches!(self.inner.kind, Kind::TransportShutdown)
    }

    /// Returns the status code, if the error was generated from a response.
    pub fn status(&self) -> Option<Code> {
        match self.inner.kind {
            Kind::Transient(code) | Kind::Permanent(code) => Some(code),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub(crate) enum Kind {
    /// Transient SMTP error, 4xx reply code
    ///
    /// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
    Transient(Code),
    /// Permanent SMTP error, 5xx reply code
    ///
    /// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
    Permanent(Code),
    /// Error parsing a response
    Response,
    /// Internal client error
    Client,
    /// Connection error
    Connection,
    /// Underlying network i/o error
    Network,
    /// TLS error
    #[cfg_attr(
        docsrs,
        doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
    )]
    #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
    Tls,
    /// Transport shutdown error
    TransportShutdown,
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut builder = f.debug_struct("lettre::transport::smtp::Error");

        builder.field("kind", &self.inner.kind);

        if let Some(source) = &self.inner.source {
            builder.field("source", source);
        }

        builder.finish()
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.inner.kind {
            Kind::Response => f.write_str("response error")?,
            Kind::Client => f.write_str("internal client error")?,
            Kind::Network => f.write_str("network error")?,
            Kind::Connection => f.write_str("Connection error")?,
            #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
            Kind::Tls => f.write_str("tls error")?,
            Kind::TransportShutdown => f.write_str("transport has been shut down")?,
            Kind::Transient(code) => {
                write!(f, "transient error ({code})")?;
            }
            Kind::Permanent(code) => {
                write!(f, "permanent error ({code})")?;
            }
        }

        if let Some(e) = &self.inner.source {
            write!(f, ": {e}")?;
        }

        Ok(())
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.inner.source.as_ref().map(|e| {
            let r: &(dyn std::error::Error + 'static) = &**e;
            r
        })
    }
}

pub(crate) fn code(c: Code, s: Option<String>) -> Error {
    match c.severity {
        Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
        Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
        _ => client("Unknown error code"),
    }
}

pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
    Error::new(Kind::Response, Some(e))
}

pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
    Error::new(Kind::Client, Some(e))
}

pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
    Error::new(Kind::Network, Some(e))
}

pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
    Error::new(Kind::Connection, Some(e))
}

#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
    Error::new(Kind::Tls, Some(e))
}

pub(crate) fn transport_shutdown() -> Error {
    Error::new::<BoxError>(Kind::TransportShutdown, None)
}