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