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 JmapChangesError {
42 #[error("JMAP Foo/changes failed: missing response in method_responses")]
44 MissingResponse,
45 #[error("JMAP Foo/changes failed: {0}")]
47 Send(#[from] JmapSendError),
48 #[error("JMAP Foo/changes failed: serialize args: {0}")]
50 SerializeArgs(#[source] serde_json::Error),
51 #[error("JMAP Foo/changes failed: parse response: {0}")]
53 ParseResponse(#[source] serde_json::Error),
54 #[error("JMAP Foo/changes failed: {0}")]
56 Method(#[from] JmapMethodError),
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct JmapChangesOptions {
62 pub max_changes: Option<u64>,
65}
66
67#[derive(Clone, Debug)]
74pub struct JmapChangesOutput {
75 pub new_state: String,
77 pub has_more_changes: bool,
79 pub created: Vec<String>,
81 pub updated: Vec<String>,
83 pub destroyed: Vec<String>,
85 pub keep_alive: bool,
87}
88
89pub struct JmapChanges {
92 state: State,
93}
94
95impl JmapChanges {
96 pub fn new(
98 account_id: String,
99 http_auth: &SecretString,
100 api_url: &Url,
101 method: impl Into<String>,
102 capabilities: Vec<String>,
103 since_state: impl Into<String>,
104 opts: JmapChangesOptions,
105 ) -> Result<Self, JmapChangesError> {
106 let since_state = since_state.into();
107 let args = serde_json::to_value(ChangesArgs {
108 account_id: &account_id,
109 since_state: &since_state,
110 max_changes: opts.max_changes,
111 })
112 .map_err(JmapChangesError::SerializeArgs)?;
113
114 let mut batch = JmapBatch::new();
115 batch.add(method, args);
116 let request = batch.into_request(capabilities);
117
118 Ok(Self {
119 state: State::Send(JmapSend::new(http_auth, api_url, request)?),
120 })
121 }
122
123 pub fn from_send(send: JmapSend) -> Self {
125 Self {
126 state: State::Send(send),
127 }
128 }
129}
130
131impl JmapCoroutine for JmapChanges {
132 type Yield = JmapYield;
133 type Return = Result<JmapChangesOutput, JmapChangesError>;
134
135 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
136 match &mut self.state {
137 State::Send(send) => {
138 let JmapSendOutput {
139 response,
140 keep_alive,
141 } = jmap_try!(send, arg);
142
143 let Some((name, args, _)) = response.method_responses.into_iter().next() else {
144 return JmapCoroutineState::Complete(Err(JmapChangesError::MissingResponse));
145 };
146
147 if name == "error" {
148 let err = serde_json::from_value::<JmapMethodError>(args)
149 .unwrap_or(JmapMethodError::Unknown);
150 return JmapCoroutineState::Complete(Err(err.into()));
151 }
152
153 match serde_json::from_value::<ChangesResponse>(args) {
154 Ok(r) => JmapCoroutineState::Complete(Ok(JmapChangesOutput {
155 new_state: r.new_state,
156 has_more_changes: r.has_more_changes,
157 created: r.created,
158 updated: r.updated,
159 destroyed: r.destroyed,
160 keep_alive,
161 })),
162 Err(err) => {
163 JmapCoroutineState::Complete(Err(JmapChangesError::ParseResponse(err)))
164 }
165 }
166 }
167 }
168 }
169}
170
171enum State {
172 Send(JmapSend),
173}
174
175#[derive(Serialize)]
176#[serde(rename_all = "camelCase")]
177struct ChangesArgs<'a> {
178 account_id: &'a str,
179 since_state: &'a str,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 max_changes: Option<u64>,
182}
183
184#[derive(Deserialize)]
185#[serde(rename_all = "camelCase")]
186struct ChangesResponse {
187 new_state: String,
188 has_more_changes: bool,
189 created: Vec<String>,
190 updated: Vec<String>,
191 destroyed: Vec<String>,
192}
193
194#[cfg(test)]
195mod tests {
196 use alloc::{format, string::ToString, vec};
197
198 use crate::rfc8620::changes::*;
199
200 fn make_auth() -> SecretString {
201 SecretString::from("Bearer test")
202 }
203
204 fn make_url() -> Url {
205 "https://api.example.com/jmap/".parse().unwrap()
206 }
207
208 fn make_changes() -> JmapChanges {
209 JmapChanges::new(
210 "a1".to_string(),
211 &make_auth(),
212 &make_url(),
213 "Email/changes",
214 vec!["urn:ietf:params:jmap:mail".to_string()],
215 "s1",
216 JmapChangesOptions::default(),
217 )
218 .unwrap()
219 }
220
221 fn build_http_reply(body: &[u8]) -> Vec<u8> {
222 let head = format!(
223 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n",
224 body.len()
225 );
226 let mut bytes = head.into_bytes();
227 bytes.extend_from_slice(body);
228 bytes
229 }
230
231 #[test]
232 fn success_returns_ok() {
233 let mut cor = make_changes();
234 expect_wants_write(&mut cor, None);
235 expect_wants_read(&mut cor);
236
237 let body = br#"{
238 "methodResponses": [["Email/changes", {"newState":"s2","hasMoreChanges":false,"created":["e1"],"updated":[],"destroyed":["e2"]}, "c0"]],
239 "sessionState": "s2"
240 }"#;
241 let reply = build_http_reply(body);
242 let out = expect_complete_ok(&mut cor, &reply);
243 assert_eq!(out.new_state, "s2");
244 assert_eq!(out.created, vec!["e1".to_string()]);
245 assert_eq!(out.destroyed, vec!["e2".to_string()]);
246 }
247
248 #[test]
249 fn method_error_returns_method_error() {
250 let mut cor = make_changes();
251 expect_wants_write(&mut cor, None);
252 expect_wants_read(&mut cor);
253
254 let body = br#"{
255 "methodResponses": [["error", {"type":"cannotCalculateChanges"}, "c0"]],
256 "sessionState": "s1"
257 }"#;
258 let reply = build_http_reply(body);
259 let err = expect_complete_err(&mut cor, &reply);
260 assert!(matches!(err, JmapChangesError::Method(_)));
261 }
262
263 #[test]
264 fn missing_response_returns_missing_response() {
265 let mut cor = make_changes();
266 expect_wants_write(&mut cor, None);
267 expect_wants_read(&mut cor);
268
269 let reply = build_http_reply(br#"{"methodResponses":[], "sessionState":"s"}"#);
270 let err = expect_complete_err(&mut cor, &reply);
271 assert!(matches!(err, JmapChangesError::MissingResponse));
272 }
273
274 #[test]
275 fn parse_error_returns_parse_response() {
276 let mut cor = make_changes();
277 expect_wants_write(&mut cor, None);
278 expect_wants_read(&mut cor);
279
280 let body = br#"{
281 "methodResponses": [["Email/changes", {"newState":42}, "c0"]],
282 "sessionState": "s"
283 }"#;
284 let reply = build_http_reply(body);
285 let err = expect_complete_err(&mut cor, &reply);
286 assert!(matches!(err, JmapChangesError::ParseResponse(_)));
287 }
288
289 #[test]
290 fn has_more_changes_flag_propagates() {
291 let mut cor = make_changes();
292 expect_wants_write(&mut cor, None);
293 expect_wants_read(&mut cor);
294
295 let body = br#"{
296 "methodResponses": [["Email/changes", {"newState":"s2","hasMoreChanges":true,"created":[],"updated":[],"destroyed":[]}, "c0"]],
297 "sessionState": "s2"
298 }"#;
299 let reply = build_http_reply(body);
300 let out = expect_complete_ok(&mut cor, &reply);
301 assert!(out.has_more_changes);
302 }
303
304 fn expect_wants_write(cor: &mut JmapChanges, arg: Option<&[u8]>) -> Vec<u8> {
305 match cor.resume(arg) {
306 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => bytes,
307 state => panic!("expected WantsWrite, got {state:?}"),
308 }
309 }
310
311 fn expect_wants_read(cor: &mut JmapChanges) {
312 match cor.resume(None) {
313 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {}
314 state => panic!("expected WantsRead, got {state:?}"),
315 }
316 }
317
318 fn expect_complete_ok(cor: &mut JmapChanges, reply: &[u8]) -> JmapChangesOutput {
319 match cor.resume(Some(reply)) {
320 JmapCoroutineState::Complete(Ok(out)) => out,
321 state => panic!("expected Complete(Ok), got {state:?}"),
322 }
323 }
324
325 fn expect_complete_err(cor: &mut JmapChanges, reply: &[u8]) -> JmapChangesError {
326 match cor.resume(Some(reply)) {
327 JmapCoroutineState::Complete(Err(err)) => err,
328 state => panic!("expected Complete(Err), got {state:?}"),
329 }
330 }
331}