//! SMTP client error types. use serde::Serialize; /// Errors that can occur during SMTP client operations. #[derive(Debug, thiserror::Error, Serialize)] pub enum SmtpClientError { #[error("Connection error: {message}")] ConnectionError { message: String }, #[error("Timeout: {message}")] TimeoutError { message: String }, #[error("TLS error: {message}")] TlsError { message: String }, #[error("Authentication failed: {message}")] AuthenticationError { message: String }, #[error("Protocol error ({code}): {message}")] ProtocolError { code: u16, message: String }, #[error("Pool exhausted: {message}")] PoolExhausted { message: String }, #[error("Invalid configuration: {message}")] ConfigError { message: String }, } impl SmtpClientError { /// Whether this error is retryable (temporary failure). /// Permanent failures (5xx, auth failures) are not retryable. pub fn is_retryable(&self) -> bool { match self { SmtpClientError::ConnectionError { .. } => true, SmtpClientError::TimeoutError { .. } => true, SmtpClientError::TlsError { .. } => false, SmtpClientError::AuthenticationError { .. } => false, SmtpClientError::ProtocolError { code, .. } => *code >= 400 && *code < 500, SmtpClientError::PoolExhausted { .. } => true, SmtpClientError::ConfigError { .. } => false, } } /// The error type as a string for IPC serialization. pub fn error_type(&self) -> &'static str { match self { SmtpClientError::ConnectionError { .. } => "connection", SmtpClientError::TimeoutError { .. } => "timeout", SmtpClientError::TlsError { .. } => "tls", SmtpClientError::AuthenticationError { .. } => "authentication", SmtpClientError::ProtocolError { .. } => "protocol", SmtpClientError::PoolExhausted { .. } => "pool_exhausted", SmtpClientError::ConfigError { .. } => "config", } } /// The SMTP code if this is a protocol error. pub fn smtp_code(&self) -> Option { match self { SmtpClientError::ProtocolError { code, .. } => Some(*code), _ => None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_retryable_errors() { assert!(SmtpClientError::ConnectionError { message: "refused".into() } .is_retryable()); assert!(SmtpClientError::TimeoutError { message: "timed out".into() } .is_retryable()); assert!(SmtpClientError::PoolExhausted { message: "full".into() } .is_retryable()); assert!(SmtpClientError::ProtocolError { code: 421, message: "try later".into() } .is_retryable()); assert!(SmtpClientError::ProtocolError { code: 450, message: "mailbox busy".into() } .is_retryable()); } #[test] fn test_non_retryable_errors() { assert!(!SmtpClientError::AuthenticationError { message: "bad creds".into() } .is_retryable()); assert!(!SmtpClientError::TlsError { message: "cert invalid".into() } .is_retryable()); assert!(!SmtpClientError::ProtocolError { code: 550, message: "no such user".into() } .is_retryable()); assert!(!SmtpClientError::ProtocolError { code: 554, message: "rejected".into() } .is_retryable()); assert!(!SmtpClientError::ConfigError { message: "bad config".into() } .is_retryable()); } #[test] fn test_error_type_strings() { assert_eq!( SmtpClientError::ConnectionError { message: "x".into() } .error_type(), "connection" ); assert_eq!( SmtpClientError::ProtocolError { code: 550, message: "x".into() } .error_type(), "protocol" ); } #[test] fn test_smtp_code() { assert_eq!( SmtpClientError::ProtocolError { code: 550, message: "x".into() } .smtp_code(), Some(550) ); assert_eq!( SmtpClientError::ConnectionError { message: "x".into() } .smtp_code(), None ); } }