use std::fmt; const ENV_PREFIX: &str = "APP_SMTP_"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ConfigError { MissingEnv { name: &'static str }, InvalidEnv { name: &'static str, reason: String }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QueueError { Closed, Full, IdExhausted, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum EmailError { MissingSender, MissingRecipients, RequestSenderDisabled, IncompleteCredentials, InvalidAddress { field: &'static str, value: String, reason: String, }, InvalidContentType { filename: String, value: String, reason: String, }, InvalidHeader { name: String, reason: String, }, ForbiddenHeader { name: String, }, UnsupportedAttachmentUrl { filename: String, url: String, }, BuildTransport(String), BuildMessage(String), Send(String), } impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingEnv { name } => { write!(f, "missing environment variable {ENV_PREFIX}{name}") } Self::InvalidEnv { name, reason } => { write!( f, "invalid environment variable {ENV_PREFIX}{name}: {reason}" ) } } } } impl fmt::Display for QueueError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Closed => f.write_str("email queue is closed"), Self::Full => f.write_str("email queue is full"), Self::IdExhausted => f.write_str("email queue id space is exhausted"), } } } impl fmt::Display for EmailError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingSender => { f.write_str("missing sender: set request.from or APP_SMTP_FROM_EMAIL") } Self::MissingRecipients => { f.write_str("missing recipients: request.to must not be empty") } Self::RequestSenderDisabled => { f.write_str("request.from is disabled: use APP_SMTP_FROM_EMAIL or enable APP_SMTP_ALLOW_REQUEST_FROM") } Self::IncompleteCredentials => { f.write_str("APP_SMTP_USERNAME and APP_SMTP_PASSWORD must be set together") } Self::InvalidAddress { field, value, reason, } => write!(f, "invalid email address in {field} ({value}): {reason}"), Self::InvalidContentType { filename, value, reason, } => write!( f, "invalid content type for attachment {filename} ({value}): {reason}" ), Self::InvalidHeader { name, reason } => { write!(f, "invalid custom header {name}: {reason}") } Self::ForbiddenHeader { name } => { write!(f, "custom header {name} is managed by the mail builder") } Self::UnsupportedAttachmentUrl { filename, url } => write!( f, "attachment {filename} uses url {url}, but URL attachment fetching is not supported" ), Self::BuildTransport(reason) => write!(f, "failed to build SMTP transport: {reason}"), Self::BuildMessage(reason) => write!(f, "failed to build email message: {reason}"), Self::Send(reason) => write!(f, "failed to send email: {reason}"), } } } impl std::error::Error for ConfigError {} impl std::error::Error for QueueError {} impl std::error::Error for EmailError {} impl EmailError { /// 返回 true 表示该错误不可重试,应直接销毁任务 pub fn is_terminal(&self) -> bool { matches!( self, Self::MissingSender | Self::MissingRecipients | Self::RequestSenderDisabled | Self::IncompleteCredentials | Self::InvalidAddress { .. } | Self::InvalidContentType { .. } | Self::InvalidHeader { .. } | Self::ForbiddenHeader { .. } | Self::UnsupportedAttachmentUrl { .. } | Self::BuildTransport(_) | Self::BuildMessage(_) ) } pub fn is_retryable(&self) -> bool { !self.is_terminal() } }