Skip to main content

io_jmap/rfc8621/vacation_response/
set.rs

1//! JMAP `VacationResponse/set` coroutine (RFC 8621 §8.3): updates the singleton
2//! VacationResponse (id `"singleton"`).
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//!     io::{Read, Write},
9//!     net::TcpStream,
10//! };
11//!
12//! use io_jmap::{
13//!     coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
14//!     rfc8620::session::JmapSession,
15//!     rfc8621::vacation_response::set::{JmapVacationResponseSet, JmapVacationResponseUpdate},
16//! };
17//! use secrecy::SecretString;
18//!
19//! // Ready stream needed (TCP-connected, TLS-negociated)
20//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
21//! let mut buf = [0u8; 4096];
22//!
23//! let session: JmapSession = serde_json::from_str(r#"{
24//!     "username": "",
25//!     "accounts": {},
26//!     "primaryAccounts": {"urn:ietf:params:jmap:mail": "a1"},
27//!     "capabilities": {},
28//!     "apiUrl": "https://api.example.com/jmap/",
29//!     "downloadUrl": "",
30//!     "uploadUrl": "",
31//!     "eventSourceUrl": "",
32//!     "state": ""
33//! }"#).unwrap();
34//! let auth = SecretString::from("Bearer xyz");
35//! let patch = JmapVacationResponseUpdate {
36//!     is_enabled: Some(true),
37//!     subject: Some("Out of office".into()),
38//!     ..Default::default()
39//! };
40//! let mut coroutine = JmapVacationResponseSet::new(&session, &auth, patch).unwrap();
41//! let mut arg = None;
42//!
43//! let out = loop {
44//!     match coroutine.resume(arg.take()) {
45//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
46//!             stream.write_all(&bytes).unwrap();
47//!         }
48//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
49//!             let n = stream.read(&mut buf).unwrap();
50//!             arg = Some(&buf[..n]);
51//!         }
52//!         JmapCoroutineState::Complete(Ok(out)) => break out,
53//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
54//!     }
55//! };
56//!
57//! println!("new state {}", out.new_state);
58//! ```
59
60use alloc::{collections::BTreeMap, string::String, vec};
61
62use secrecy::SecretString;
63use serde::{Deserialize, Serialize};
64use thiserror::Error;
65
66use crate::{
67    coroutine::*,
68    jmap_try,
69    rfc8620::{
70        JMAP_CORE_CAPABILITY, error::JmapMethodError, request::JmapBatch, send::*,
71        session::JmapSession,
72    },
73    rfc8621::{
74        JMAP_MAIL_CAPABILITY,
75        vacation_response::{JMAP_VACATION_RESPONSE_CAPABILITY, JmapVacationResponse},
76    },
77};
78
79/// Patch object for `VacationResponse/set` update (RFC 8621 §8).
80///
81/// Only `Some` fields are serialized; `None` fields are left unchanged.
82#[derive(Clone, Debug, Default, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct JmapVacationResponseUpdate {
85    /// Whether the vacation response is sent.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub is_enabled: Option<bool>,
88    /// RFC 3339 start of the vacation period.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub from_date: Option<String>,
91    /// RFC 3339 end of the vacation period.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub to_date: Option<String>,
94    /// Subject of the auto-reply message.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub subject: Option<String>,
97    /// Plaintext body of the auto-reply message.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub text_body: Option<String>,
100    /// HTML body of the auto-reply message.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub html_body: Option<String>,
103}
104
105/// Failure causes during a JMAP `VacationResponse/set` flow.
106#[derive(Debug, Error)]
107pub enum JmapVacationResponseSetError {
108    /// The response carried no method response.
109    #[error("JMAP VacationResponse/set failed: missing response in method_responses")]
110    MissingResponse,
111    /// The inner send coroutine failed.
112    #[error("JMAP VacationResponse/set failed: {0}")]
113    Send(#[from] JmapSendError),
114    /// The method arguments could not be serialized.
115    #[error("JMAP VacationResponse/set failed: serialize args: {0}")]
116    SerializeArgs(#[source] serde_json::Error),
117    /// The method response could not be parsed.
118    #[error("JMAP VacationResponse/set failed: parse response: {0}")]
119    ParseResponse(#[source] serde_json::Error),
120    /// The server returned a method-level error.
121    #[error("JMAP VacationResponse/set failed: {0}")]
122    Method(#[from] JmapMethodError),
123}
124
125/// Successful terminal output of [`JmapVacationResponseSet`].
126#[derive(Clone, Debug)]
127pub struct JmapVacationResponseSetOutput {
128    /// The new server state after the call.
129    pub new_state: String,
130    /// The updated singleton, when the server echoed it back.
131    pub updated: Option<JmapVacationResponse>,
132    /// Whether the server indicated the connection can be reused.
133    pub keep_alive: bool,
134}
135
136/// I/O-free coroutine for the JMAP `VacationResponse/set` method.
137pub struct JmapVacationResponseSet {
138    state: State,
139}
140
141impl JmapVacationResponseSet {
142    /// Prepares the method call request and builds the coroutine.
143    pub fn new(
144        session: &JmapSession,
145        http_auth: &SecretString,
146        patch: JmapVacationResponseUpdate,
147    ) -> Result<Self, JmapVacationResponseSetError> {
148        let account_id = session
149            .primary_accounts
150            .get(JMAP_VACATION_RESPONSE_CAPABILITY)
151            .or_else(|| session.primary_accounts.get(JMAP_MAIL_CAPABILITY))
152            .cloned()
153            .unwrap_or_default();
154        let api_url = &session.api_url;
155
156        let args = serde_json::to_value(VacationResponseSetArgs {
157            account_id,
158            update: BTreeMap::from([("singleton", patch)]),
159        })
160        .map_err(JmapVacationResponseSetError::SerializeArgs)?;
161
162        let mut using = vec![JMAP_CORE_CAPABILITY.into(), JMAP_MAIL_CAPABILITY.into()];
163        if session
164            .capabilities
165            .contains_key(JMAP_VACATION_RESPONSE_CAPABILITY)
166        {
167            using.push(JMAP_VACATION_RESPONSE_CAPABILITY.into());
168        }
169
170        let mut batch = JmapBatch::new();
171        batch.add("VacationResponse/set", args);
172        let request = batch.into_request(using);
173
174        Ok(Self {
175            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
176        })
177    }
178}
179
180impl JmapCoroutine for JmapVacationResponseSet {
181    type Yield = JmapYield;
182    type Return = Result<JmapVacationResponseSetOutput, JmapVacationResponseSetError>;
183
184    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
185        match &mut self.state {
186            State::Send(send) => {
187                let JmapSendOutput {
188                    response,
189                    keep_alive,
190                } = jmap_try!(send, arg);
191
192                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
193                    return JmapCoroutineState::Complete(Err(
194                        JmapVacationResponseSetError::MissingResponse,
195                    ));
196                };
197
198                if name == "error" {
199                    let err = serde_json::from_value::<JmapMethodError>(args)
200                        .unwrap_or(JmapMethodError::Unknown);
201                    return JmapCoroutineState::Complete(Err(err.into()));
202                }
203
204                match serde_json::from_value::<VacationResponseSetResponse>(args) {
205                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapVacationResponseSetOutput {
206                        new_state: r.new_state,
207                        updated: r.updated.unwrap_or_default().into_values().flatten().next(),
208                        keep_alive,
209                    })),
210                    Err(err) => JmapCoroutineState::Complete(Err(
211                        JmapVacationResponseSetError::ParseResponse(err),
212                    )),
213                }
214            }
215        }
216    }
217}
218
219enum State {
220    Send(JmapSend),
221}
222
223#[derive(Serialize)]
224#[serde(rename_all = "camelCase")]
225struct VacationResponseSetArgs {
226    account_id: String,
227    update: BTreeMap<&'static str, JmapVacationResponseUpdate>,
228}
229
230#[derive(Deserialize)]
231#[serde(rename_all = "camelCase")]
232struct VacationResponseSetResponse {
233    new_state: String,
234    #[serde(default)]
235    updated: Option<BTreeMap<String, Option<JmapVacationResponse>>>,
236}