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::JmapSession,
15//!     rfc8621::vacation_response::{JmapVacationResponseUpdate, set::JmapVacationResponseSet},
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 core::fmt;
61
62use alloc::{collections::BTreeMap, string::String, vec};
63
64use log::trace;
65use secrecy::SecretString;
66use serde::{Deserialize, Serialize};
67use thiserror::Error;
68
69use crate::{
70    coroutine::*,
71    jmap_try,
72    rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
73    rfc8621::{
74        MAIL_CAPABILITY,
75        vacation_response::{
76            JmapVacationResponse, JmapVacationResponseUpdate, VACATION_RESPONSE_CAPABILITY,
77        },
78    },
79};
80
81/// Failure causes during a JMAP `VacationResponse/set` flow.
82#[derive(Debug, Error)]
83pub enum JmapVacationResponseSetError {
84    #[error("JMAP VacationResponse/set failed: missing response in method_responses")]
85    MissingResponse,
86    #[error("JMAP VacationResponse/set failed: {0}")]
87    Send(#[from] JmapSendError),
88    #[error("JMAP VacationResponse/set failed: serialize args: {0}")]
89    SerializeArgs(#[source] serde_json::Error),
90    #[error("JMAP VacationResponse/set failed: parse response: {0}")]
91    ParseResponse(#[source] serde_json::Error),
92    #[error("JMAP VacationResponse/set failed: {0}")]
93    Method(#[from] JmapMethodError),
94}
95
96/// Successful terminal output of [`JmapVacationResponseSet`].
97#[derive(Clone, Debug)]
98pub struct JmapVacationResponseSetOutput {
99    pub new_state: String,
100    pub updated: Option<JmapVacationResponse>,
101    pub keep_alive: bool,
102}
103
104/// I/O-free coroutine for the JMAP `VacationResponse/set` method.
105pub struct JmapVacationResponseSet {
106    state: State,
107}
108
109impl JmapVacationResponseSet {
110    pub fn new(
111        session: &JmapSession,
112        http_auth: &SecretString,
113        patch: JmapVacationResponseUpdate,
114    ) -> Result<Self, JmapVacationResponseSetError> {
115        let account_id = session
116            .primary_accounts
117            .get(VACATION_RESPONSE_CAPABILITY)
118            .or_else(|| session.primary_accounts.get(MAIL_CAPABILITY))
119            .cloned()
120            .unwrap_or_default();
121        let api_url = &session.api_url;
122
123        let args = serde_json::to_value(VacationResponseSetArgs {
124            account_id,
125            update: BTreeMap::from([("singleton", patch)]),
126        })
127        .map_err(JmapVacationResponseSetError::SerializeArgs)?;
128
129        let mut using = vec![CORE_CAPABILITY.into(), MAIL_CAPABILITY.into()];
130        if session
131            .capabilities
132            .contains_key(VACATION_RESPONSE_CAPABILITY)
133        {
134            using.push(VACATION_RESPONSE_CAPABILITY.into());
135        }
136
137        let mut batch = JmapBatch::new();
138        batch.add("VacationResponse/set", args);
139        let request = batch.into_request(using);
140
141        Ok(Self {
142            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
143        })
144    }
145}
146
147impl JmapCoroutine for JmapVacationResponseSet {
148    type Yield = JmapYield;
149    type Return = Result<JmapVacationResponseSetOutput, JmapVacationResponseSetError>;
150
151    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
152        trace!("VacationResponse/set: {}", self.state);
153        match &mut self.state {
154            State::Send(send) => {
155                let JmapSendOutput {
156                    response,
157                    keep_alive,
158                } = jmap_try!(send, arg);
159
160                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
161                    return JmapCoroutineState::Complete(Err(
162                        JmapVacationResponseSetError::MissingResponse,
163                    ));
164                };
165
166                if name == "error" {
167                    let err = serde_json::from_value::<JmapMethodError>(args)
168                        .unwrap_or(JmapMethodError::Unknown);
169                    return JmapCoroutineState::Complete(Err(err.into()));
170                }
171
172                match serde_json::from_value::<VacationResponseSetResponse>(args) {
173                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapVacationResponseSetOutput {
174                        new_state: r.new_state,
175                        updated: r.updated.unwrap_or_default().into_values().flatten().next(),
176                        keep_alive,
177                    })),
178                    Err(err) => JmapCoroutineState::Complete(Err(
179                        JmapVacationResponseSetError::ParseResponse(err),
180                    )),
181                }
182            }
183        }
184    }
185}
186
187enum State {
188    Send(JmapSend),
189}
190
191impl fmt::Display for State {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        match self {
194            Self::Send(_) => f.write_str("send"),
195        }
196    }
197}
198
199#[derive(Serialize)]
200#[serde(rename_all = "camelCase")]
201struct VacationResponseSetArgs {
202    account_id: String,
203    update: BTreeMap<&'static str, JmapVacationResponseUpdate>,
204}
205
206#[derive(Deserialize)]
207#[serde(rename_all = "camelCase")]
208struct VacationResponseSetResponse {
209    new_state: String,
210    #[serde(default)]
211    updated: Option<BTreeMap<String, Option<JmapVacationResponse>>>,
212}