1use std::collections::HashMap;
6
7use http::{
8 Request, Response, StatusCode, Uri,
9 response::Parts,
10 uri::{InvalidUri, PathAndQuery},
11};
12use http_body_util::BodyExt as _;
13use hyper::body::{Bytes, Incoming};
14use log::trace;
15use serde::{Deserialize, Serialize, de::DeserializeOwned};
16use serde_json::Value as JsonValue;
17use tokio::sync::Mutex;
18use tower_service::Service;
19
20use crate::{
21 addressbook::JMAP_CONTACTS,
22 calendar::JMAP_CALENDARS,
23 error::{Error, Result},
24};
25
26pub const JMAP_CORE: &str = "urn:ietf:params:jmap:core";
28
29pub mod addressbook;
30pub mod calendar;
31pub mod error;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JmapResponse<T> {
36 #[serde(rename = "methodResponses")]
37 pub method_responses: Vec<MethodResponse<T>>,
38 #[serde(rename = "sessionState")]
39 pub session_state: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(untagged)]
46pub enum MethodResponseData<T> {
47 Error(JmapError),
50 Success(T),
52}
53
54impl<T> MethodResponseData<T> {
55 pub fn into_result(self) -> Result<T> {
57 match self {
58 MethodResponseData::Error(error) => Err(Error::ServerError {
59 error_type: error.error_type,
60 description: error.description,
61 }),
62 MethodResponseData::Success(data) => Ok(data),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct MethodResponse<T>(pub String, pub MethodResponseData<T>, pub String);
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct GetResponse<T> {
76 pub state: String,
77 pub list: Vec<T>,
78 #[serde(rename = "notFound")]
79 pub not_found: Vec<String>,
80 #[serde(rename = "accountId")]
81 pub account_id: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct QueryResponse {
89 pub filter: Option<JsonValue>,
90 pub sort: Option<JsonValue>,
91 #[serde(rename = "queryState")]
92 pub query_state: String,
93 #[serde(rename = "canCalculateChanges")]
94 pub can_calculate_changes: bool,
95 pub position: u32,
96 pub ids: Vec<String>,
97 pub total: Option<u32>,
98 #[serde(rename = "accountId")]
99 pub account_id: String,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ChangesResponse {
107 #[serde(rename = "oldState")]
108 pub old_state: String,
109 #[serde(rename = "newState")]
110 pub new_state: String,
111 #[serde(rename = "hasMoreChanges")]
112 pub has_more_changes: bool,
113 pub created: Vec<String>,
114 pub updated: Vec<String>,
115 pub destroyed: Vec<String>,
116 #[serde(rename = "accountId")]
117 pub account_id: String,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ChangeStatus {
123 NotChanged,
125 Changed,
127 Deleted,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SetResponse<T> {
134 #[serde(rename = "oldState")]
135 pub old_state: String,
136 #[serde(rename = "newState")]
137 pub new_state: String,
138 pub created: Option<HashMap<String, T>>,
139 pub updated: Option<HashMap<String, T>>,
140 pub destroyed: Option<Vec<String>>,
141 #[serde(rename = "notCreated")]
142 pub not_created: Option<HashMap<String, JmapError>>,
143 #[serde(rename = "notUpdated")]
144 pub not_updated: Option<HashMap<String, JmapError>>,
145 #[serde(rename = "notDestroyed")]
146 pub not_destroyed: Option<HashMap<String, JmapError>>,
147 #[serde(rename = "accountId")]
148 pub account_id: String,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct JmapError {
154 #[serde(rename = "type")]
155 pub error_type: String,
156 pub properties: Option<Vec<String>>,
157 pub description: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RequestObject {
163 pub using: Vec<String>,
165 #[serde(rename = "methodCalls")]
167 pub method_calls: Vec<Invocation>,
168 #[serde(rename = "createdIds", skip_serializing_if = "Option::is_none")]
170 pub created_ids: Option<HashMap<String, String>>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct Invocation(pub String, pub JsonValue, pub String);
176
177impl RequestObject {
178 #[must_use]
180 pub fn new(capabilities: Vec<String>) -> Self {
181 Self {
182 using: capabilities,
183 method_calls: Vec::new(),
184 created_ids: None,
185 }
186 }
187
188 #[must_use]
190 pub fn with_method_call(
191 mut self,
192 method_name: String,
193 arguments: JsonValue,
194 call_id: String,
195 ) -> Self {
196 self.method_calls
197 .push(Invocation(method_name, arguments, call_id));
198 self
199 }
200
201 #[must_use]
203 pub fn with_core_capability(capability: &str) -> Self {
204 Self::new(vec![JMAP_CORE.to_string(), capability.to_string()])
205 }
206}
207
208pub async fn discover_session_resource<C, E>(
221 http_client: &mut C,
222 base_url: &Uri,
223) -> Result<(Uri, SessionResource), DiscoverSessionError<E>>
224where
225 C: Service<http::Request<String>, Response = Response<Incoming>, Error = E> + 'static,
227 E: std::error::Error + Send + Sync,
228{
229 let mut uri: Uri = make_relative_url(base_url.clone(), "/.well-known/jmap")
230 .map_err(DiscoverSessionError::InvalidInput)?;
231 let (mut head, mut raw_body);
232
233 loop {
234 let request = Request::builder()
235 .method("GET")
236 .uri(&uri)
237 .body(String::new())
238 .expect("All provided arguments are propertly validated.");
239 let response = http_client
240 .call(request)
241 .await
242 .map_err(DiscoverSessionError::ClientError)?;
243 (head, raw_body) = response.into_parts();
244 if !head.status.is_redirection() {
245 break;
246 }
247
248 let location = head
249 .headers
250 .get(hyper::header::LOCATION)
251 .ok_or(DiscoverSessionError::MissingLocation)?
252 .as_bytes();
253
254 let location_uri =
255 Uri::try_from(location).map_err(DiscoverSessionError::InvalidLocation)?;
256
257 uri = if location_uri.host().is_some() {
258 location_uri
259 } else {
260 let loc_parts = location_uri.into_parts();
261
262 let mut parts = uri.into_parts();
263 parts.path_and_query = loc_parts.path_and_query;
264 Uri::from_parts(parts).expect("Building from validated parts must succeed")
265 }
266 }
267
268 let body = raw_body
269 .collect()
270 .await
271 .map_err(DiscoverSessionError::StreamingError)?
272 .to_bytes();
273 trace!("{body:?}");
274 check_status(head.status).map_err(DiscoverSessionError::BadStatus)?;
275 let session_resource: SessionResource =
276 serde_json::from_slice(body.as_ref()).map_err(DiscoverSessionError::ResponseNotJson)?;
277 Ok((uri, session_resource))
278}
279
280pub struct JmapClient<C>
282where
283 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + 'static,
284{
285 http_client: Mutex<C>,
286 session_url: Uri,
287 api_url: Uri,
288}
289
290pub trait RecordType: DeserializeOwned + Sized {
292 const ITEM_NAME: &'static str;
293 const COLLECTION_NAME: &'static str;
294 const CAPABILITY: &'static str;
295
296 fn id(&self) -> &str;
297}
298
299#[derive(Debug)]
301pub struct Record {
302 pub id: String,
304 pub state: String,
305 pub data: JsonValue,
306}
307
308#[derive(Debug)]
310pub struct RecordRef {
311 pub id: String,
312 pub state: String,
313}
314
315impl<C> JmapClient<C>
316where
317 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + 'static,
318 <C as Service<http::Request<String>>>::Error: std::error::Error + Send + Sync,
319{
320 pub fn new(client: C, session_url: Uri, api_url: Uri) -> JmapClient<C> {
323 JmapClient {
324 http_client: Mutex::new(client),
325 session_url,
326 api_url,
327 }
328 }
329
330 pub async fn discover_session_url(
340 mut client: C,
341 scheme: Scheme,
342 domain: String,
343 ) -> Result<Uri, DiscoverError> {
345 let uri: Uri = format!("{scheme}://{domain}:443/.well-known/jmap")
350 .parse()
351 .map_err(DiscoverError::InvalidDomain)?;
352
353 let request = Request::builder()
354 .method("GET")
355 .uri(uri)
356 .body(String::new())
357 .expect("All provided arguments are propertly validated.");
358 let response_future = client.call(request).await;
359 let response = response_future.unwrap();
360 let (head, body) = response.into_parts();
361 check_status(head.status).unwrap();
362 let _body = body.collect().await.unwrap().to_bytes();
363 todo!()
364 }
365
366 pub fn api_url(&self) -> &Uri {
368 &self.api_url
369 }
370
371 pub fn session_context_url(&self) -> &Uri {
373 &self.session_url
374 }
375
376 pub async fn get_session_resource(&self) -> Result<SessionResource> {
383 let request = Request::builder()
384 .method("GET")
385 .uri(&self.session_url)
386 .body(String::new())?;
387
388 let mut http_client = self.http_client.lock().await;
389 let response = http_client
390 .call(request)
391 .await
392 .map_err(|e| Error::ClientConnection(Box::new(e)))?;
393 let (head, body) = response.into_parts();
394 check_status(head.status)?;
395 let body_bytes = body.collect().await?.to_bytes();
396 let session_resource: SessionResource = serde_json::from_slice(body_bytes.as_ref())?;
397 Ok(session_resource)
398 }
399
400 async fn jmap_request(&self, request: RequestObject) -> Result<Bytes> {
401 let body = serde_json::to_string(&request)?;
402 let uri = make_relative_url(self.session_url.clone(), &self.api_url.to_string())?;
403 let request = Request::builder()
404 .method("POST")
405 .header("Content-Type", "application/json; charset=utf8")
406 .uri(uri)
407 .body(body)?;
408 let (head, body) = self.request(request).await?;
409 check_status(head.status)?;
410 Ok(body)
411 }
412
413 async fn request(&self, request: Request<String>) -> Result<(Parts, Bytes), RequestError> {
421 let mut client = self.http_client.lock().await;
422 let response_future = client.call(request);
423 drop(client); let response = response_future
426 .await
427 .map_err(|e| RequestError::Client(Box::from(e)))?;
428 let (head, body) = response.into_parts();
429 let body = body.collect().await?.to_bytes();
430
431 Ok((head, body))
433 }
434
435 async fn create(
438 &self,
439 capability: &str,
440 data_type: &str,
441 data: JsonValue,
442 if_state: Option<&str>,
443 ) -> Result<Record> {
444 let mut arguments = serde_json::json!({
445 "create": {
446 "new0": data
447 }
448 });
449 if let Some(state) = if_state {
450 arguments["ifInState"] = serde_json::json!(state);
451 }
452 let request = RequestObject::new(vec![JMAP_CORE.to_string(), capability.to_string()])
453 .with_method_call(format!("{data_type}/set"), arguments, "0".to_string());
454 let body = self.jmap_request(request).await?;
455 let response_json: JmapResponse<SetResponse<JsonValue>> =
456 serde_json::from_slice(body.as_ref())?;
457 let mut responses = response_json.method_responses;
458 let method_response = responses.pop().ok_or(Error::InvalidResponse {
459 reason: "No method responses found".to_string(),
460 })?;
461
462 let set_response = method_response.1.into_result()?;
463
464 if let Some(not_created) = set_response.not_created
465 && let Some(error) = not_created.get("new0")
466 {
467 return Err(Error::ServerError {
468 error_type: error.error_type.clone(),
469 description: error.description.clone(),
470 });
471 }
472
473 let created_item = set_response
474 .created
475 .ok_or(Error::InvalidResponse {
476 reason: "No items were created".to_string(),
477 })?
478 .get("new0")
479 .ok_or(Error::InvalidResponse {
480 reason: "Created item 'new0' missing from response".to_string(),
481 })?
482 .clone();
483
484 let id = created_item["id"]
485 .as_str()
486 .ok_or(Error::InvalidResponse {
487 reason: "Created item missing 'id' field".to_string(),
488 })?
489 .to_string();
490
491 let created = Record {
492 id,
493 state: set_response.new_state,
494 data: created_item,
495 };
496 Ok(created)
497 }
498
499 async fn update(
500 &self,
501 capability: &str,
502 data_type: &str,
503 id: &str,
504 data: JsonValue,
505 if_state: Option<&str>,
506 ) -> Result<Record> {
507 let arguments = serde_json::json!({
508 "update": { id.to_string(): data},
509 "ifInState": if_state,
510 });
511 let request = RequestObject::new(vec![JMAP_CORE.to_string(), capability.to_string()])
512 .with_method_call(format!("{data_type}/set"), arguments, "0".to_string());
513 let body = self.jmap_request(request).await?;
514 let response_json: JmapResponse<SetResponse<JsonValue>> =
515 serde_json::from_slice(body.as_ref())?;
516 let mut responses = response_json.method_responses;
517 let method_response = responses.pop().ok_or(Error::InvalidResponse {
518 reason: "No method responses found".to_string(),
519 })?;
520
521 let set_response = method_response.1.into_result()?;
522
523 if let Some(not_updated) = set_response.not_updated
524 && let Some(error) = not_updated.get(id)
525 {
526 return Err(Error::ServerError {
527 error_type: error.error_type.clone(),
528 description: error.description.clone(),
529 });
530 }
531
532 let updated_item = set_response
533 .updated
534 .ok_or(Error::InvalidResponse {
535 reason: "No items were updated".to_string(),
536 })?
537 .get(id)
538 .ok_or_else(|| Error::InvalidResponse {
539 reason: format!("Updated item '{id}' missing from response"),
540 })?
541 .clone();
542
543 let updated = Record {
544 id: id.to_string(),
545 state: set_response.new_state,
546 data: updated_item,
547 };
548 Ok(updated)
549 }
550
551 pub async fn create_collection<T: RecordType>(&self, name: &str) -> Result<Record> {
557 let collection = serde_json::json!({
558 "name": name
559 });
561 self.create(T::CAPABILITY, T::COLLECTION_NAME, collection, None)
562 .await
563 }
565
566 pub async fn create_record<T: RecordType>(
573 &self,
574 calendar_id: &str,
575 record: &JsonValue,
576 if_state: Option<&str>,
577 ) -> Result<Record> {
578 let mut record = record.clone();
579 if let JsonValue::Object(ref mut map) = record {
580 map.insert(
581 "calendarIds".to_string(),
582 serde_json::json!({
583 calendar_id: true
584 }),
585 );
586 } else {
587 return Err(Error::InvalidData {
588 message: "Record must be a JSON object".to_string(),
589 });
590 }
591
592 self.create(T::CAPABILITY, T::ITEM_NAME, record, if_state)
593 .await
594 }
595
596 pub async fn update_record<T: RecordType>(
603 &self,
604 record_id: &str,
605 calendar_id: &str,
606 record: &JsonValue,
607 if_state: Option<&str>,
608 ) -> Result<Record> {
609 let mut record = record.clone();
610 if let JsonValue::Object(ref mut map) = record {
611 map.insert(
612 "calendarIds".to_string(),
613 serde_json::json!({
614 calendar_id: true
615 }),
616 );
617 } else {
618 return Err(Error::InvalidData {
619 message: "Record must be a JSON object".to_string(),
620 });
621 }
622
623 self.update(T::CAPABILITY, T::ITEM_NAME, record_id, record, if_state)
624 .await
625 }
626
627 async fn delete(
629 &self,
630 capability: &str,
631 data_type: &str,
632 id: &str,
633 if_state: Option<&str>,
634 ) -> Result<String> {
635 let arguments = serde_json::json!({
636 "destroy": [id],
637 "ifInState": if_state,
638 });
639 let request = RequestObject::new(vec![JMAP_CORE.to_string(), capability.to_string()])
640 .with_method_call(format!("{data_type}/set"), arguments, "d0".to_string());
641 let body = self.jmap_request(request).await?;
642 let response_json: JmapResponse<SetResponse<JsonValue>> =
643 serde_json::from_slice(body.as_ref())?;
644 let mut responses = response_json.method_responses;
645 let method_response = responses.pop().ok_or(Error::InvalidResponse {
646 reason: "No method responses found".to_string(),
647 })?;
648
649 let set_response = method_response.1.into_result()?;
650
651 if let Some(not_destroyed) = set_response.not_destroyed
652 && let Some(error) = not_destroyed.get(id)
653 {
654 return Err(Error::ServerError {
655 error_type: error.error_type.clone(),
656 description: error.description.clone(),
657 });
658 }
659
660 let destroyed_items = set_response.destroyed.ok_or(Error::InvalidResponse {
661 reason: "No items were destroyed".to_string(),
662 })?;
663
664 if !destroyed_items.contains(&id.to_string()) {
665 return Err(Error::IncompleteOperation {
666 message: format!("Item '{id}' was not destroyed"),
667 });
668 }
669
670 Ok(set_response.new_state)
671 }
672
673 pub async fn delete_collection<T: RecordType>(
679 &self,
680 collection_id: &str,
681 if_state: Option<&str>,
682 ) -> Result<String> {
683 self.delete(T::CAPABILITY, T::COLLECTION_NAME, collection_id, if_state)
684 .await
685 }
686
687 pub async fn delete_record<T: RecordType>(
694 &self,
695 record_id: &str,
696 if_state: Option<&str>,
697 ) -> Result<String> {
698 self.delete(T::CAPABILITY, T::ITEM_NAME, record_id, if_state)
699 .await
700 }
701
702 pub async fn get_collections<T>(&self) -> Result<Vec<T>>
708 where
709 T: RecordType,
710 {
711 let arguments = serde_json::json!({ "ids": null });
712 let request = RequestObject::new(vec![JMAP_CORE.to_string(), T::CAPABILITY.to_string()])
713 .with_method_call(
714 format!("{}/get", T::COLLECTION_NAME),
715 arguments,
716 "0".to_string(),
717 );
718 let body = self.jmap_request(request).await?;
719
720 let parsed_body: JmapResponse<GetResponse<T>> = serde_json::from_slice(body.as_ref())?;
721 let mut responses = parsed_body.method_responses;
722 let method_response = responses.pop().ok_or(Error::InvalidResponse {
723 reason: "No method responses found".to_string(),
724 })?;
725
726 let get_response = method_response.1.into_result()?;
727
728 Ok(get_response.list)
729 }
730
731 pub async fn get_records<T: RecordType>(&self, calendar_id: &str) -> Result<Vec<Record>> {
737 let query_args = serde_json::json!({
739 "filter": { "inCalendars": [calendar_id] }
740 });
741 let get_args = serde_json::json!({
743 "#ids": {
744 "resultOf": "Q1",
745 "name": "CalendarEvent/query",
746 "path": "/ids"
747 }
748 });
749
750 let request = RequestObject::new(vec![JMAP_CORE.to_string(), T::CAPABILITY.to_string()])
751 .with_method_call(
752 format!("{}/query", T::ITEM_NAME),
753 query_args,
754 "Q1".to_string(),
755 )
756 .with_method_call(format!("{}/get", T::ITEM_NAME), get_args, "Q2".to_string());
757
758 let body = self.jmap_request(request).await?;
759 let response_json: JmapResponse<JsonValue> = serde_json::from_slice(body.as_ref())?;
761
762 let method_response = response_json
763 .method_responses
764 .into_iter()
765 .find(|m| m.2 == "Q2")
766 .ok_or(Error::InvalidResponse {
767 reason: "Method response Q2 not found".to_string(),
768 })?;
769
770 let response_data = method_response.1.into_result()?;
771
772 let get_response: GetResponse<JsonValue> = serde_json::from_value(response_data.clone())?;
773
774 let records = get_response
775 .list
776 .iter()
777 .map(|item| Record {
778 id: item["id"].as_str().unwrap_or("").to_string(),
779 state: get_response.state.clone(),
780 data: item.clone(),
781 })
782 .collect::<Vec<_>>();
783
784 Ok(records)
785 }
786
787 pub async fn changes<T: RecordType>(
796 &self,
797 since_state: &str,
798 max_changes: Option<u32>,
799 ) -> Result<ChangesResponse> {
800 let mut arguments = serde_json::json!({
801 "sinceState": since_state,
802 });
803
804 if let Some(max) = max_changes {
805 arguments["maxChanges"] = serde_json::json!(max);
806 }
807
808 let request = RequestObject::new(vec![JMAP_CORE.to_string(), T::CAPABILITY.to_string()])
809 .with_method_call(
810 format!("{}/changes", T::ITEM_NAME),
811 arguments,
812 "c0".to_string(),
813 );
814
815 let body = self.jmap_request(request).await?;
816 let response_json: JmapResponse<ChangesResponse> = serde_json::from_slice(body.as_ref())?;
817
818 let mut responses = response_json.method_responses;
819 let method_response = responses.pop().ok_or(Error::InvalidResponse {
820 reason: "No method responses found".to_string(),
821 })?;
822
823 method_response.1.into_result()
824 }
825
826 pub async fn changed_since<T: RecordType>(
840 &self,
841 id: &str,
842 old_state: &str,
843 ) -> Result<(ChangeStatus, String)> {
844 let mut current_state = old_state.to_string();
845 let id_str = id.to_string();
846
847 let mut latest_status = ChangeStatus::NotChanged;
848 loop {
849 let changes_response = self.changes::<T>(¤t_state, Some(500)).await?;
850
851 #[allow(clippy::collapsible_if)] if changes_response.destroyed.contains(&id_str) {
854 latest_status = ChangeStatus::Deleted;
855 } else if changes_response.updated.contains(&id_str) {
856 if latest_status != ChangeStatus::Deleted {
857 latest_status = ChangeStatus::Changed;
858 }
859 } else if changes_response.created.contains(&id_str) {
860 if latest_status == ChangeStatus::NotChanged {
861 latest_status = ChangeStatus::Changed;
862 }
863 }
864
865 if !changes_response.has_more_changes {
866 break;
867 }
868
869 current_state = changes_response.new_state;
870 }
871
872 Ok((latest_status, current_state))
873 }
874}
875
876#[derive(Debug)]
878pub enum DiscoverSessionError<E>
879where
880 E: std::fmt::Debug,
881{
882 InvalidInput(http::Error),
883 ClientError(E),
884 MissingLocation,
885 InvalidLocation(InvalidUri),
886 BadStatus(StatusCode),
887 StreamingError(hyper::Error),
888 ResponseNotJson(serde_json::Error),
889}
890
891impl<E> std::fmt::Display for DiscoverSessionError<E>
892where
893 E: std::fmt::Debug,
894{
895 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896 std::fmt::Debug::fmt(self, f)
897 }
898}
899
900impl<E> std::error::Error for DiscoverSessionError<E> where E: std::fmt::Debug {}
901
902pub enum Scheme {
904 HTTP,
906 HTTPS,
908}
909
910impl std::fmt::Display for Scheme {
911 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
912 match self {
913 Scheme::HTTP => f.write_str("http"),
914 Scheme::HTTPS => f.write_str("https"),
915 }
916 }
917}
918
919#[derive(thiserror::Error, Debug)]
920pub enum DiscoverError {
921 #[error("Invalid domain provided as input")]
922 InvalidDomain(InvalidUri),
923
924 #[error("Received error retrieveing well-known resource.")]
925 WellKnownError(StatusCode),
926}
927
928#[derive(thiserror::Error, Debug)]
931pub enum RequestError {
932 #[error("error executing http request: {0}")]
934 Http(#[from] hyper::Error),
935
936 #[error("client error executing request: {0}")]
938 Client(Box<dyn std::error::Error + Send + Sync>),
940}
941
942#[derive(Debug, PartialEq, Deserialize)]
943pub struct SessionResource {
944 #[serde(rename = "apiUrl")]
945 pub api_url: Option<String>,
946 pub capabilities: std::collections::HashMap<String, JsonValue>,
947 #[serde(flatten)]
948 pub other_fields: JsonValue,
949}
950
951impl SessionResource {
952 #[must_use]
953 pub fn api_url(&self) -> Option<&str> {
954 self.api_url.as_deref()
955 }
956
957 #[must_use]
958 pub fn supports_calendars(&self) -> bool {
959 self.capabilities.contains_key(JMAP_CALENDARS)
960 }
961
962 #[must_use]
963 pub fn supports_address_books(&self) -> bool {
964 self.capabilities.contains_key(JMAP_CONTACTS)
965 }
966
967 }
972
973#[inline]
975pub(crate) fn check_status(status: StatusCode) -> Result<(), StatusCode> {
976 if status.is_success() {
977 Ok(())
978 } else {
979 Err(status)
980 }
981}
982
983fn make_relative_url(base: Uri, path: &str) -> Result<Uri, http::Error> {
984 let mut parts = base.into_parts();
986 parts.path_and_query = Some(PathAndQuery::try_from(path)?);
987 Uri::from_parts(parts).map_err(http::Error::from)
988}
989
990#[cfg(test)]
1003mod test {
1004 use http::Uri;
1005 use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
1006 use hyper_util::{
1007 client::legacy::{Client, connect::HttpConnector},
1008 rt::TokioExecutor,
1009 };
1010 use serde_json::json;
1011 use tower_http::auth::AddAuthorization;
1012
1013 use crate::{
1014 ChangeStatus, GetResponse, JmapClient, JmapResponse, JsonValue, MethodResponseData,
1015 SetResponse, addressbook::AddressBook, calendar::Calendar, discover_session_resource,
1016 };
1017
1018 const TEST_SERVER_BASE_URL: &str = "http://host.containers.internal:8080";
1020
1021 type TestClient = AddAuthorization<Client<HttpsConnector<HttpConnector>, String>>;
1022
1023 struct TestSetup {
1025 pub client: JmapClient<TestClient>,
1026 pub session: crate::SessionResource,
1027 }
1028
1029 async fn setup_test_client() -> TestSetup {
1030 let connector = HttpsConnectorBuilder::new()
1031 .with_native_roots()
1032 .unwrap()
1033 .https_or_http()
1034 .enable_http1()
1035 .build();
1036 let base_url = TEST_SERVER_BASE_URL.parse::<Uri>().unwrap();
1037
1038 let http_client = Client::builder(TokioExecutor::new()).build(connector);
1039 let mut auth_client = AddAuthorization::basic(http_client, "user1", "");
1040 let (session_uri, session) = discover_session_resource(&mut auth_client, &base_url)
1041 .await
1042 .unwrap();
1043
1044 let api_url = session.api_url().unwrap().parse().unwrap();
1046 let client = JmapClient::new(auth_client, session_uri, api_url);
1047
1048 TestSetup { client, session }
1049 }
1050
1051 #[tokio::test]
1055 async fn test_collection_methods() {
1056 let setup = setup_test_client().await;
1057 let jmap = setup.client;
1058 let session = setup.session;
1059
1060 assert!(session.supports_calendars());
1061 assert!(session.supports_address_books());
1062 assert_eq!(session.api_url(), Some("/jmap/"));
1063
1064 let initial = jmap.get_collections::<Calendar>().await.unwrap();
1065 let initial_count = initial.len();
1066
1067 let created = jmap.create_collection::<Calendar>("Created").await.unwrap();
1068 let post_create_count = jmap.get_collections::<Calendar>().await.unwrap().len();
1069
1070 assert_eq!(initial_count + 1, post_create_count);
1071
1072 let created_id = created.data["id"].as_str().unwrap();
1073 jmap.delete_collection::<Calendar>(created_id, None)
1074 .await
1075 .unwrap();
1076
1077 let post_delete_count = jmap.get_collections::<Calendar>().await.unwrap().len();
1078 assert_eq!(initial_count, post_delete_count);
1079 }
1080
1081 #[tokio::test]
1082 #[ignore = "Cyrus returns internal error"]
1083 async fn test_address_book_methods() {
1084 let setup = setup_test_client().await;
1085 let jmap = setup.client;
1086 let session = setup.session;
1087
1088 assert!(session.supports_calendars());
1089 assert!(session.supports_address_books());
1090 assert_eq!(session.api_url(), Some("/jmap/"));
1091
1092 let initial = jmap.get_collections::<AddressBook>().await.unwrap();
1093 let initial_count = initial.len();
1094
1095 let created = jmap.create_collection::<AddressBook>("Blah").await.unwrap();
1096 let post_create_count = jmap.get_collections::<AddressBook>().await.unwrap().len();
1097
1098 assert_eq!(initial_count + 1, post_create_count);
1099
1100 let created_id = created.data["id"].as_str().unwrap();
1101 jmap.delete_collection::<AddressBook>(created_id, None)
1102 .await
1103 .unwrap();
1104
1105 let post_delete_count = jmap.get_collections::<AddressBook>().await.unwrap().len();
1106 assert_eq!(initial_count, post_delete_count);
1107 }
1108
1109 #[tokio::test]
1110 async fn get_session_resource() {
1111 let setup = setup_test_client().await;
1112 let jmap = setup.client;
1113 let session = setup.session;
1114
1115 let session2 = jmap.get_session_resource().await.unwrap();
1116
1117 assert_eq!(session.api_url(), Some("/jmap/"));
1118 assert!(session.supports_calendars());
1119 assert!(session.supports_address_books());
1120
1121 assert_eq!(session, session2);
1122 }
1123
1124 #[tokio::test]
1125 async fn test_record_methods() {
1126 let setup = setup_test_client().await;
1127 let jmap = setup.client;
1128 let session = setup.session;
1129
1130 assert!(session.supports_calendars());
1131 assert!(session.supports_address_books());
1132 assert_eq!(session.api_url(), Some("/jmap/"));
1133
1134 let initial_count = jmap.get_records::<Calendar>("Default").await.unwrap().len();
1135
1136 let event = json!({
1137 "title": "Meeting with Bob",
1139 "start": "2025-07-10T10:00:00",
1140 "duration": "PT1H"
1141 });
1142 let created = jmap
1143 .create_record::<Calendar>("Default", &event, None)
1144 .await
1145 .unwrap();
1146
1147 let records = jmap.get_records::<Calendar>("Default").await.unwrap();
1148 let post_create_count = records.len();
1149 assert!(records.iter().any(|r| r.id == created.id));
1150 assert_eq!(initial_count + 1, post_create_count);
1151
1152 let updated_event = json!({
1154 "title": "Updated Meeting with Bob",
1155 "start": "2025-07-10T10:00:00",
1156 "duration": "PT1H"
1157 });
1158 jmap.update_record::<Calendar>(&created.id, "Default", &updated_event, Some("0"))
1159 .await
1160 .unwrap_err();
1161
1162 let updated = jmap
1164 .update_record::<Calendar>(&created.id, "Default", &updated_event, None)
1165 .await
1166 .unwrap();
1167 assert_eq!(updated.id, created.id);
1168 assert_ne!(updated.state, created.state); let updated_event = json!({
1172 "title": "Updated Meeting with Bobby",
1173 "start": "2025-07-10T10:00:00",
1174 "duration": "PT1H"
1175 });
1176 let updated = jmap
1177 .update_record::<Calendar>(&created.id, "Default", &updated_event, Some(&updated.state))
1178 .await
1179 .unwrap();
1180 assert_eq!(updated.id, created.id);
1181 assert_ne!(updated.state, created.state); let records_after_update = jmap.get_records::<Calendar>("Default").await.unwrap();
1184 let updated_record = records_after_update
1185 .iter()
1186 .find(|r| r.id == created.id)
1187 .unwrap();
1188 assert_eq!(updated_record.data["title"], "Updated Meeting with Bobby");
1189
1190 jmap.delete_record::<Calendar>(&created.id, Some("0"))
1192 .await
1193 .unwrap_err();
1194 let records = jmap.get_records::<Calendar>("Default").await.unwrap();
1195 let failed_delete_count = records.len();
1196 assert!(records.iter().any(|r| r.id == created.id));
1197 assert_eq!(post_create_count, failed_delete_count);
1198
1199 jmap.delete_record::<Calendar>(&created.id, None)
1201 .await
1202 .unwrap();
1203 let records = jmap.get_records::<Calendar>("Default").await.unwrap();
1204 let post_delete_count = records.len();
1205 assert!(!records.iter().any(|r| r.id == created.id));
1206 assert_eq!(initial_count, post_delete_count);
1207 }
1208
1209 #[tokio::test]
1210 async fn changes() {
1211 let setup = setup_test_client().await;
1212 let jmap = setup.client;
1213
1214 let event1 = json!({
1216 "title": "Initial Event",
1217 "start": "2025-07-09T10:00:00",
1218 "duration": "PT1H"
1219 });
1220 let created1 = jmap
1221 .create_record::<Calendar>("Default", &event1, None)
1222 .await
1223 .unwrap();
1224
1225 let baseline_state = created1.state.clone();
1227
1228 let event2 = json!({
1230 "title": "Test Event for Changes",
1231 "start": "2025-07-10T10:00:00",
1232 "duration": "PT1H"
1233 });
1234 let created2 = jmap
1235 .create_record::<Calendar>("Default", &event2, None)
1236 .await
1237 .unwrap();
1238
1239 let changes = jmap
1241 .changes::<Calendar>(&baseline_state, None)
1242 .await
1243 .unwrap();
1244 assert!(changes.created.contains(&created2.id));
1245 assert!(!changes.has_more_changes);
1246 assert_ne!(changes.old_state, changes.new_state);
1247
1248 let updated_event = json!({
1250 "title": "Updated Test Event",
1251 "start": "2025-07-10T11:00:00",
1252 "duration": "PT2H"
1253 });
1254 let state_before_update = changes.new_state.clone();
1255 jmap.update_record::<Calendar>(&created2.id, "Default", &updated_event, None)
1256 .await
1257 .unwrap();
1258
1259 let changes = jmap
1261 .changes::<Calendar>(&state_before_update, Some(100))
1262 .await
1263 .unwrap();
1264 assert!(changes.updated.contains(&created2.id));
1265 assert!(!changes.created.contains(&created2.id));
1266
1267 let state_before_delete = changes.new_state.clone();
1269 jmap.delete_record::<Calendar>(&created2.id, None)
1270 .await
1271 .unwrap();
1272
1273 let changes = jmap
1275 .changes::<Calendar>(&state_before_delete, None)
1276 .await
1277 .unwrap();
1278 assert!(changes.destroyed.contains(&created2.id));
1279 assert!(!changes.updated.contains(&created2.id));
1280 }
1281
1282 #[tokio::test]
1283 async fn changed_since() {
1284 let setup = setup_test_client().await;
1285 let jmap = setup.client;
1286
1287 let event1 = json!({
1289 "title": "Test Event for Changes",
1290 "start": "2025-07-10T10:00:00",
1291 "duration": "PT1H"
1292 });
1293 let created1 = jmap
1294 .create_record::<Calendar>("Default", &event1, None)
1295 .await
1296 .unwrap();
1297 let state_after_create = created1.state.clone();
1298
1299 let (status, _new_state) = jmap
1301 .changed_since::<Calendar>(&created1.id, &state_after_create)
1302 .await
1303 .unwrap();
1304 assert_eq!(status, ChangeStatus::NotChanged);
1305
1306 let event2 = json!({
1308 "title": "Another Event",
1309 "start": "2025-07-11T10:00:00",
1310 "duration": "PT1H"
1311 });
1312 let _created2 = jmap
1313 .create_record::<Calendar>("Default", &event2, None)
1314 .await
1315 .unwrap();
1316
1317 let (status, _new_state) = jmap
1319 .changed_since::<Calendar>(&created1.id, &state_after_create)
1320 .await
1321 .unwrap();
1322 assert_eq!(status, ChangeStatus::NotChanged);
1323
1324 let updated_event = json!({
1326 "title": "Updated Test Event",
1327 "start": "2025-07-10T10:00:00",
1328 "duration": "PT1H"
1329 });
1330 let _updated1 = jmap
1331 .update_record::<Calendar>(&created1.id, "Default", &updated_event, None)
1332 .await
1333 .unwrap();
1334
1335 let (status, _new_state) = jmap
1337 .changed_since::<Calendar>(&created1.id, &state_after_create)
1338 .await
1339 .unwrap();
1340 assert_eq!(status, ChangeStatus::Changed);
1341
1342 jmap.delete_record::<Calendar>(&created1.id, None)
1344 .await
1345 .unwrap();
1346
1347 let (status, _new_state) = jmap
1349 .changed_since::<Calendar>(&created1.id, &state_after_create)
1350 .await
1351 .unwrap();
1352 assert_eq!(status, ChangeStatus::Deleted);
1353 }
1354
1355 #[test]
1356 fn parse_get_calendar_success() {
1357 let body = r#"
1358{
1359 "methodResponses": [
1360 [
1361 "Calendar/get",
1362 {
1363 "state": "18",
1364 "list": [
1365 {
1366 "id": "Default",
1367 "name": "personal",
1368 "description": null,
1369 "sortOrder": 0,
1370 "isVisible": true,
1371 "isSubscribed": true,
1372 "includeInAvailability": "all",
1373 "timeZone": null,
1374 "myRights": {
1375 "mayReadFreeBusy": true,
1376 "mayReadItems": true,
1377 "mayWriteAll": true,
1378 "mayWriteOwn": true,
1379 "mayUpdatePrivate": true,
1380 "mayRSVP": true,
1381 "mayDelete": true,
1382 "mayAdmin": true
1383 },
1384 "shareWith": null
1385 }
1386 ],
1387 "notFound": [],
1388 "accountId": "user1"
1389 },
1390 "0"
1391 ]
1392 ],
1393 "sessionState": "0"
1394}
1395"#;
1396
1397 let expected = vec![Calendar {
1398 id: "Default".into(),
1399 name: "personal".into(),
1400 description: None,
1401 color: None,
1402 sort_order: 0,
1403 }];
1404 let mut parsed_body: JmapResponse<GetResponse<Calendar>> =
1405 serde_json::from_slice(body.as_bytes()).unwrap();
1406 let method_response = parsed_body.method_responses.pop().unwrap();
1407 let got = method_response.1.into_result().unwrap().list;
1408
1409 assert_eq!(expected, got);
1410 }
1411
1412 #[test]
1413 fn parse_error_response_state_mismatch() {
1414 let body = serde_json::json!( {
1415 "methodResponses": [
1416 [ "error", { "type": "stateMismatch" }, "0" ]
1417 ],
1418 "sessionState": "0"
1419 })
1420 .to_string();
1421
1422 let parsed_body: JmapResponse<SetResponse<JsonValue>> =
1423 serde_json::from_str(&body).unwrap();
1424
1425 assert_eq!(parsed_body.method_responses.len(), 1);
1426 let method_response = &parsed_body.method_responses[0];
1427
1428 assert_eq!(method_response.0, "error");
1429 assert_eq!(method_response.2, "0");
1430
1431 match &method_response.1 {
1432 MethodResponseData::Error(error) => {
1433 assert_eq!(error.error_type, "stateMismatch");
1434 assert_eq!(error.properties, None);
1435 assert_eq!(error.description, None);
1436 }
1437 MethodResponseData::Success(_) => panic!("Expected error response, got success"),
1438 }
1439 }
1440
1441 #[test]
1442 fn parse_error_response_unknown_method() {
1443 let body = serde_json::json!( {
1444 "methodResponses": [
1445 [ "error", { "type": "unknownMethod" }, "call-id" ]
1446 ],
1447 "sessionState": "0"
1448 })
1449 .to_string();
1450
1451 let parsed_body: JmapResponse<JsonValue> = serde_json::from_str(&body).unwrap();
1452
1453 assert_eq!(parsed_body.method_responses.len(), 1);
1454 let method_response = &parsed_body.method_responses[0];
1455
1456 assert_eq!(method_response.0, "error");
1457 assert_eq!(method_response.2, "call-id");
1458
1459 match &method_response.1 {
1460 MethodResponseData::Error(error) => {
1461 assert_eq!(error.error_type, "unknownMethod");
1462 }
1463 MethodResponseData::Success(_) => panic!("Expected error response, got success"),
1464 }
1465 }
1466}