Skip to main content

daaki_smtp/
error.rs

1//! Error types for SMTP operations.
2//!
3//! Distinguishes permanent (5xx), transient (4xx), I/O, auth, parse, and timeout errors.
4//! Reply code classes are defined in RFC 5321 Section 4.2.1.
5
6use std::sync::Arc;
7
8use crate::types::SmtpResponse;
9
10/// Error type for SMTP client operations.
11///
12/// Implements `Serialize`/`Deserialize` behind the `serde` feature flag.
13/// The [`Io`](Error::Io) variant is serialized as its
14/// [`ErrorKind`](std::io::ErrorKind) name and message string; on
15/// deserialization an `std::io::Error` is reconstructed from these fields.
16#[non_exhaustive]
17#[derive(Debug, Clone, thiserror::Error)]
18pub enum Error {
19    /// Underlying I/O error (includes TLS transport errors).
20    ///
21    /// Wrapped in [`Arc`] so that `Error` can implement `Clone`.
22    #[error("I/O error: {0}")]
23    Io(#[source] Arc<std::io::Error>),
24
25    /// Authentication was rejected by the server (RFC 4954 Section 4).
26    ///
27    /// The `response` field preserves the full server reply so callers can
28    /// distinguish transient failures (454) from permanent ones (535).
29    #[error("authentication failed: {message}")]
30    Auth {
31        message: String,
32        response: SmtpResponse,
33    },
34
35    /// Permanent failure — 5xx response (RFC 5321 Section 4.2.1). Do not retry.
36    #[error("permanent failure ({code}): {message}")]
37    Permanent {
38        code: u16,
39        message: String,
40        response: SmtpResponse,
41    },
42
43    /// Transient failure — 4xx response (RFC 5321 Section 4.2.1). May succeed on retry.
44    #[error("transient failure ({code}): {message}")]
45    Transient {
46        code: u16,
47        message: String,
48        response: SmtpResponse,
49    },
50
51    /// SMTP protocol violation by the server (RFC 5321 Section 4.2).
52    #[error("protocol error: {0}")]
53    Protocol(String),
54
55    /// Failed to parse a server response (RFC 5321 Section 4.2).
56    #[error("parse error: {0}")]
57    Parse(String),
58
59    /// Operation exceeded the caller-supplied timeout (RFC 5321 Section 4.5.3.2).
60    #[error("operation timed out")]
61    Timeout,
62
63    /// The connection has been closed (RFC 5321 Section 3.8).
64    #[error("connection closed")]
65    Closed,
66
67    /// STARTTLS was requested but the server does not advertise it (RFC 3207).
68    #[error("STARTTLS not supported by server")]
69    StartTlsUnavailable,
70
71    /// All recipients were rejected (RFC 5321 Section 3.3 / RFC 1854 Section 3).
72    #[error("all {count} recipients were rejected")]
73    AllRecipientsFailed {
74        count: usize,
75        responses: Vec<SmtpResponse>,
76    },
77
78    /// The message or envelope requires SMTPUTF8, but the server does not
79    /// advertise the SMTPUTF8 extension (RFC 6531 Sections 3.1, 3.3, 3.4).
80    #[error(
81        "message requires SMTPUTF8 but the server does not advertise it \
82         (RFC 6531 Sections 3.1, 3.3, 3.4)"
83    )]
84    SmtpUtf8Required,
85}
86
87/// Compares two SMTP errors for equality.
88///
89/// The [`Io`](Error::Io) variant compares by [`std::io::ErrorKind`] only, since
90/// `std::io::Error` does not implement `PartialEq`. Two `Io` errors with the
91/// same `ErrorKind` are considered equal even if their messages differ.
92impl PartialEq for Error {
93    fn eq(&self, other: &Self) -> bool {
94        match (self, other) {
95            (Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
96            (
97                Self::Auth {
98                    message: m1,
99                    response: r1,
100                },
101                Self::Auth {
102                    message: m2,
103                    response: r2,
104                },
105            ) => m1 == m2 && r1 == r2,
106            (
107                Self::Permanent {
108                    code: c1,
109                    message: m1,
110                    response: r1,
111                },
112                Self::Permanent {
113                    code: c2,
114                    message: m2,
115                    response: r2,
116                },
117            )
118            | (
119                Self::Transient {
120                    code: c1,
121                    message: m1,
122                    response: r1,
123                },
124                Self::Transient {
125                    code: c2,
126                    message: m2,
127                    response: r2,
128                },
129            ) => c1 == c2 && m1 == m2 && r1 == r2,
130            (Self::Protocol(a), Self::Protocol(b)) | (Self::Parse(a), Self::Parse(b)) => a == b,
131            (Self::Timeout, Self::Timeout)
132            | (Self::Closed, Self::Closed)
133            | (Self::StartTlsUnavailable, Self::StartTlsUnavailable)
134            | (Self::SmtpUtf8Required, Self::SmtpUtf8Required) => true,
135            (
136                Self::AllRecipientsFailed {
137                    count: c1,
138                    responses: r1,
139                },
140                Self::AllRecipientsFailed {
141                    count: c2,
142                    responses: r2,
143                },
144            ) => c1 == c2 && r1 == r2,
145            _ => false,
146        }
147    }
148}
149
150impl Eq for Error {}
151
152impl From<std::io::Error> for Error {
153    fn from(e: std::io::Error) -> Self {
154        Self::Io(Arc::new(e))
155    }
156}
157
158impl From<crate::types::ValidationError> for Error {
159    fn from(e: crate::types::ValidationError) -> Self {
160        Self::Protocol(e.to_string())
161    }
162}
163
164impl Error {
165    /// Returns `true` if the error is transient and the operation may succeed on retry.
166    ///
167    /// Transient errors include 4xx SMTP responses (RFC 5321 Section 4.2.1),
168    /// I/O errors, timeouts, and transient auth failures (454, RFC 4954 Section 4).
169    pub fn is_transient(&self) -> bool {
170        match self {
171            Self::Transient { .. } | Self::Io(_) | Self::Timeout => true,
172            // RFC 4954 Section 4: 454 is a transient auth failure.
173            Self::Auth { response, .. } => response.is_transient_error(),
174            _ => false,
175        }
176    }
177
178    /// Returns `true` if the error is permanent and the operation should not be retried.
179    ///
180    /// Permanent errors include 5xx SMTP responses (RFC 5321 Section 4.2.1)
181    /// and permanent authentication failures (535, RFC 4954 Section 4).
182    pub fn is_permanent(&self) -> bool {
183        match self {
184            Self::Permanent { .. } => true,
185            // RFC 4954 Section 4: 535 is a permanent auth failure.
186            Self::Auth { response, .. } => response.is_permanent_error(),
187            _ => false,
188        }
189    }
190}
191
192// ---------------------------------------------------------------------------
193// Serde support — custom Serialize/Deserialize behind the `serde` feature
194// ---------------------------------------------------------------------------
195
196#[cfg(feature = "serde")]
197mod serde_support {
198    use super::{Arc, Error, SmtpResponse};
199    use serde::{Deserialize, Deserializer, Serialize, Serializer};
200
201    /// Convert an [`std::io::ErrorKind`] to its stable `Debug` name
202    /// (e.g., `"ConnectionReset"`) for serialization.
203    fn error_kind_to_str(kind: std::io::ErrorKind) -> &'static str {
204        match kind {
205            std::io::ErrorKind::NotFound => "NotFound",
206            std::io::ErrorKind::PermissionDenied => "PermissionDenied",
207            std::io::ErrorKind::ConnectionRefused => "ConnectionRefused",
208            std::io::ErrorKind::ConnectionReset => "ConnectionReset",
209            std::io::ErrorKind::ConnectionAborted => "ConnectionAborted",
210            std::io::ErrorKind::NotConnected => "NotConnected",
211            std::io::ErrorKind::AddrInUse => "AddrInUse",
212            std::io::ErrorKind::AddrNotAvailable => "AddrNotAvailable",
213            std::io::ErrorKind::BrokenPipe => "BrokenPipe",
214            std::io::ErrorKind::AlreadyExists => "AlreadyExists",
215            std::io::ErrorKind::WouldBlock => "WouldBlock",
216            std::io::ErrorKind::InvalidInput => "InvalidInput",
217            std::io::ErrorKind::InvalidData => "InvalidData",
218            std::io::ErrorKind::TimedOut => "TimedOut",
219            std::io::ErrorKind::WriteZero => "WriteZero",
220            std::io::ErrorKind::Interrupted => "Interrupted",
221            std::io::ErrorKind::Unsupported => "Unsupported",
222            std::io::ErrorKind::UnexpectedEof => "UnexpectedEof",
223            std::io::ErrorKind::OutOfMemory => "OutOfMemory",
224            _ => "Other",
225        }
226    }
227
228    /// Reconstruct an [`std::io::ErrorKind`] from its `Debug` name.
229    /// Unrecognised names map to [`std::io::ErrorKind::Other`].
230    fn error_kind_from_str(s: &str) -> std::io::ErrorKind {
231        match s {
232            "NotFound" => std::io::ErrorKind::NotFound,
233            "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
234            "ConnectionRefused" => std::io::ErrorKind::ConnectionRefused,
235            "ConnectionReset" => std::io::ErrorKind::ConnectionReset,
236            "ConnectionAborted" => std::io::ErrorKind::ConnectionAborted,
237            "NotConnected" => std::io::ErrorKind::NotConnected,
238            "AddrInUse" => std::io::ErrorKind::AddrInUse,
239            "AddrNotAvailable" => std::io::ErrorKind::AddrNotAvailable,
240            "BrokenPipe" => std::io::ErrorKind::BrokenPipe,
241            "AlreadyExists" => std::io::ErrorKind::AlreadyExists,
242            "WouldBlock" => std::io::ErrorKind::WouldBlock,
243            "InvalidInput" => std::io::ErrorKind::InvalidInput,
244            "InvalidData" => std::io::ErrorKind::InvalidData,
245            "TimedOut" => std::io::ErrorKind::TimedOut,
246            "WriteZero" => std::io::ErrorKind::WriteZero,
247            "Interrupted" => std::io::ErrorKind::Interrupted,
248            "Unsupported" => std::io::ErrorKind::Unsupported,
249            "UnexpectedEof" => std::io::ErrorKind::UnexpectedEof,
250            "OutOfMemory" => std::io::ErrorKind::OutOfMemory,
251            _ => std::io::ErrorKind::Other,
252        }
253    }
254
255    /// Serializable representation of an [`std::io::Error`].
256    #[derive(Serialize, Deserialize)]
257    struct IoFields {
258        kind: String,
259        message: String,
260    }
261
262    /// Serde-compatible mirror of [`Error`].
263    ///
264    /// Uses adjacently-tagged representation (`"type"` + `"data"`) so that
265    /// unit variants serialize cleanly and struct variants keep their field names.
266    #[derive(Serialize, Deserialize)]
267    #[serde(tag = "type", content = "data")]
268    enum ErrorRepr {
269        Io(IoFields),
270        Auth {
271            message: String,
272            response: SmtpResponse,
273        },
274        Permanent {
275            code: u16,
276            message: String,
277            response: SmtpResponse,
278        },
279        Transient {
280            code: u16,
281            message: String,
282            response: SmtpResponse,
283        },
284        Protocol {
285            message: String,
286        },
287        Parse {
288            message: String,
289        },
290        Timeout,
291        Closed,
292        StartTlsUnavailable,
293        AllRecipientsFailed {
294            count: usize,
295            responses: Vec<SmtpResponse>,
296        },
297        SmtpUtf8Required,
298    }
299
300    impl From<&Error> for ErrorRepr {
301        fn from(err: &Error) -> Self {
302            match err {
303                Error::Io(e) => Self::Io(IoFields {
304                    kind: error_kind_to_str(e.kind()).to_owned(),
305                    message: e.to_string(),
306                }),
307                Error::Auth { message, response } => Self::Auth {
308                    message: message.clone(),
309                    response: response.clone(),
310                },
311                Error::Permanent {
312                    code,
313                    message,
314                    response,
315                } => Self::Permanent {
316                    code: *code,
317                    message: message.clone(),
318                    response: response.clone(),
319                },
320                Error::Transient {
321                    code,
322                    message,
323                    response,
324                } => Self::Transient {
325                    code: *code,
326                    message: message.clone(),
327                    response: response.clone(),
328                },
329                Error::Protocol(msg) => Self::Protocol {
330                    message: msg.clone(),
331                },
332                Error::Parse(msg) => Self::Parse {
333                    message: msg.clone(),
334                },
335                Error::Timeout => Self::Timeout,
336                Error::Closed => Self::Closed,
337                Error::StartTlsUnavailable => Self::StartTlsUnavailable,
338                Error::AllRecipientsFailed { count, responses } => Self::AllRecipientsFailed {
339                    count: *count,
340                    responses: responses.clone(),
341                },
342                Error::SmtpUtf8Required => Self::SmtpUtf8Required,
343            }
344        }
345    }
346
347    impl From<ErrorRepr> for Error {
348        fn from(repr: ErrorRepr) -> Self {
349            match repr {
350                ErrorRepr::Io(fields) => {
351                    let kind = error_kind_from_str(&fields.kind);
352                    Self::Io(Arc::new(std::io::Error::new(kind, fields.message)))
353                }
354                ErrorRepr::Auth { message, response } => Self::Auth { message, response },
355                ErrorRepr::Permanent {
356                    code,
357                    message,
358                    response,
359                } => Self::Permanent {
360                    code,
361                    message,
362                    response,
363                },
364                ErrorRepr::Transient {
365                    code,
366                    message,
367                    response,
368                } => Self::Transient {
369                    code,
370                    message,
371                    response,
372                },
373                ErrorRepr::Protocol { message } => Self::Protocol(message),
374                ErrorRepr::Parse { message } => Self::Parse(message),
375                ErrorRepr::Timeout => Self::Timeout,
376                ErrorRepr::Closed => Self::Closed,
377                ErrorRepr::StartTlsUnavailable => Self::StartTlsUnavailable,
378                ErrorRepr::AllRecipientsFailed { count, responses } => {
379                    Self::AllRecipientsFailed { count, responses }
380                }
381                ErrorRepr::SmtpUtf8Required => Self::SmtpUtf8Required,
382            }
383        }
384    }
385
386    impl Serialize for Error {
387        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
388            ErrorRepr::from(self).serialize(serializer)
389        }
390    }
391
392    impl<'de> Deserialize<'de> for Error {
393        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
394            ErrorRepr::deserialize(deserializer).map(Self::from)
395        }
396    }
397}
398
399#[cfg(test)]
400#[path = "error_tests.rs"]
401mod tests;