Skip to main content

jmap_mail_types/
vacation.rs

1//! RFC 8621 §8 VacationResponse object.
2//!
3//! Provides [`VacationResponse`] — a singleton object (one per account) that
4//! controls the automatic out-of-office reply behaviour for the account.
5
6use jmap_types::{Id, UTCDate};
7use serde::{Deserialize, Serialize};
8
9/// Vacation-response settings for an account (RFC 8621 §8).
10///
11/// There is exactly one `VacationResponse` object per account; its `id` is
12/// always the string `"singleton"` (RFC 8621 §8).
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct VacationResponse {
17    /// The object id.  Always `"singleton"` (server-set; immutable).
18    pub id: Id,
19
20    /// Whether the vacation response is currently active.
21    pub is_enabled: bool,
22
23    /// Start of the active window.  If `None`, the response is effective
24    /// immediately.  Only consulted when `is_enabled` is `true`.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub from_date: Option<UTCDate>,
27
28    /// End of the active window.  If `None`, the response is effective
29    /// indefinitely.  Only consulted when `is_enabled` is `true`.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub to_date: Option<UTCDate>,
32
33    /// Subject line for the auto-reply.  `None` means the server should
34    /// generate a suitable subject.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub subject: Option<String>,
37
38    /// Plaintext body for the auto-reply.  `None` means the server may derive
39    /// a text part from `html_body` or generate a default.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub text_body: Option<String>,
42
43    /// HTML body for the auto-reply.  `None` means the server may derive an
44    /// HTML part from `text_body` or send a text-only response.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub html_body: Option<String>,
47    /// Catch-all for vendor / site / private extension fields not covered
48    /// by the typed fields above. Preserves unknown fields across
49    /// deserialize/serialize round-trip per workspace extras-preservation
50    /// policy (see workspace AGENTS.md).
51    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
52    pub extra: serde_json::Map<String, serde_json::Value>,
53}
54
55impl VacationResponse {
56    /// Construct a [`VacationResponse`] from its two required fields.
57    ///
58    /// All optional fields default to `None`.
59    pub fn new(id: Id, is_enabled: bool) -> Self {
60        Self {
61            id,
62            is_enabled,
63            from_date: None,
64            to_date: None,
65            subject: None,
66            text_body: None,
67            html_body: None,
68            extra: serde_json::Map::new(),
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    /// Oracle: hand-written fixture derived from RFC 8621 §8 field descriptions.
78    /// The RFC does not provide a full JSON example; the fixture was constructed
79    /// from the spec's field definitions.
80    #[test]
81    fn vacation_response_full_roundtrip() {
82        let json = r#"{"id":"singleton","isEnabled":true,"fromDate":"2024-06-01T00:00:00Z","toDate":"2024-06-30T23:59:59Z","subject":"Out of office","textBody":"I am out of the office.","htmlBody":"<p>I am out of the office.</p>"}"#;
83        let vr: VacationResponse = serde_json::from_str(json).expect("must parse");
84        assert_eq!(vr.id, "singleton");
85        assert!(vr.is_enabled);
86        assert_eq!(
87            vr.from_date.as_ref().map(|d| d.as_ref()),
88            Some("2024-06-01T00:00:00Z")
89        );
90        assert_eq!(vr.subject.as_deref(), Some("Out of office"));
91        assert_eq!(vr.text_body.as_deref(), Some("I am out of the office."));
92        let back = serde_json::to_string(&vr).expect("serialize");
93        assert_eq!(back, json);
94    }
95
96    /// Oracle: disabled vacation response with no optional fields.
97    /// RFC 8621 §8 states only `id` and `isEnabled` are always present.
98    #[test]
99    fn vacation_response_minimal_roundtrip() {
100        let json = r#"{"id":"singleton","isEnabled":false}"#;
101        let vr: VacationResponse = serde_json::from_str(json).expect("must parse");
102        assert_eq!(vr.id, "singleton");
103        assert!(!vr.is_enabled);
104        assert!(vr.from_date.is_none());
105        assert!(vr.to_date.is_none());
106        assert!(vr.subject.is_none());
107        assert!(vr.text_body.is_none());
108        assert!(vr.html_body.is_none());
109        let back = serde_json::to_string(&vr).expect("serialize");
110        assert_eq!(back, json);
111    }
112
113    // ── Extras-preservation policy tests (JMAP-lbdy.2) ───────────────────
114
115    /// `VacationResponse.extra` captures vendor fields and preserves them across
116    /// deserialize/serialize round-trip.
117    #[test]
118    fn vacation_response_preserves_vendor_extras() {
119        let raw = serde_json::json!({
120            "id": "singleton",
121            "isEnabled": true,
122            "acmeCorpAutoExtend": true
123        });
124        let vr: VacationResponse = serde_json::from_value(raw).unwrap();
125        assert_eq!(
126            vr.extra.get("acmeCorpAutoExtend").and_then(|v| v.as_bool()),
127            Some(true)
128        );
129        let back = serde_json::to_value(&vr).unwrap();
130        assert_eq!(back["acmeCorpAutoExtend"], true);
131    }
132
133    /// Oracle: RFC 8621 §8 — optional fields are omitted from serialization
134    /// when None.
135    #[test]
136    fn vacation_response_none_fields_omitted() {
137        let vr = VacationResponse {
138            id: Id::from("singleton"),
139            is_enabled: false,
140            from_date: None,
141            to_date: None,
142            subject: None,
143            text_body: None,
144            html_body: None,
145            extra: serde_json::Map::new(),
146        };
147        let json = serde_json::to_string(&vr).expect("serialize");
148        assert!(!json.contains("fromDate"), "fromDate must be absent");
149        assert!(!json.contains("toDate"), "toDate must be absent");
150        assert!(!json.contains("subject"), "subject must be absent");
151        assert!(!json.contains("textBody"), "textBody must be absent");
152        assert!(!json.contains("htmlBody"), "htmlBody must be absent");
153    }
154}