/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::utils::account::Account; use base64::{Engine, engine::general_purpose}; use hyper::header; use jmap_proto::error::set::SetErrorType; use registry::types::error::ValidationError; use registry::types::id::ObjectId; use serde_json::{Value, json}; use std::{fmt::Display, str::FromStr, time::Duration}; use types::id::Id; pub struct JmapResponse(pub Value); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ChangeType<'x> { Created(&'x str), Updated(&'x str), Destroyed(&'x str), } impl Account { pub async fn jmap_get( &self, object: impl Display, properties: impl IntoIterator, ids: impl IntoIterator, ) -> JmapResponse { self.jmap_get_account(self, object, properties, ids).await } pub async fn jmap_get_account( &self, account: &Account, object: impl Display, properties: impl IntoIterator, ids: impl IntoIterator, ) -> JmapResponse { let ids = ids .into_iter() .map(|id| Value::String(id.to_string())) .collect::>(); if account.id().document_id() != u32::MAX { self.jmap_method_calls(json!([[ format!("{object}/get"), { "accountId": account.id_string(), "properties": properties .into_iter() .map(|p| Value::String(p.to_string())) .collect::>(), "ids": if !ids.is_empty() { Some(ids) } else { None } }, "0" ]])) .await } else { self.jmap_method_calls(json!([[ format!("{object}/get"), { "properties": properties .into_iter() .map(|p| Value::String(p.to_string())) .collect::>(), "ids": if !ids.is_empty() { Some(ids) } else { None } }, "0" ]])) .await } } pub async fn jmap_query( &self, object: impl Display, filter: impl IntoIterator)>, sort_by: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { let filter = filter .into_iter() .map(|(k, v)| (k.to_string(), v.into())) .collect::>(); let sort_by = sort_by .into_iter() .map(|id| { json! ({ "property": id.to_string() }) }) .collect::>(); let arguments = [ ("filter".to_string(), Value::Object(filter)), ("sort".to_string(), Value::Array(sort_by)), ] .into_iter() .chain( arguments .into_iter() .map(|(k, v)| (k.to_string(), v.into())), ) .collect::>(); self.jmap_method_calls(json!([[format!("{object}/query"), arguments, "0"]])) .await } pub async fn jmap_create( &self, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { self.jmap_create_account(self, object, items, arguments) .await } pub async fn jmap_create_account( &self, account: &Account, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { let create = items .into_iter() .enumerate() .map(|(i, item)| (format!("i{i}"), item)) .collect::>(); let arguments = [ ( "accountId".to_string(), Value::String(account.id_string().to_string()), ), ("create".to_string(), Value::Object(create)), ] .into_iter() .chain( arguments .into_iter() .map(|(k, v)| (k.to_string(), v.into())), ) .collect::>(); self.jmap_method_calls(json!([[format!("{object}/set"), arguments, "0"]])) .await } pub async fn jmap_update( &self, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { self.jmap_update_account(self, object, items, arguments) .await } pub async fn jmap_update_account( &self, account: &Account, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { let update = items .into_iter() .map(|(i, item)| (i.to_string(), item)) .collect::>(); let arguments = [ ( "accountId".to_string(), Value::String(account.id_string().to_string()), ), ("update".to_string(), Value::Object(update)), ] .into_iter() .chain( arguments .into_iter() .map(|(k, v)| (k.to_string(), v.into())), ) .collect::>(); self.jmap_method_calls(json!([[format!("{object}/set"), arguments, "0"]])) .await } pub async fn jmap_destroy( &self, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { self.jmap_destroy_account(self, object, items, arguments) .await } pub async fn jmap_destroy_account( &self, account: &Account, object: impl Display, items: impl IntoIterator, arguments: impl IntoIterator)>, ) -> JmapResponse { let destroy = items .into_iter() .map(|id| Value::String(id.to_string())) .collect::>(); let arguments = [ ( "accountId".to_string(), Value::String(account.id_string().to_string()), ), ("destroy".to_string(), Value::Array(destroy)), ] .into_iter() .chain( arguments .into_iter() .map(|(k, v)| (k.to_string(), v.into())), ) .collect::>(); self.jmap_method_calls(json!([[format!("{object}/set"), arguments, "0"]])) .await } pub async fn jmap_copy( &self, from_account: &Account, to_account: &Account, object: impl Display, items: impl IntoIterator, on_success_destroy: bool, ) -> JmapResponse { self.jmap_method_calls(json!([[ format!("{object}/copy"), { "fromAccountId": from_account.id_string(), "accountId": to_account.id_string(), "onSuccessDestroyOriginal": on_success_destroy, "create": items .into_iter() .map(|(i, item)| (i.to_string(), item)).collect::>() }, "0" ]])) .await } pub async fn jmap_changes(&self, object: impl Display, state: impl Display) -> JmapResponse { self.jmap_method_calls(json!([[ format!("{object}/changes"), { "sinceState": state.to_string() }, "0" ]])) .await } pub async fn jmap_method_call(&self, method_name: &str, body: Value) -> JmapResponse { self.jmap_method_calls(json!([[method_name, body, "0"]])) .await } pub async fn jmap_method_calls(&self, calls: Value) -> JmapResponse { let mut headers = header::HeaderMap::new(); headers.insert( header::AUTHORIZATION, header::HeaderValue::from_str(&format!( "Basic {}", general_purpose::STANDARD.encode(format!("{}:{}", self.name(), self.secret())) )) .unwrap(), ); let body = json!({ "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:quota" ], "methodCalls": calls }); JmapResponse( serde_json::from_slice( &reqwest::Client::builder() .danger_accept_invalid_certs(true) .timeout(Duration::from_millis(5000)) .default_headers(headers) .build() .unwrap() .post(format!( "https://127.0.0.1:{}/jmap", self.http_listener_port )) .body(body.to_string()) .send() .await .unwrap() .bytes() .await .unwrap(), ) .unwrap(), ) } pub async fn jmap_session_object(&self) -> JmapResponse { let mut headers = header::HeaderMap::new(); headers.insert( header::AUTHORIZATION, header::HeaderValue::from_str(&format!( "Basic {}", general_purpose::STANDARD.encode(format!("{}:{}", self.name(), self.secret())) )) .unwrap(), ); JmapResponse( serde_json::from_slice( &reqwest::Client::builder() .danger_accept_invalid_certs(true) .timeout(Duration::from_millis(1000)) .default_headers(headers) .build() .unwrap() .get(format!( "https://127.0.0.1:{}/jmap/session", self.http_listener_port )) .send() .await .unwrap() .bytes() .await .unwrap(), ) .unwrap(), ) } pub async fn destroy_all_addressbooks(&self) { self.jmap_method_calls(json!([[ "AddressBook/get", { "ids" : (), "properties" : [ "id" ] }, "R1" ], [ "AddressBook/set", { "#destroy" : { "resultOf": "R1", "name": "AddressBook/get", "path": "/list/*/id" }, "onDestroyRemoveContents" : true }, "R2" ] ])) .await; } pub async fn destroy_all_calendars(&self) { self.jmap_method_calls(json!([[ "Calendar/get", { "ids" : (), "properties" : [ "id" ] }, "R1" ], [ "Calendar/set", { "#destroy" : { "resultOf": "R1", "name": "Calendar/get", "path": "/list/*/id" }, "onDestroyRemoveEvents" : true }, "R2" ] ])) .await; } pub async fn destroy_all_event_notifications(&self) { self.jmap_method_calls(json!([[ "CalendarEventNotification/get", { "ids" : (), "properties" : [ "id" ] }, "R1" ], [ "CalendarEventNotification/set", { "#destroy" : { "resultOf": "R1", "name": "CalendarEventNotification/get", "path": "/list/*/id" } }, "R2" ] ])) .await; } } impl JmapResponse { pub fn created(&self, item_idx: u32) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/created/i{item_idx}")) .unwrap_or_else(|| panic!("Missing created item {item_idx}: {self:?}")) } pub fn created_id(&self, item_idx: u32) -> Id { Id::from_str(self.created(item_idx).id()).unwrap_or_else(|_| { panic!("Created item {item_idx} does not have a valid id: {self:?}") }) } pub fn not_created(&self, item_idx: u32) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/notCreated/i{item_idx}")) .unwrap_or_else(|| panic!("Missing not created item {item_idx}: {self:?}")) } pub fn updated(&self, id: &str) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/updated/{id}")) .unwrap_or_else(|| panic!("Missing updated item {id}: {self:?}")) } pub fn updated_id(&self, id: Id) -> &Value { self.updated(&id.to_string()) } pub fn not_updated(&self, id: &str) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/notUpdated/{id}")) .unwrap_or_else(|| panic!("Missing not updated item {id}: {self:?}")) } pub fn copied(&self, id: &str) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/created/{id}")) .unwrap_or_else(|| panic!("Missing created item {id}: {self:?}")) } pub fn method_response(&self) -> &Value { self.0 .pointer("/methodResponses/0/1") .unwrap_or_else(|| panic!("Missing method response in response: {self:?}")) } pub fn list_array(&self) -> &Value { self.0 .pointer("/methodResponses/0/1/list") .unwrap_or_else(|| panic!("Missing list in response: {self:?}")) } pub fn list(&self) -> &[Value] { self.0 .pointer("/methodResponses/0/1/list") .and_then(|v| v.as_array()) .unwrap_or_else(|| panic!("Missing list in response: {self:?}")) } pub fn not_found(&self) -> impl Iterator { self.0 .pointer("/methodResponses/0/1/notFound") .and_then(|v| v.as_array()) .unwrap_or_else(|| panic!("Missing notFound in response: {self:?}")) .iter() .map(|v| v.as_str().unwrap()) } pub fn ids(&self) -> impl Iterator { self.0 .pointer("/methodResponses/0/1/ids") .and_then(|v| v.as_array()) .unwrap_or_else(|| panic!("Missing ids in response: {self:?}")) .iter() .map(|v| v.as_str().unwrap()) } pub fn object_ids(&self) -> impl Iterator { self.ids().map(move |id| { Id::from_str(id).unwrap_or_else(|_| panic!("Invalid id {id} in response: {self:?}")) }) } pub fn destroyed(&self) -> impl Iterator { self.0 .pointer("/methodResponses/0/1/destroyed") .and_then(|v| v.as_array()) .unwrap_or_else(|| panic!("Missing destroyed in response: {self:?}")) .iter() .map(|v| v.as_str().unwrap()) } pub fn destroyed_ids(&self) -> impl Iterator { self.destroyed().map(move |id| { Id::from_str(id).unwrap_or_else(|_| panic!("Invalid id {id} in response: {self:?}")) }) } pub fn assert_destroyed(&self, expected: &[Id]) -> &Self { let destroyed_ids = self.destroyed_ids().collect::>(); for expected in expected { if !destroyed_ids.contains(expected) { panic!( "Expected id {expected} to be destroyed but got destroyed ids {destroyed_ids:?}: {self:?}" ); } } self } pub fn not_destroyed(&self, id: &str) -> &Value { self.0 .pointer(&format!("/methodResponses/0/1/notDestroyed/{id}")) .unwrap_or_else(|| panic!("Missing not destroyed item {id}: {self:?}")) } pub fn state(&self) -> &str { self.0 .pointer("/methodResponses/0/1/state") .and_then(|v| v.as_str()) .unwrap_or_else(|| panic!("Missing state in response: {self:?}")) } pub fn new_state(&self) -> &str { self.0 .pointer("/methodResponses/0/1/newState") .and_then(|v| v.as_str()) .unwrap_or_else(|| panic!("Missing new state in response: {self:?}")) } pub fn changes(&self) -> impl Iterator> { self.changes_by_type("created") .map(ChangeType::Created) .chain(self.changes_by_type("updated").map(ChangeType::Updated)) .chain(self.changes_by_type("destroyed").map(ChangeType::Destroyed)) } fn changes_by_type(&self, typ: &str) -> impl Iterator { self.0 .pointer(&format!("/methodResponses/0/1/{typ}")) .and_then(|v| v.as_array()) .unwrap_or_else(|| panic!("Missing {typ} changes in response: {self:?}")) .iter() .map(|v| v.as_str().unwrap()) } pub fn pointer(&self, pointer: &str) -> Option<&Value> { self.0.pointer(pointer) } pub fn into_inner(self) -> Value { self.0 } } #[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct JmapSetError { #[serde(rename = "type")] pub type_: SetErrorType, #[serde(default)] pub description: Option, #[serde(default)] pub properties: Option>, #[serde(rename = "existingId")] #[serde(default)] pub existing_id: Option, #[serde(rename = "objectId")] #[serde(default)] pub object_id: Option, #[serde(default)] #[serde(rename = "linkedObjects")] pub linked_objects: Vec, #[serde(default)] #[serde(rename = "validationErrors")] pub validation_errors: Vec, } impl JmapSetError { pub fn assert_type(&self, expected: SetErrorType) -> &Self { if self.type_ != expected { panic!("Expected error type {expected:?} but got {self:?}"); } self } pub fn assert_description_contains(&self, expected: &str) -> &Self { if let Some(description) = &self.description { if !description.contains(expected) { panic!("Expected error description to contain {expected} but got {description}"); } } else { panic!("Expected error description to contain {expected} but got no description"); } self } pub fn assert_properties(&self, expected: &[&str]) -> &Self { let properties = self.properties.as_ref().unwrap_or_else(|| { panic!("Expected error to have properties {expected:?} but got no properties: {self:?}") }); for expected in expected { if !properties.contains(&expected.to_string()) { panic!( "Expected error to have property {expected} but got properties {properties:?}: {self:?}" ); } } self } } pub trait JmapUtils { fn id(&self) -> &str { self.text_field("id") } fn object_id(&self) -> Id { self.id() .parse() .unwrap_or_else(|_| panic!("Invalid id {} in object", self.id())) } fn blob_id(&self) -> &str { self.text_field("blobId") } fn typ(&self) -> &str { self.text_field("type") } fn description(&self) -> &str { self.text_field("description") } fn to_set_error(&self) -> JmapSetError; fn with_property(self, field: impl Display, value: impl Into) -> Self; fn text_field(&self, field: &str) -> &str; fn integer_field(&self, field: &str) -> i64; fn assert_is_equal(&self, other: Value); } impl JmapUtils for Value { fn text_field(&self, field: &str) -> &str { self.pointer(&format!("/{field}")) .and_then(|v| v.as_str()) .unwrap_or_else(|| panic!("Missing {field} in object: {self:?}")) } fn integer_field(&self, field: &str) -> i64 { self.pointer(&format!("/{field}")) .and_then(|v| v.as_i64()) .unwrap_or_else(|| panic!("Missing {field} in object: {self:?}")) } fn to_set_error(&self) -> JmapSetError { serde_json::from_str(&self.to_string()).expect("Failed to deserialize set error") } fn assert_is_equal(&self, expected: Value) { if self != &expected { panic!( "Values are not equal:\ngot: {}\nexpected: {}", serde_json::to_string_pretty(self).unwrap(), serde_json::to_string_pretty(&expected).unwrap() ); } } fn with_property(mut self, field: impl Display, value: impl Into) -> Self { if let Value::Object(map) = &mut self { map.insert(field.to_string(), value.into()); } else { panic!("Not an object: {self:?}"); } self } } impl<'x> ChangeType<'x> { pub fn as_created(&self) -> &str { match self { ChangeType::Created(id) => id, _ => panic!("Not a created change: {self:?}"), } } pub fn as_updated(&self) -> &str { match self { ChangeType::Updated(id) => id, _ => panic!("Not an updated change: {self:?}"), } } pub fn as_destroyed(&self) -> &str { match self { ChangeType::Destroyed(id) => id, _ => panic!("Not a destroyed change: {self:?}"), } } } impl Display for JmapResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) } } impl std::fmt::Debug for JmapResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { serde_json::to_string_pretty(&self.0) .map_err(|_| std::fmt::Error) .and_then(|s| std::fmt::Display::fmt(&s, f)) } } pub trait IntoJmapSet { fn into_jmap_set(self) -> Value; } impl> IntoJmapSet for T { fn into_jmap_set(self) -> Value { Value::Object( self.into_iter() .map(|id| (id.to_string(), Value::Bool(true))) .collect::>(), ) } }