Skip to main content

io_jmap/rfc8621/email_submission/
cancel.rs

1//! JMAP `EmailSubmission/set` cancel coroutine (RFC 8621 ยง7.5): patches
2//! `undoStatus: "canceled"` on each pending submission id.  Submissions not in
3//! `pending` state surface in `notUpdated`.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//!     io::{Read, Write},
10//!     net::TcpStream,
11//! };
12//!
13//! use io_jmap::{
14//!     coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
15//!     rfc8620::JmapSession,
16//!     rfc8621::email_submission::cancel::JmapEmailSubmissionCancel,
17//! };
18//! use secrecy::SecretString;
19//!
20//! // Ready stream needed (TCP-connected, TLS-negociated)
21//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
22//! let mut buf = [0u8; 4096];
23//!
24//! let session: JmapSession = serde_json::from_str(r#"{
25//!     "username": "",
26//!     "accounts": {},
27//!     "primaryAccounts": {"urn:ietf:params:jmap:mail": "a1"},
28//!     "capabilities": {},
29//!     "apiUrl": "https://api.example.com/jmap/",
30//!     "downloadUrl": "",
31//!     "uploadUrl": "",
32//!     "eventSourceUrl": "",
33//!     "state": ""
34//! }"#).unwrap();
35//! let auth = SecretString::from("Bearer xyz");
36//! let mut coroutine =
37//!     JmapEmailSubmissionCancel::new(&session, &auth, vec!["s1".into()]).unwrap();
38//! let mut arg = None;
39//!
40//! let out = loop {
41//!     match coroutine.resume(arg.take()) {
42//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
43//!             stream.write_all(&bytes).unwrap();
44//!         }
45//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
46//!             let n = stream.read(&mut buf).unwrap();
47//!             arg = Some(&buf[..n]);
48//!         }
49//!         JmapCoroutineState::Complete(Ok(out)) => break out,
50//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
51//!     }
52//! };
53//!
54//! println!("new state {}", out.new_state);
55//! ```
56
57use core::fmt;
58
59use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
60
61use log::trace;
62use secrecy::SecretString;
63use serde::{Deserialize, Serialize};
64use thiserror::Error;
65
66use crate::{
67    coroutine::*,
68    jmap_try,
69    rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
70    rfc8621::{
71        MAIL_CAPABILITY,
72        email_submission::{
73            JmapEmailSubmission, JmapEmailSubmissionSetItemError, JmapEmailSubmissionUpdate,
74            JmapUndoStatus, SUBMISSION_CAPABILITY,
75        },
76    },
77};
78
79/// Failure causes during a JMAP `EmailSubmission/set` cancel flow.
80#[derive(Debug, Error)]
81pub enum JmapEmailSubmissionCancelError {
82    #[error("JMAP EmailSubmission/set (cancel) failed: missing response in method_responses")]
83    MissingResponse,
84    #[error("JMAP EmailSubmission/set (cancel) failed: {0}")]
85    Send(#[from] JmapSendError),
86    #[error("JMAP EmailSubmission/set (cancel) failed: serialize args: {0}")]
87    SerializeArgs(#[source] serde_json::Error),
88    #[error("JMAP EmailSubmission/set (cancel) failed: parse response: {0}")]
89    ParseResponse(#[source] serde_json::Error),
90    #[error("JMAP EmailSubmission/set (cancel) failed: {0}")]
91    Method(#[from] JmapMethodError),
92}
93
94/// Successful terminal output of [`JmapEmailSubmissionCancel`].
95#[derive(Clone, Debug)]
96pub struct JmapEmailSubmissionCancelOutput {
97    pub new_state: String,
98    pub updated: BTreeMap<String, Option<JmapEmailSubmission>>,
99    pub not_updated: BTreeMap<String, JmapEmailSubmissionSetItemError>,
100    pub keep_alive: bool,
101}
102
103/// I/O-free coroutine for canceling pending JMAP email submissions.
104pub struct JmapEmailSubmissionCancel {
105    state: State,
106}
107
108impl JmapEmailSubmissionCancel {
109    /// `ids` is the list of submission IDs to cancel.
110    pub fn new(
111        session: &JmapSession,
112        http_auth: &SecretString,
113        ids: Vec<String>,
114    ) -> Result<Self, JmapEmailSubmissionCancelError> {
115        let account_id = session
116            .primary_accounts
117            .get(MAIL_CAPABILITY)
118            .cloned()
119            .unwrap_or_default();
120        let api_url = &session.api_url;
121
122        let update = ids
123            .into_iter()
124            .map(|id| {
125                (
126                    id,
127                    JmapEmailSubmissionUpdate {
128                        undo_status: Some(JmapUndoStatus::Canceled),
129                    },
130                )
131            })
132            .collect();
133
134        let args = serde_json::to_value(CancelEmailSubmissionsArgs { account_id, update })
135            .map_err(JmapEmailSubmissionCancelError::SerializeArgs)?;
136
137        let mut batch = JmapBatch::new();
138        batch.add("EmailSubmission/set", args);
139        let request = batch.into_request(vec![
140            CORE_CAPABILITY.into(),
141            MAIL_CAPABILITY.into(),
142            SUBMISSION_CAPABILITY.into(),
143        ]);
144
145        Ok(Self {
146            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
147        })
148    }
149}
150
151impl JmapCoroutine for JmapEmailSubmissionCancel {
152    type Yield = JmapYield;
153    type Return = Result<JmapEmailSubmissionCancelOutput, JmapEmailSubmissionCancelError>;
154
155    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
156        trace!("JmapEmailSubmission/cancel: {}", self.state);
157        match &mut self.state {
158            State::Send(send) => {
159                let JmapSendOutput {
160                    response,
161                    keep_alive,
162                } = jmap_try!(send, arg);
163
164                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
165                    return JmapCoroutineState::Complete(Err(
166                        JmapEmailSubmissionCancelError::MissingResponse,
167                    ));
168                };
169
170                if name == "error" {
171                    let err = serde_json::from_value::<JmapMethodError>(args)
172                        .unwrap_or(JmapMethodError::Unknown);
173                    return JmapCoroutineState::Complete(Err(err.into()));
174                }
175
176                match serde_json::from_value::<EmailSubmissionCancelResponse>(args) {
177                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailSubmissionCancelOutput {
178                        new_state: r.new_state,
179                        updated: r.updated.unwrap_or_default(),
180                        not_updated: r.not_updated.unwrap_or_default(),
181                        keep_alive,
182                    })),
183                    Err(err) => JmapCoroutineState::Complete(Err(
184                        JmapEmailSubmissionCancelError::ParseResponse(err),
185                    )),
186                }
187            }
188        }
189    }
190}
191
192enum State {
193    Send(JmapSend),
194}
195
196impl fmt::Display for State {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        match self {
199            Self::Send(_) => f.write_str("send"),
200        }
201    }
202}
203
204#[derive(Serialize)]
205#[serde(rename_all = "camelCase")]
206struct CancelEmailSubmissionsArgs {
207    account_id: String,
208    update: BTreeMap<String, JmapEmailSubmissionUpdate>,
209}
210
211#[derive(Deserialize)]
212#[serde(rename_all = "camelCase")]
213struct EmailSubmissionCancelResponse {
214    new_state: String,
215    #[serde(default)]
216    updated: Option<BTreeMap<String, Option<JmapEmailSubmission>>>,
217    #[serde(default)]
218    not_updated: Option<BTreeMap<String, JmapEmailSubmissionSetItemError>>,
219}