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