1pub type Result<T> = std::result::Result<T, MailError>;
2
3#[derive(Debug, Clone, PartialEq, thiserror::Error)]
4pub enum MailError {
5 #[error("configuration error: {0}")]
6 Config(String),
7
8 #[error("invalid email message: {0}")]
9 Validation(String),
10
11 #[error("sender domain is not allowed: {domain}")]
12 SenderDomainNotAllowed { domain: String },
13
14 #[error("request rate limited")]
15 RateLimited,
16
17 #[error("relay authentication failed")]
18 Authentication,
19
20 #[error("relay rejected request: status={status}, message={message}")]
21 RelayRejected { status: u16, message: String },
22
23 #[error("temporary delivery failure: {0}")]
24 Temporary(String),
25
26 #[error("queue error: {0}")]
27 Queue(String),
28}
29
30impl MailError {
31 #[must_use]
32 pub const fn is_retryable(&self) -> bool {
33 match self {
34 Self::RateLimited | Self::Temporary(_) => true,
35 Self::RelayRejected { status, .. } => *status == 429 || *status >= 500,
36 Self::Config(_)
37 | Self::Validation(_)
38 | Self::SenderDomainNotAllowed { .. }
39 | Self::Authentication
40 | Self::Queue(_) => false,
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn retry_classification_marks_temporary_errors_retryable() {
51 let error = MailError::Temporary("timeout".to_owned());
52
53 assert!(error.is_retryable());
54 }
55
56 #[test]
57 fn retry_classification_marks_validation_errors_permanent() {
58 let error = MailError::Validation("missing body".to_owned());
59
60 assert!(!error.is_retryable());
61 }
62}