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