Skip to main content

io_jmap/rfc8621/email_submission/
set.rs

1//! JMAP `EmailSubmission/set` coroutine (RFC 8621 ยง7.5): submits emails for
2//! sending. JMAP equivalent of SMTP message submission.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//!     collections::BTreeMap,
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::{JmapEmailSubmissionCreate, set::JmapEmailSubmissionSet},
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 submissions = BTreeMap::new();
37//! submissions.insert(
38//!     "c1".to_string(),
39//!     JmapEmailSubmissionCreate {
40//!         identity_id: "id1".into(),
41//!         email_id: "e1".into(),
42//!         envelope: None,
43//!     },
44//! );
45//! let mut coroutine = JmapEmailSubmissionSet::new(&session, &auth, submissions).unwrap();
46//! let mut arg = None;
47//!
48//! let out = loop {
49//!     match coroutine.resume(arg.take()) {
50//!         JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
51//!             stream.write_all(&bytes).unwrap();
52//!         }
53//!         JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
54//!             let n = stream.read(&mut buf).unwrap();
55//!             arg = Some(&buf[..n]);
56//!         }
57//!         JmapCoroutineState::Complete(Ok(out)) => break out,
58//!         JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
59//!     }
60//! };
61//!
62//! println!("{} created", out.created.len());
63//! ```
64
65use core::fmt;
66
67use alloc::{collections::BTreeMap, string::String, vec};
68
69use log::trace;
70use secrecy::SecretString;
71use serde::{Deserialize, Serialize};
72use thiserror::Error;
73
74use crate::{
75    coroutine::*,
76    jmap_try,
77    rfc8620::{CORE_CAPABILITY, JmapBatch, JmapMethodError, JmapSession, send::*},
78    rfc8621::{
79        MAIL_CAPABILITY,
80        email_submission::{
81            JmapEmailSubmission, JmapEmailSubmissionCreate, JmapEmailSubmissionSetItemError,
82            SUBMISSION_CAPABILITY,
83        },
84    },
85};
86
87/// Failure causes during a JMAP `EmailSubmission/set` flow.
88#[derive(Debug, Error)]
89pub enum JmapEmailSubmissionSetError {
90    #[error("JMAP EmailSubmission/set failed: missing response in method_responses")]
91    MissingResponse,
92    #[error("JMAP EmailSubmission/set failed: {0}")]
93    Send(#[from] JmapSendError),
94    #[error("JMAP EmailSubmission/set failed: serialize args: {0}")]
95    SerializeArgs(#[source] serde_json::Error),
96    #[error("JMAP EmailSubmission/set failed: parse response: {0}")]
97    ParseResponse(#[source] serde_json::Error),
98    #[error("JMAP EmailSubmission/set failed: {0}")]
99    Method(#[from] JmapMethodError),
100}
101
102/// Successful terminal output of [`JmapEmailSubmissionSet`].
103#[derive(Clone, Debug)]
104pub struct JmapEmailSubmissionSetOutput {
105    pub new_state: String,
106    pub created: BTreeMap<String, JmapEmailSubmission>,
107    pub not_created: BTreeMap<String, JmapEmailSubmissionSetItemError>,
108    pub keep_alive: bool,
109}
110
111/// I/O-free coroutine for the JMAP `EmailSubmission/set` method.
112pub struct JmapEmailSubmissionSet {
113    state: State,
114}
115
116impl JmapEmailSubmissionSet {
117    /// `submissions` maps client-assigned IDs to [`JmapEmailSubmissionCreate`].
118    pub fn new(
119        session: &JmapSession,
120        http_auth: &SecretString,
121        submissions: BTreeMap<String, JmapEmailSubmissionCreate>,
122    ) -> Result<Self, JmapEmailSubmissionSetError> {
123        let account_id = session
124            .primary_accounts
125            .get(MAIL_CAPABILITY)
126            .cloned()
127            .unwrap_or_default();
128        let api_url = &session.api_url;
129
130        let args = serde_json::to_value(EmailSubmissionSetArgs {
131            account_id,
132            create: submissions,
133        })
134        .map_err(JmapEmailSubmissionSetError::SerializeArgs)?;
135
136        let mut batch = JmapBatch::new();
137        batch.add("EmailSubmission/set", args);
138        let request = batch.into_request(vec![
139            CORE_CAPABILITY.into(),
140            MAIL_CAPABILITY.into(),
141            SUBMISSION_CAPABILITY.into(),
142        ]);
143
144        Ok(Self {
145            state: State::Send(JmapSend::new(http_auth, api_url, request)?),
146        })
147    }
148}
149
150impl JmapCoroutine for JmapEmailSubmissionSet {
151    type Yield = JmapYield;
152    type Return = Result<JmapEmailSubmissionSetOutput, JmapEmailSubmissionSetError>;
153
154    fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
155        trace!("EmailSubmission/set: {}", self.state);
156        match &mut self.state {
157            State::Send(send) => {
158                let JmapSendOutput {
159                    response,
160                    keep_alive,
161                } = jmap_try!(send, arg);
162
163                let Some((name, args, _)) = response.method_responses.into_iter().next() else {
164                    return JmapCoroutineState::Complete(Err(
165                        JmapEmailSubmissionSetError::MissingResponse,
166                    ));
167                };
168
169                if name == "error" {
170                    let err = serde_json::from_value::<JmapMethodError>(args)
171                        .unwrap_or(JmapMethodError::Unknown);
172                    return JmapCoroutineState::Complete(Err(err.into()));
173                }
174
175                match serde_json::from_value::<EmailSubmissionSetResponse>(args) {
176                    Ok(r) => JmapCoroutineState::Complete(Ok(JmapEmailSubmissionSetOutput {
177                        new_state: r.new_state,
178                        created: r.created.unwrap_or_default(),
179                        not_created: r.not_created.unwrap_or_default(),
180                        keep_alive,
181                    })),
182                    Err(err) => JmapCoroutineState::Complete(Err(
183                        JmapEmailSubmissionSetError::ParseResponse(err),
184                    )),
185                }
186            }
187        }
188    }
189}
190
191enum State {
192    Send(JmapSend),
193}
194
195impl fmt::Display for State {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match self {
198            Self::Send(_) => f.write_str("send"),
199        }
200    }
201}
202
203#[derive(Serialize)]
204#[serde(rename_all = "camelCase")]
205struct EmailSubmissionSetArgs {
206    account_id: String,
207    create: BTreeMap<String, JmapEmailSubmissionCreate>,
208}
209
210#[derive(Deserialize)]
211#[serde(rename_all = "camelCase")]
212struct EmailSubmissionSetResponse {
213    new_state: String,
214    #[serde(default)]
215    created: Option<BTreeMap<String, JmapEmailSubmission>>,
216    #[serde(default)]
217    not_created: Option<BTreeMap<String, JmapEmailSubmissionSetItemError>>,
218}