Skip to main content

mailrs_dav/
error.rs

1//! CalDAV / CardDAV error variants used by handler return values.
2//!
3//! Each variant carries enough information to produce a meaningful HTTP
4//! response via [`DavError::to_dav_response`], so server-side wiring code can
5//! blanket-convert handler errors without inspecting them.
6
7use crate::xml::DavResponse;
8
9/// Errors a DAV handler can return.
10///
11/// Most variants map 1:1 to HTTP status codes; [`ServerError`](Self::ServerError)
12/// is the catch-all for anything the store impl can't classify.
13#[derive(Debug, Clone)]
14pub enum DavError {
15    /// 401 — auth required. Server-side wrapper is expected to add the
16    /// `WWW-Authenticate` header.
17    Unauthorized,
18    /// 403 — authenticated but not permitted.
19    Forbidden,
20    /// 404 — resource doesn't exist.
21    NotFound,
22    /// 400 — malformed request body / unsupported parameters.
23    BadRequest(String),
24    /// 409 — server-side semantic conflict (e.g. parent collection missing).
25    Conflict,
26    /// 412 — `If-Match` / `If-None-Match` precondition failed.
27    PreconditionFailed,
28    /// 405 — verb not allowed on this resource.
29    MethodNotAllowed,
30    /// 503 — backing store unavailable (e.g. database down).
31    ServiceUnavailable,
32    /// 500 — anything else. Description is for the server log, not the client.
33    ServerError(String),
34}
35
36impl DavError {
37    /// Convert into the minimal `DavResponse` a server can serve directly.
38    ///
39    /// Bodies are short, plain-text, ASCII; suitable as a fallback when the
40    /// server-side adapter doesn't want to do custom error formatting.
41    pub fn to_dav_response(&self) -> DavResponse {
42        let (status, body) = match self {
43            DavError::Unauthorized => (401, "authentication required"),
44            DavError::Forbidden => (403, "forbidden"),
45            DavError::NotFound => (404, "not found"),
46            DavError::BadRequest(_) => (400, "bad request"),
47            DavError::Conflict => (409, "conflict"),
48            DavError::PreconditionFailed => (412, "precondition failed"),
49            DavError::MethodNotAllowed => (405, "method not allowed"),
50            DavError::ServiceUnavailable => (503, "service unavailable"),
51            DavError::ServerError(_) => (500, "internal server error"),
52        };
53        let mut resp = DavResponse::new(status).with_body(body.as_bytes().to_vec());
54        if matches!(self, DavError::Unauthorized) {
55            resp = resp.with_header("www-authenticate", "Basic realm=\"mailrs-dav\"");
56        }
57        resp
58    }
59}
60
61impl std::fmt::Display for DavError {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            DavError::Unauthorized => write!(f, "unauthorized"),
65            DavError::Forbidden => write!(f, "forbidden"),
66            DavError::NotFound => write!(f, "not found"),
67            DavError::BadRequest(d) => write!(f, "bad request: {d}"),
68            DavError::Conflict => write!(f, "conflict"),
69            DavError::PreconditionFailed => write!(f, "precondition failed"),
70            DavError::MethodNotAllowed => write!(f, "method not allowed"),
71            DavError::ServiceUnavailable => write!(f, "service unavailable"),
72            DavError::ServerError(d) => write!(f, "server error: {d}"),
73        }
74    }
75}
76
77impl std::error::Error for DavError {}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn unauthorized_response_has_auth_header() {
85        let resp = DavError::Unauthorized.to_dav_response();
86        assert_eq!(resp.status, 401);
87        assert!(
88            resp.headers
89                .iter()
90                .any(|(k, _)| k.eq_ignore_ascii_case("www-authenticate"))
91        );
92    }
93
94    #[test]
95    fn not_found_maps_to_404() {
96        assert_eq!(DavError::NotFound.to_dav_response().status, 404);
97    }
98
99    #[test]
100    fn precondition_failed_maps_to_412() {
101        assert_eq!(DavError::PreconditionFailed.to_dav_response().status, 412);
102    }
103
104    #[test]
105    fn server_error_does_not_leak_description() {
106        // description is for logs; body must not include it
107        let resp = DavError::ServerError("db borked".into()).to_dav_response();
108        assert_eq!(resp.status, 500);
109        assert!(!String::from_utf8_lossy(&resp.body).contains("borked"));
110    }
111}