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