1use core::fmt;
27
28use alloc::{string::String, vec::Vec};
29
30use log::trace;
31use secrecy::SecretString;
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34use url::Url;
35
36use crate::{
37 coroutine::*,
38 jmap_try,
39 rfc8620::{JmapBatch, JmapMethodError, send::*},
40};
41
42#[derive(Debug, Error)]
44pub enum JmapQueryError {
45 #[error("JMAP Foo/query failed: missing response in method_responses")]
46 MissingResponse,
47 #[error("JMAP Foo/query failed: {0}")]
48 Send(#[from] JmapSendError),
49 #[error("JMAP Foo/query failed: serialize args: {0}")]
50 SerializeArgs(#[source] serde_json::Error),
51 #[error("JMAP Foo/query failed: parse response: {0}")]
52 ParseResponse(#[source] serde_json::Error),
53 #[error("JMAP Foo/query failed: {0}")]
54 Method(#[from] JmapMethodError),
55}
56
57#[derive(Clone, Debug)]
59pub struct JmapQueryOptions<F: Serialize, S: Serialize> {
60 pub filter: Option<F>,
61 pub sort: Option<Vec<S>>,
62 pub position: Option<u64>,
63 pub anchor: Option<String>,
64 pub anchor_offset: Option<i64>,
65 pub limit: Option<u64>,
66 pub calculate_total: bool,
68}
69
70impl<F: Serialize, S: Serialize> Default for JmapQueryOptions<F, S> {
71 fn default() -> Self {
72 Self {
73 filter: None,
74 sort: None,
75 position: None,
76 anchor: None,
77 anchor_offset: None,
78 limit: None,
79 calculate_total: false,
80 }
81 }
82}
83
84#[derive(Clone, Debug)]
86pub struct JmapQueryOutput {
87 pub query_state: String,
88 pub can_calculate_changes: bool,
89 pub position: u64,
90 pub ids: Vec<String>,
91 pub total: Option<u64>,
92 pub limit: Option<u64>,
93 pub keep_alive: bool,
94}
95
96pub struct JmapQuery {
98 state: State,
99}
100
101impl JmapQuery {
102 pub fn new<F: Serialize, S: Serialize>(
104 account_id: String,
105 http_auth: &SecretString,
106 api_url: &Url,
107 method: impl Into<String>,
108 capabilities: Vec<String>,
109 opts: JmapQueryOptions<F, S>,
110 ) -> Result<Self, JmapQueryError> {
111 let args = serde_json::to_value(QueryArgs {
112 account_id: &account_id,
113 filter: opts.filter,
114 sort: opts.sort,
115 position: opts.position,
116 anchor: opts.anchor,
117 anchor_offset: opts.anchor_offset,
118 limit: opts.limit,
119 calculate_total: opts.calculate_total,
120 })
121 .map_err(JmapQueryError::SerializeArgs)?;
122
123 let mut batch = JmapBatch::new();
124 batch.add(method, args);
125 let request = batch.into_request(capabilities);
126
127 Ok(Self {
128 state: State::Send(JmapSend::new(http_auth, api_url, request)?),
129 })
130 }
131
132 pub fn from_send(send: JmapSend) -> Self {
134 Self {
135 state: State::Send(send),
136 }
137 }
138}
139
140impl JmapCoroutine for JmapQuery {
141 type Yield = JmapYield;
142 type Return = Result<JmapQueryOutput, JmapQueryError>;
143
144 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
145 trace!("query: {}", self.state);
146 match &mut self.state {
147 State::Send(send) => {
148 let JmapSendOutput {
149 response,
150 keep_alive,
151 } = jmap_try!(send, arg);
152
153 let Some((name, args, _)) = response.method_responses.into_iter().next() else {
154 return JmapCoroutineState::Complete(Err(JmapQueryError::MissingResponse));
155 };
156
157 if name == "error" {
158 let err = serde_json::from_value::<JmapMethodError>(args)
159 .unwrap_or(JmapMethodError::Unknown);
160 return JmapCoroutineState::Complete(Err(err.into()));
161 }
162
163 match serde_json::from_value::<QueryResponse>(args) {
164 Ok(r) => JmapCoroutineState::Complete(Ok(JmapQueryOutput {
165 query_state: r.query_state,
166 can_calculate_changes: r.can_calculate_changes,
167 position: r.position,
168 ids: r.ids,
169 total: r.total,
170 limit: r.limit,
171 keep_alive,
172 })),
173 Err(err) => {
174 JmapCoroutineState::Complete(Err(JmapQueryError::ParseResponse(err)))
175 }
176 }
177 }
178 }
179 }
180}
181
182enum State {
183 Send(JmapSend),
184}
185
186impl fmt::Display for State {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::Send(_) => f.write_str("send"),
190 }
191 }
192}
193
194#[derive(Serialize)]
195#[serde(rename_all = "camelCase")]
196struct QueryArgs<'a, F: Serialize, S: Serialize> {
197 account_id: &'a str,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 filter: Option<F>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 sort: Option<Vec<S>>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 position: Option<u64>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 anchor: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 anchor_offset: Option<i64>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 limit: Option<u64>,
210 calculate_total: bool,
211}
212
213#[derive(Deserialize)]
214#[serde(rename_all = "camelCase")]
215struct QueryResponse {
216 query_state: String,
217 #[serde(default)]
218 can_calculate_changes: bool,
219 #[serde(default)]
220 position: u64,
221 ids: Vec<String>,
222 #[serde(default)]
223 total: Option<u64>,
224 #[serde(default)]
225 limit: Option<u64>,
226}
227
228#[cfg(test)]
229mod tests {
230 use alloc::{format, string::ToString, vec};
231
232 use super::*;
233
234 fn make_auth() -> SecretString {
235 SecretString::from("Bearer test")
236 }
237
238 fn make_url() -> Url {
239 "https://api.example.com/jmap/".parse().unwrap()
240 }
241
242 fn make_query() -> JmapQuery {
243 JmapQuery::new::<serde_json::Value, serde_json::Value>(
244 "a1".to_string(),
245 &make_auth(),
246 &make_url(),
247 "Email/query",
248 vec!["urn:ietf:params:jmap:mail".to_string()],
249 JmapQueryOptions {
250 limit: Some(10),
251 ..Default::default()
252 },
253 )
254 .unwrap()
255 }
256
257 fn build_http_reply(body: &[u8]) -> Vec<u8> {
258 let head = format!(
259 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n",
260 body.len()
261 );
262 let mut bytes = head.into_bytes();
263 bytes.extend_from_slice(body);
264 bytes
265 }
266
267 #[test]
268 fn success_returns_ok() {
269 let mut cor = make_query();
270 expect_wants_write(&mut cor, None);
271 expect_wants_read(&mut cor);
272
273 let body = br#"{
274 "methodResponses": [["Email/query", {"queryState":"qs","position":0,"ids":["e1","e2"]}, "c0"]],
275 "sessionState": "s1"
276 }"#;
277 let reply = build_http_reply(body);
278 let out = expect_complete_ok(&mut cor, &reply);
279 assert_eq!(out.ids, vec!["e1".to_string(), "e2".to_string()]);
280 }
281
282 #[test]
283 fn method_error_returns_method_error() {
284 let mut cor = make_query();
285 expect_wants_write(&mut cor, None);
286 expect_wants_read(&mut cor);
287
288 let body = br#"{
289 "methodResponses": [["error", {"type":"invalidArguments"}, "c0"]],
290 "sessionState": "s1"
291 }"#;
292 let reply = build_http_reply(body);
293 let err = expect_complete_err(&mut cor, &reply);
294 assert!(matches!(err, JmapQueryError::Method(_)));
295 }
296
297 #[test]
298 fn missing_response_returns_missing_response() {
299 let mut cor = make_query();
300 expect_wants_write(&mut cor, None);
301 expect_wants_read(&mut cor);
302
303 let reply = build_http_reply(br#"{"methodResponses":[], "sessionState":"s"}"#);
304 let err = expect_complete_err(&mut cor, &reply);
305 assert!(matches!(err, JmapQueryError::MissingResponse));
306 }
307
308 #[test]
309 fn parse_error_returns_parse_response() {
310 let mut cor = make_query();
311 expect_wants_write(&mut cor, None);
312 expect_wants_read(&mut cor);
313
314 let body = br#"{
315 "methodResponses": [["Email/query", {"queryState":42}, "c0"]],
316 "sessionState": "s"
317 }"#;
318 let reply = build_http_reply(body);
319 let err = expect_complete_err(&mut cor, &reply);
320 assert!(matches!(err, JmapQueryError::ParseResponse(_)));
321 }
322
323 #[test]
324 fn total_when_calculate_total_set() {
325 let mut cor = make_query();
326 expect_wants_write(&mut cor, None);
327 expect_wants_read(&mut cor);
328
329 let body = br#"{
330 "methodResponses": [["Email/query", {"queryState":"qs","position":0,"ids":[],"total":42}, "c0"]],
331 "sessionState": "s"
332 }"#;
333 let reply = build_http_reply(body);
334 let out = expect_complete_ok(&mut cor, &reply);
335 assert_eq!(out.total, Some(42));
336 }
337
338 fn expect_wants_write(cor: &mut JmapQuery, arg: Option<&[u8]>) -> Vec<u8> {
341 match cor.resume(arg) {
342 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => bytes,
343 state => panic!("expected WantsWrite, got {state:?}"),
344 }
345 }
346
347 fn expect_wants_read(cor: &mut JmapQuery) {
348 match cor.resume(None) {
349 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {}
350 state => panic!("expected WantsRead, got {state:?}"),
351 }
352 }
353
354 fn expect_complete_ok(cor: &mut JmapQuery, reply: &[u8]) -> JmapQueryOutput {
355 match cor.resume(Some(reply)) {
356 JmapCoroutineState::Complete(Ok(out)) => out,
357 state => panic!("expected Complete(Ok), got {state:?}"),
358 }
359 }
360
361 fn expect_complete_err(cor: &mut JmapQuery, reply: &[u8]) -> JmapQueryError {
362 match cor.resume(Some(reply)) {
363 JmapCoroutineState::Complete(Err(err)) => err,
364 state => panic!("expected Complete(Err), got {state:?}"),
365 }
366 }
367}