Skip to main content

daaki_imap/
error.rs

1//! Error types for IMAP operations.
2//!
3//! Distinguishes protocol errors, I/O errors, auth failures, parse errors, and timeouts.
4//! Server status responses (OK, NO, BAD, BYE) are defined in RFC 3501 Section 7.1
5//! and RFC 9051 Section 7.1.
6
7use std::sync::Arc;
8
9use crate::types::ResponseCode;
10
11/// Error type for IMAP client operations.
12///
13/// Implements `Serialize`/`Deserialize` behind the `serde` feature flag.
14/// The [`Io`](Error::Io) variant is serialized as its
15/// [`ErrorKind`](std::io::ErrorKind) name and message string; on
16/// deserialization an `std::io::Error` is reconstructed from these fields.
17#[non_exhaustive]
18#[derive(Debug, Clone, thiserror::Error)]
19pub enum Error {
20    /// Underlying I/O error, including TLS transport errors (RFC 3501 Section 2.1).
21    ///
22    /// Wrapped in [`Arc`] so that `Error` can implement `Clone`.
23    #[error("I/O error: {0}")]
24    Io(#[source] Arc<std::io::Error>),
25
26    /// Authentication was rejected by the server (RFC 3501 Section 6.2.2).
27    ///
28    /// The optional [`ResponseCode`] carries the structured reason code
29    /// (e.g., `[AUTHENTICATIONFAILED]`, `[EXPIRED]`, `[PRIVACYREQUIRED]`)
30    /// when the server provides one (RFC 5530 Section 3).
31    #[error("authentication failed: {text}")]
32    Auth {
33        /// Human-readable response text.
34        text: String,
35        /// Structured response code, if present (RFC 5530 Section 3).
36        code: Option<ResponseCode>,
37    },
38
39    /// Server returned a NO response to a command (RFC 3501 Section 7.1.2).
40    ///
41    /// The optional [`ResponseCode`] carries the structured reason code
42    /// (e.g., `[NOPERM]`, `[OVERQUOTA]`) when the server provides one
43    /// (RFC 5530 Section 3).
44    #[error("server rejected command: {text}")]
45    No {
46        /// Human-readable response text.
47        text: String,
48        /// Structured response code, if present (RFC 5530 Section 3).
49        code: Option<ResponseCode>,
50    },
51
52    /// Server returned a BAD response — client sent something invalid (RFC 3501 Section 7.1.3).
53    ///
54    /// The optional [`ResponseCode`] carries the structured reason code
55    /// when the server provides one (RFC 5530 Section 3).
56    #[error("server reported bad command: {text}")]
57    Bad {
58        /// Human-readable response text.
59        text: String,
60        /// Structured response code, if present (RFC 5530 Section 3).
61        code: Option<ResponseCode>,
62    },
63
64    /// Server sent BYE — closing connection (RFC 3501 Section 7.1.5).
65    ///
66    /// BYE responses can include response codes such as `[ALERT]` or
67    /// `[UNAVAILABLE]` that carry actionable information for the client
68    /// (RFC 3501 Section 7.1.5, RFC 5530 Section 3).
69    /// The `[ALERT]` code in particular MUST be presented to the user
70    /// (RFC 3501 Section 7.1).
71    #[error("server closing connection: {text}")]
72    Bye {
73        /// Human-readable response text.
74        text: String,
75        /// Structured response code, if present (RFC 5530 Section 3).
76        code: Option<ResponseCode>,
77    },
78
79    /// IMAP protocol violation by the server (RFC 3501 Section 7 / RFC 9051 Section 7).
80    #[error("protocol error: {0}")]
81    Protocol(String),
82
83    /// Failed to parse a server response (RFC 3501 Section 7 / RFC 9051 Section 7).
84    #[error("parse error: {0}")]
85    Parse(String),
86
87    /// Operation exceeded the caller-supplied timeout.
88    ///
89    /// This is a client-imposed constraint, not a protocol-level error.
90    /// See RFC 3501 Section 5.4 for the server-side autologout timer;
91    /// client-side timeouts guard against indefinite blocking on I/O.
92    #[error("operation timed out")]
93    Timeout,
94
95    /// The TCP connection has been closed (RFC 3501 Section 2.1).
96    #[error("connection closed")]
97    Closed,
98
99    /// STARTTLS was requested but the server does not advertise it
100    /// (RFC 3501 Section 6.2.1, RFC 9051 Section 6.2.1).
101    #[error("STARTTLS not supported by server")]
102    StartTlsUnavailable,
103
104    /// A capability required for the requested operation is not advertised
105    /// (RFC 3501 Section 6.1.1).
106    #[error("missing required capability: {0}")]
107    MissingCapability(String),
108
109    /// Message exceeds the server's advertised APPENDLIMIT (RFC 7889 Section 3).
110    #[error("message size {size} exceeds server APPENDLIMIT of {limit}")]
111    AppendLimit {
112        /// Size of the message the caller tried to append (RFC 7889 Section 3).
113        size: u64,
114        /// Server-advertised maximum in octets (RFC 7889 Section 5).
115        limit: u64,
116    },
117
118    /// The date-time string supplied to APPEND does not conform to the
119    /// `date-time` production in RFC 3501 Section 9.
120    ///
121    /// ```text
122    /// date-time      = DQUOTE date-day-fixed "-" date-month "-" date-year
123    ///                  SP time SP zone DQUOTE
124    /// date-day-fixed = (SP DIGIT) / 2DIGIT
125    /// date-month     = "Jan" / "Feb" / ... / "Dec"
126    /// time           = 2DIGIT ":" 2DIGIT ":" 2DIGIT
127    /// zone           = ("+" / "-") 4DIGIT
128    /// ```
129    #[error("invalid APPEND date-time: {0}")]
130    InvalidAppendDate(String),
131
132    /// Internal driver error — the driver task stub has not been replaced
133    /// by its full implementation yet, or an invariant was violated that
134    /// indicates a bug in the library.
135    #[error("internal error: {0}")]
136    Internal(String),
137
138    /// The driver task panicked. The payload is the panic message
139    /// extracted from the `JoinError` (best-effort — non-string panics
140    /// produce a generic description).
141    #[error("driver task panicked: {0}")]
142    DriverPanicked(String),
143
144    /// The driver task exited (cleanly or via cancellation) and the
145    /// command channel is closed, but no panic was observed.
146    #[error("driver task gone")]
147    DriverGone,
148}
149
150impl From<std::io::Error> for Error {
151    fn from(e: std::io::Error) -> Self {
152        Self::Io(Arc::new(e))
153    }
154}
155
156impl From<crate::types::ValidationError> for Error {
157    fn from(e: crate::types::ValidationError) -> Self {
158        Self::Protocol(e.to_string())
159    }
160}
161
162impl From<crate::codec::encode::EncodeError> for Error {
163    fn from(e: crate::codec::encode::EncodeError) -> Self {
164        match e {
165            crate::codec::encode::EncodeError::MissingCapability { cmd, cap } => {
166                Self::MissingCapability(format!("{cmd} requires {cap}"))
167            }
168            crate::codec::encode::EncodeError::Validation(msg) => Self::Protocol(msg),
169        }
170    }
171}
172
173/// Compares two IMAP errors for equality.
174///
175/// The [`Io`](Error::Io) variant compares by [`std::io::ErrorKind`] only, since
176/// `std::io::Error` does not implement `PartialEq`. Two `Io` errors with the
177/// same `ErrorKind` are considered equal even if their messages differ.
178impl PartialEq for Error {
179    fn eq(&self, other: &Self) -> bool {
180        match (self, other) {
181            (Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
182            (Self::Auth { text: t1, code: c1 }, Self::Auth { text: t2, code: c2 })
183            | (Self::No { text: t1, code: c1 }, Self::No { text: t2, code: c2 })
184            | (Self::Bad { text: t1, code: c1 }, Self::Bad { text: t2, code: c2 })
185            | (Self::Bye { text: t1, code: c1 }, Self::Bye { text: t2, code: c2 }) => {
186                t1 == t2 && c1 == c2
187            }
188            (Self::Protocol(a), Self::Protocol(b))
189            | (Self::Parse(a), Self::Parse(b))
190            | (Self::MissingCapability(a), Self::MissingCapability(b))
191            | (Self::InvalidAppendDate(a), Self::InvalidAppendDate(b))
192            | (Self::Internal(a), Self::Internal(b))
193            | (Self::DriverPanicked(a), Self::DriverPanicked(b)) => a == b,
194            (Self::Timeout, Self::Timeout)
195            | (Self::Closed, Self::Closed)
196            | (Self::StartTlsUnavailable, Self::StartTlsUnavailable)
197            | (Self::DriverGone, Self::DriverGone) => true,
198            (
199                Self::AppendLimit {
200                    size: s1,
201                    limit: l1,
202                },
203                Self::AppendLimit {
204                    size: s2,
205                    limit: l2,
206                },
207            ) => s1 == s2 && l1 == l2,
208            _ => false,
209        }
210    }
211}
212
213impl Eq for Error {}
214
215impl Error {
216    /// Construct an [`Error::No`] with an optional response code (RFC 5530 Section 3).
217    pub(crate) fn no_with_code(text: String, code: Option<ResponseCode>) -> Self {
218        Self::No { text, code }
219    }
220
221    /// Construct an [`Error::Bad`] with an optional response code (RFC 5530 Section 3).
222    pub(crate) fn bad_with_code(text: String, code: Option<ResponseCode>) -> Self {
223        Self::Bad { text, code }
224    }
225
226    /// Construct an [`Error::Auth`] with an optional response code (RFC 5530 Section 3).
227    pub(crate) fn auth_with_code(text: String, code: Option<ResponseCode>) -> Self {
228        Self::Auth { text, code }
229    }
230
231    /// Construct an [`Error::Bye`] with an optional response code
232    /// (RFC 3501 Section 7.1.5, RFC 5530 Section 3).
233    pub(crate) fn bye_with_code(text: String, code: Option<ResponseCode>) -> Self {
234        Self::Bye { text, code }
235    }
236}
237
238// ---------------------------------------------------------------------------
239// Serde support — custom Serialize/Deserialize behind the `serde` feature
240// ---------------------------------------------------------------------------
241
242#[cfg(feature = "serde")]
243mod serde_support {
244    use super::{Arc, Error, ResponseCode};
245    use serde::{Deserialize, Deserializer, Serialize, Serializer};
246
247    /// Convert an [`std::io::ErrorKind`] to its stable `Debug` name
248    /// (e.g., `"ConnectionReset"`) for serialization.
249    fn error_kind_to_str(kind: std::io::ErrorKind) -> &'static str {
250        match kind {
251            std::io::ErrorKind::NotFound => "NotFound",
252            std::io::ErrorKind::PermissionDenied => "PermissionDenied",
253            std::io::ErrorKind::ConnectionRefused => "ConnectionRefused",
254            std::io::ErrorKind::ConnectionReset => "ConnectionReset",
255            std::io::ErrorKind::ConnectionAborted => "ConnectionAborted",
256            std::io::ErrorKind::NotConnected => "NotConnected",
257            std::io::ErrorKind::AddrInUse => "AddrInUse",
258            std::io::ErrorKind::AddrNotAvailable => "AddrNotAvailable",
259            std::io::ErrorKind::BrokenPipe => "BrokenPipe",
260            std::io::ErrorKind::AlreadyExists => "AlreadyExists",
261            std::io::ErrorKind::WouldBlock => "WouldBlock",
262            std::io::ErrorKind::InvalidInput => "InvalidInput",
263            std::io::ErrorKind::InvalidData => "InvalidData",
264            std::io::ErrorKind::TimedOut => "TimedOut",
265            std::io::ErrorKind::WriteZero => "WriteZero",
266            std::io::ErrorKind::Interrupted => "Interrupted",
267            std::io::ErrorKind::Unsupported => "Unsupported",
268            std::io::ErrorKind::UnexpectedEof => "UnexpectedEof",
269            std::io::ErrorKind::OutOfMemory => "OutOfMemory",
270            _ => "Other",
271        }
272    }
273
274    /// Reconstruct an [`std::io::ErrorKind`] from its `Debug` name.
275    /// Unrecognised names map to [`std::io::ErrorKind::Other`].
276    fn error_kind_from_str(s: &str) -> std::io::ErrorKind {
277        match s {
278            "NotFound" => std::io::ErrorKind::NotFound,
279            "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
280            "ConnectionRefused" => std::io::ErrorKind::ConnectionRefused,
281            "ConnectionReset" => std::io::ErrorKind::ConnectionReset,
282            "ConnectionAborted" => std::io::ErrorKind::ConnectionAborted,
283            "NotConnected" => std::io::ErrorKind::NotConnected,
284            "AddrInUse" => std::io::ErrorKind::AddrInUse,
285            "AddrNotAvailable" => std::io::ErrorKind::AddrNotAvailable,
286            "BrokenPipe" => std::io::ErrorKind::BrokenPipe,
287            "AlreadyExists" => std::io::ErrorKind::AlreadyExists,
288            "WouldBlock" => std::io::ErrorKind::WouldBlock,
289            "InvalidInput" => std::io::ErrorKind::InvalidInput,
290            "InvalidData" => std::io::ErrorKind::InvalidData,
291            "TimedOut" => std::io::ErrorKind::TimedOut,
292            "WriteZero" => std::io::ErrorKind::WriteZero,
293            "Interrupted" => std::io::ErrorKind::Interrupted,
294            "Unsupported" => std::io::ErrorKind::Unsupported,
295            "UnexpectedEof" => std::io::ErrorKind::UnexpectedEof,
296            "OutOfMemory" => std::io::ErrorKind::OutOfMemory,
297            _ => std::io::ErrorKind::Other,
298        }
299    }
300
301    /// Serializable representation of an [`std::io::Error`].
302    #[derive(Serialize, Deserialize)]
303    struct IoFields {
304        kind: String,
305        message: String,
306    }
307
308    /// Serde-compatible mirror of [`Error`].
309    ///
310    /// Uses adjacently-tagged representation (`"type"` + `"data"`) so that
311    /// unit variants serialize cleanly and struct variants keep their field names.
312    #[derive(Serialize, Deserialize)]
313    #[serde(tag = "type", content = "data")]
314    enum ErrorRepr {
315        Io(IoFields),
316        Auth {
317            text: String,
318            code: Option<ResponseCode>,
319        },
320        No {
321            text: String,
322            code: Option<ResponseCode>,
323        },
324        Bad {
325            text: String,
326            code: Option<ResponseCode>,
327        },
328        Bye {
329            text: String,
330            code: Option<ResponseCode>,
331        },
332        Protocol {
333            message: String,
334        },
335        Parse {
336            message: String,
337        },
338        Timeout,
339        Closed,
340        StartTlsUnavailable,
341        MissingCapability {
342            capability: String,
343        },
344        AppendLimit {
345            size: u64,
346            limit: u64,
347        },
348        InvalidAppendDate {
349            date: String,
350        },
351        Internal {
352            message: String,
353        },
354        DriverPanicked {
355            message: String,
356        },
357        DriverGone,
358    }
359
360    impl From<&Error> for ErrorRepr {
361        fn from(err: &Error) -> Self {
362            match err {
363                Error::Io(e) => Self::Io(IoFields {
364                    kind: error_kind_to_str(e.kind()).to_owned(),
365                    message: e.to_string(),
366                }),
367                Error::Auth { text, code } => Self::Auth {
368                    text: text.clone(),
369                    code: code.clone(),
370                },
371                Error::No { text, code } => Self::No {
372                    text: text.clone(),
373                    code: code.clone(),
374                },
375                Error::Bad { text, code } => Self::Bad {
376                    text: text.clone(),
377                    code: code.clone(),
378                },
379                Error::Bye { text, code } => Self::Bye {
380                    text: text.clone(),
381                    code: code.clone(),
382                },
383                Error::Protocol(msg) => Self::Protocol {
384                    message: msg.clone(),
385                },
386                Error::Parse(msg) => Self::Parse {
387                    message: msg.clone(),
388                },
389                Error::Timeout => Self::Timeout,
390                Error::Closed => Self::Closed,
391                Error::StartTlsUnavailable => Self::StartTlsUnavailable,
392                Error::MissingCapability(cap) => Self::MissingCapability {
393                    capability: cap.clone(),
394                },
395                Error::AppendLimit { size, limit } => Self::AppendLimit {
396                    size: *size,
397                    limit: *limit,
398                },
399                Error::InvalidAppendDate(msg) => Self::InvalidAppendDate { date: msg.clone() },
400                Error::Internal(msg) => Self::Internal {
401                    message: msg.clone(),
402                },
403                Error::DriverPanicked(msg) => Self::DriverPanicked {
404                    message: msg.clone(),
405                },
406                Error::DriverGone => Self::DriverGone,
407            }
408        }
409    }
410
411    impl From<ErrorRepr> for Error {
412        fn from(repr: ErrorRepr) -> Self {
413            match repr {
414                ErrorRepr::Io(fields) => {
415                    let kind = error_kind_from_str(&fields.kind);
416                    Self::Io(Arc::new(std::io::Error::new(kind, fields.message)))
417                }
418                ErrorRepr::Auth { text, code } => Self::Auth { text, code },
419                ErrorRepr::No { text, code } => Self::No { text, code },
420                ErrorRepr::Bad { text, code } => Self::Bad { text, code },
421                ErrorRepr::Bye { text, code } => Self::Bye { text, code },
422                ErrorRepr::Protocol { message } => Self::Protocol(message),
423                ErrorRepr::Parse { message } => Self::Parse(message),
424                ErrorRepr::Timeout => Self::Timeout,
425                ErrorRepr::Closed => Self::Closed,
426                ErrorRepr::StartTlsUnavailable => Self::StartTlsUnavailable,
427                ErrorRepr::MissingCapability { capability } => Self::MissingCapability(capability),
428                ErrorRepr::AppendLimit { size, limit } => Self::AppendLimit { size, limit },
429                ErrorRepr::InvalidAppendDate { date } => Self::InvalidAppendDate(date),
430                ErrorRepr::Internal { message } => Self::Internal(message),
431                ErrorRepr::DriverPanicked { message } => Self::DriverPanicked(message),
432                ErrorRepr::DriverGone => Self::DriverGone,
433            }
434        }
435    }
436
437    impl Serialize for Error {
438        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
439            ErrorRepr::from(self).serialize(serializer)
440        }
441    }
442
443    impl<'de> Deserialize<'de> for Error {
444        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
445            ErrorRepr::deserialize(deserializer).map(Self::from)
446        }
447    }
448}
449
450#[cfg(test)]
451#[path = "error_tests.rs"]
452mod tests;