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