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