1use io_http::{
58 coroutine::*,
59 rfc9110::{
60 request::HttpRequest,
61 send::{HttpSendOutput, HttpSendYield},
62 },
63 rfc9112::send::{Http11Send, Http11SendError},
64};
65use log::{debug, trace};
66use secrecy::{ExposeSecret, SecretString};
67use thiserror::Error;
68use url::Url;
69
70use crate::{
71 coroutine::*,
72 rfc8620::request::{JmapRequest, JmapResponse},
73};
74
75#[derive(Debug, Error)]
77pub enum JmapSendError {
78 #[error("JMAP send failed: HTTP {0}")]
80 HttpStatus(u16),
81 #[error("JMAP send failed: unexpected redirect")]
83 UnexpectedRedirect,
84 #[error("JMAP send failed: {0}")]
86 Send(#[from] Http11SendError),
87 #[error("JMAP send failed: serialize request: {0}")]
89 SerializeRequest(#[source] serde_json::Error),
90 #[error("JMAP send failed: parse response: {0}")]
92 ParseResponse(#[source] serde_json::Error),
93}
94
95#[derive(Clone, Debug)]
97pub struct JmapSendOutput {
98 pub response: JmapResponse,
100 pub keep_alive: bool,
102}
103
104pub struct JmapSend {
106 state: State,
107}
108
109impl JmapSend {
110 pub fn new(
113 http_auth: &SecretString,
114 api_url: &Url,
115 request: JmapRequest,
116 ) -> Result<Self, JmapSendError> {
117 let body = serde_json::to_vec(&request).map_err(JmapSendError::SerializeRequest)?;
118
119 let host = api_url.host_str().unwrap_or("localhost");
120
121 let mut http_request = HttpRequest::get(api_url.clone())
122 .header("Host", host)
123 .header("Content-Type", "application/json")
124 .header("Accept", "application/json")
125 .header("Authorization", http_auth.expose_secret())
126 .body(body);
127
128 http_request.method = "POST".into();
129
130 debug!("prepare request to send");
131 trace!("api url: {api_url}");
132
133 Ok(Self {
134 state: State::Send(Http11Send::new(http_request)),
135 })
136 }
137}
138
139impl JmapCoroutine for JmapSend {
140 type Yield = JmapYield;
141 type Return = Result<JmapSendOutput, JmapSendError>;
142
143 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
144 match &mut self.state {
145 State::Send(send) => match send.resume(arg) {
146 HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
147 JmapCoroutineState::Yielded(JmapYield::WantsRead)
148 }
149 HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
150 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes))
151 }
152 HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect { .. }) => {
153 JmapCoroutineState::Complete(Err(JmapSendError::UnexpectedRedirect))
154 }
155 HttpCoroutineState::Complete(Err(err)) => {
156 JmapCoroutineState::Complete(Err(err.into()))
157 }
158 HttpCoroutineState::Complete(Ok(HttpSendOutput {
159 response,
160 keep_alive,
161 ..
162 })) => {
163 if !response.status.is_success() {
164 let err = JmapSendError::HttpStatus(*response.status);
165 return JmapCoroutineState::Complete(Err(err));
166 }
167
168 match serde_json::from_slice::<JmapResponse>(&response.body) {
169 Ok(response) => JmapCoroutineState::Complete(Ok(JmapSendOutput {
170 response,
171 keep_alive,
172 })),
173 Err(err) => {
174 JmapCoroutineState::Complete(Err(JmapSendError::ParseResponse(err)))
175 }
176 }
177 }
178 },
179 }
180 }
181}
182
183enum State {
184 Send(Http11Send),
185}
186
187#[cfg(test)]
188mod tests {
189 use alloc::{format, string::ToString, vec, vec::Vec};
190
191 use crate::rfc8620::request::JmapBatch;
192 use crate::rfc8620::send::*;
193
194 fn make_auth() -> SecretString {
195 SecretString::from("Bearer test")
196 }
197
198 fn make_url() -> Url {
199 "https://api.example.com/jmap/".parse().unwrap()
200 }
201
202 fn make_request() -> JmapRequest {
203 let mut batch = JmapBatch::new();
204 batch.add("Mailbox/get", serde_json::json!({ "accountId": "a1" }));
205 batch.into_request(vec!["urn:ietf:params:jmap:core".to_string()])
206 }
207
208 fn make_response_body() -> Vec<u8> {
209 br#"{
210 "methodResponses": [["Mailbox/get", {"list":[],"notFound":[],"state":"s1"}, "c0"]],
211 "sessionState": "s1"
212 }"#
213 .to_vec()
214 }
215
216 fn build_http_reply(status: u16, body: &[u8]) -> Vec<u8> {
217 let head = format!(
218 "HTTP/1.1 {} OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n",
219 status,
220 body.len()
221 );
222 let mut bytes = head.into_bytes();
223 bytes.extend_from_slice(body);
224 bytes
225 }
226
227 #[test]
228 fn success_returns_ok() {
229 let mut cor = JmapSend::new(&make_auth(), &make_url(), make_request()).unwrap();
230
231 expect_wants_write(&mut cor, None);
232 expect_wants_read(&mut cor);
233
234 let reply = build_http_reply(200, &make_response_body());
235 let out = expect_complete_ok(&mut cor, &reply);
236 assert_eq!(out.response.method_responses.len(), 1);
237 assert_eq!(out.response.session_state, "s1");
238 }
239
240 #[test]
241 fn http_error_returns_status() {
242 let mut cor = JmapSend::new(&make_auth(), &make_url(), make_request()).unwrap();
243
244 expect_wants_write(&mut cor, None);
245 expect_wants_read(&mut cor);
246
247 let reply = b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n";
248 let err = expect_complete_err(&mut cor, reply);
249 assert!(matches!(err, JmapSendError::HttpStatus(401)));
250 }
251
252 #[test]
253 fn redirect_returns_unexpected_redirect() {
254 let mut cor = JmapSend::new(&make_auth(), &make_url(), make_request()).unwrap();
255
256 expect_wants_write(&mut cor, None);
257 expect_wants_read(&mut cor);
258
259 let reply = b"HTTP/1.1 301 Moved\r\nLocation: https://other.example.com/jmap/\r\nContent-Length: 0\r\n\r\n";
260 let err = expect_complete_err(&mut cor, reply);
261 assert!(matches!(err, JmapSendError::UnexpectedRedirect));
262 }
263
264 #[test]
265 fn invalid_json_returns_parse_error() {
266 let mut cor = JmapSend::new(&make_auth(), &make_url(), make_request()).unwrap();
267
268 expect_wants_write(&mut cor, None);
269 expect_wants_read(&mut cor);
270
271 let reply = build_http_reply(200, b"{not json");
272 let err = expect_complete_err(&mut cor, &reply);
273 assert!(matches!(err, JmapSendError::ParseResponse(_)));
274 }
275
276 #[test]
277 fn batch_assigns_sequential_ids() {
278 let mut batch = JmapBatch::new();
279 let a = batch.add("A", serde_json::json!({}));
280 let b = batch.add("B", serde_json::json!({}));
281 assert_eq!(a, "c0");
282 assert_eq!(b, "c1");
283 }
284
285 fn expect_wants_write(cor: &mut JmapSend, arg: Option<&[u8]>) -> Vec<u8> {
286 match cor.resume(arg) {
287 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => bytes,
288 state => panic!("expected WantsWrite, got {state:?}"),
289 }
290 }
291
292 fn expect_wants_read(cor: &mut JmapSend) {
293 match cor.resume(None) {
294 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {}
295 state => panic!("expected WantsRead, got {state:?}"),
296 }
297 }
298
299 fn expect_complete_ok(cor: &mut JmapSend, reply: &[u8]) -> JmapSendOutput {
300 match cor.resume(Some(reply)) {
301 JmapCoroutineState::Complete(Ok(out)) => out,
302 state => panic!("expected Complete(Ok), got {state:?}"),
303 }
304 }
305
306 fn expect_complete_err(cor: &mut JmapSend, reply: &[u8]) -> JmapSendError {
307 match cor.resume(Some(reply)) {
308 JmapCoroutineState::Complete(Err(err)) => err,
309 state => panic!("expected Complete(Err), got {state:?}"),
310 }
311 }
312}