/* * meli - jmap module. * * Copyright 2019 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ use std::{ hash::{Hash, Hasher}, marker::PhantomData, sync::Arc, }; use indexmap::IndexMap; use serde::{ de::DeserializeOwned, ser::{Serialize, SerializeStruct, Serializer}, }; use serde_json::{value::RawValue, Value}; use url::Url; use crate::{ email::parser::BytesExt, error::{Error, ErrorKind, Result}, jmap::{deserialize_from_str, protocol::Method, session::Session}, }; mod filters; pub use filters::*; mod comparator; pub use comparator::*; mod argument; pub use argument::*; #[cfg(test)] mod tests; pub type PatchObject = Value; impl Object for PatchObject { const NAME: &'static str = "PatchObject"; } pub trait Object: Send + Sync { const NAME: &'static str; } #[derive(Deserialize, Serialize)] #[serde(transparent)] pub struct Id { pub inner: String, #[serde(skip)] pub _ph: PhantomData OBJ>, } impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple(&format!("Id<{}>", OBJ::NAME)) .field(&self.inner) .finish() } } impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple("Id").field(&self.inner).finish() } } //, Hash, Eq, PartialEq, Default)] impl Clone for Id { fn clone(&self) -> Self { Self { inner: self.inner.clone(), _ph: PhantomData, } } } impl std::cmp::Eq for Id {} impl std::cmp::PartialEq for Id { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl Hash for Id { fn hash(&self, state: &mut H) { self.inner.hash(state); } } impl Default for Id { fn default() -> Self { Self::empty() } } impl From for Id { fn from(inner: String) -> Self { Self { inner, _ph: PhantomData, } } } impl From<&str> for Id { fn from(inner: &str) -> Self { Self { inner: inner.to_string(), _ph: PhantomData, } } } impl std::fmt::Display for Id { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt(&self.inner, fmt) } } impl Id { pub fn empty() -> Self { Self { inner: String::new(), _ph: PhantomData, } } pub fn new_uuid_v4() -> Self { Self { inner: uuid::Uuid::new_v4().hyphenated().to_string(), _ph: PhantomData, } } pub fn as_str(&self) -> &str { self.inner.as_str() } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } } #[derive(Debug, Deserialize, Serialize)] #[serde(transparent)] pub struct State { pub inner: String, #[serde(skip)] pub _ph: PhantomData OBJ>, } //, Hash, Eq, PartialEq, Default)] impl Clone for State { fn clone(&self) -> Self { Self { inner: self.inner.clone(), _ph: PhantomData, } } } impl std::cmp::Eq for State {} impl std::cmp::PartialEq for State { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl Hash for State { fn hash(&self, state: &mut H) { self.inner.hash(state); } } impl Default for State { fn default() -> Self { Self::new() } } impl From for State { fn from(inner: String) -> Self { Self { inner, _ph: PhantomData, } } } impl std::fmt::Display for State { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt(&self.inner, fmt) } } impl State { pub fn new() -> Self { Self { inner: String::new(), _ph: PhantomData, } } pub fn as_str(&self) -> &str { self.inner.as_str() } pub fn len(&self) -> usize { self.inner.len() } pub fn is_empty(&self) -> bool { self.inner.is_empty() } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Account { pub name: String, pub is_personal: bool, pub is_read_only: bool, pub account_capabilities: IndexMap, #[serde(flatten)] pub extra_properties: IndexMap, } impl Object for Account { const NAME: &'static str = stringify!(Account); } #[derive(Clone, Copy, Debug)] pub struct BlobObject; impl Object for BlobObject { const NAME: &'static str = "Blob"; } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct BlobGet; impl Method for BlobGet { const NAME: &'static str = "Blob/get"; } /// #`get` /// /// Objects of type `Foo` are fetched via a call to `Foo/get`. /// /// It takes the following arguments: /// /// - `account_id`: `Id` /// /// The id of the account to use. #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Get where OBJ: Object + std::fmt::Debug + Serialize, { pub account_id: Id, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] pub ids: Option>>>, #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>, #[serde(skip)] _ph: PhantomData OBJ>, } impl Get where OBJ: Object + std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: Id::empty(), ids: None, properties: None, _ph: PhantomData, } } _impl!( /// - accountId: `Id` /// /// The id of the account to use. account_id: Id ); _impl!( /// - ids: `Option>>` /// /// The ids of the Foo objects to return. If `None`, then *all* /// records of the data type are returned, if this is /// supported for that data type and the number of records /// does not exceed the `max_objects_in_get` limit. ids: Option>>> ); _impl!( /// - properties: `Option>` /// /// If supplied, only the properties listed in the array are /// returned for each `Foo` object. If `None`, all /// properties of the object are returned. The `id` /// property of the object is *always* returned, even if /// not explicitly requested. If an invalid property is /// requested, the call WILL be rejected with an /// `invalid_arguments` error. properties: Option> ); } impl Default for Get where OBJ: Object + std::fmt::Debug + Serialize, { fn default() -> Self { Self::new() } } impl Serialize for Get { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { let mut fields_no = 0; if !self.account_id.is_empty() { fields_no += 1; } if self.ids.is_some() { fields_no += 1; } if self.properties.is_some() { fields_no += 1; } let mut state = serializer.serialize_struct("Get", fields_no)?; if !self.account_id.is_empty() { state.serialize_field("accountId", &self.account_id)?; } match self.ids.as_ref() { None => {} Some(Argument::Value(ref v)) => state.serialize_field("ids", v)?, Some(Argument::ResultReference { ref result_of, ref name, ref path, .. }) => { #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct A<'a> { result_of: &'a str, name: &'a str, path: &'a str, } state.serialize_field( "#ids", &A { result_of, name, path, }, )?; } } if self.properties.is_some() { state.serialize_field("properties", self.properties.as_ref().unwrap())?; } state.end() } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MethodResponse<'a> { #[serde(borrow)] pub method_responses: Vec<&'a RawValue>, #[serde(default)] pub created_ids: IndexMap, Id>, #[serde(default)] pub session_state: State, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetResponse { pub account_id: Id, #[serde(default = "State::default")] pub state: State, pub list: Vec, pub not_found: Vec>, } impl std::convert::TryFrom<&RawValue> for GetResponse { type Error = crate::error::Error; fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/get", OBJ::NAME)); Ok(res.1) } } impl GetResponse { _impl!(get_mut account_id_mut, account_id: Id); _impl!(get_mut state_mut, state: State); _impl!(get_mut list_mut, list: Vec); _impl!(get_mut not_found_mut, not_found: Vec>); } #[derive(Clone, Copy, Debug, Deserialize)] #[serde(rename_all = "camelCase")] enum JmapError { RequestTooLarge, InvalidArguments, InvalidResultReference, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Query, OBJ> where OBJ: Object + std::fmt::Debug + Serialize, { pub account_id: Id, pub filter: Option, pub sort: Option>, #[serde(default)] pub position: u64, #[serde(skip_serializing_if = "Option::is_none")] pub anchor: Option, #[serde(default)] #[serde(skip_serializing_if = "u64_zero")] pub anchor_offset: u64, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, #[serde(default = "bool_false")] pub calculate_total: bool, #[serde(skip)] _ph: PhantomData OBJ>, } impl, OBJ> Query where OBJ: Object + std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: Id::empty(), filter: None, sort: None, position: 0, anchor: None, anchor_offset: 0, limit: None, calculate_total: false, _ph: PhantomData, } } _impl!(account_id: Id); _impl!(filter: Option); _impl!(sort: Option>); _impl!(position: u64); _impl!(anchor: Option); _impl!(anchor_offset: u64); _impl!(limit: Option); _impl!(calculate_total: bool); } impl, OBJ> Default for Query where OBJ: Object + std::fmt::Debug + Serialize, { fn default() -> Self { Self::new() } } pub fn u64_zero(num: &u64) -> bool { *num == 0 } pub fn bool_false() -> bool { false } pub fn bool_true() -> bool { true } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueryResponse { pub account_id: Id, pub query_state: String, pub can_calculate_changes: bool, pub position: u64, pub ids: Vec>, #[serde(default)] pub total: u64, #[serde(default)] pub limit: u64, #[serde(skip)] _ph: PhantomData OBJ>, } impl std::convert::TryFrom<&RawValue> for QueryResponse { type Error = crate::error::Error; fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/query", OBJ::NAME)); Ok(res.1) } } impl QueryResponse { _impl!(get_mut ids_mut, ids: Vec>); } pub struct ResultField, OBJ: Object> { pub field: &'static str, pub _ph: PhantomData (OBJ, M)>, } impl, OBJ: Object> ResultField { pub const fn new(field: &'static str) -> Self { Self { field, _ph: PhantomData, } } } impl, OBJ: Object> From<&'static str> for ResultField { fn from(field: &'static str) -> Self { Self::new(field) } } /// #`changes` /// /// The `Foo/changes` method allows a client to efficiently update the state /// of its Foo cache to match the new state on the server. It takes the /// following arguments: /// /// - accountId: `Id` The id of the account to use. /// - sinceState: `String` /// The current state of the client. This is the string that was /// returned as the `state` argument in the `Foo/get` response. The /// server will return the changes that have occurred since this /// state. /// /// - maxChanges: `UnsignedInt|null` /// The maximum number of ids to return in the response. The server /// MAY choose to return fewer than this value but MUST NOT return /// more. If not given by the client, the server may choose how many /// to return. If supplied by the client, the value MUST be a /// positive integer greater than 0. If a value outside of this range /// is given, the server MUST re #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] /* ch-ch-ch-ch-ch-Changes */ pub struct Changes where OBJ: Object + std::fmt::Debug + Serialize, { pub account_id: Id, pub since_state: State, #[serde(skip_serializing_if = "Option::is_none")] pub max_changes: Option, #[serde(skip)] _ph: PhantomData OBJ>, } impl Changes where OBJ: Object + std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: Id::empty(), since_state: State::new(), max_changes: None, _ph: PhantomData, } } _impl!( /// - accountId: `Id` /// /// The id of the account to use. account_id: Id ); _impl!( /// - since_state: `String` /// The current state of the client. This is the string that was /// returned as the `state` argument in the `Foo/get` response. The /// server will return the changes that have occurred since this /// state. since_state: State ); _impl!( /// - max_changes: `UnsignedInt|null` /// The maximum number of ids to return in the response. The server /// MAY choose to return fewer than this value but MUST NOT return /// more. If not given by the client, the server may choose how many /// to return. If supplied by the client, the value MUST be a /// positive integer greater than 0. If a value outside of this /// range is given, the server MUST re max_changes: Option ); } impl Default for Changes where OBJ: Object + std::fmt::Debug + Serialize, { fn default() -> Self { Self::new() } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangesResponse { pub account_id: Id, pub old_state: State, pub new_state: State, pub has_more_changes: bool, pub created: Vec>, pub updated: Vec>, pub destroyed: Vec>, #[serde(skip)] pub _ph: PhantomData OBJ>, } impl std::convert::TryFrom<&RawValue> for ChangesResponse { type Error = crate::error::Error; fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME)); Ok(res.1) } } impl ChangesResponse { _impl!(get_mut account_id_mut, account_id: Id); _impl!(get_mut old_state_mut, old_state: State); _impl!(get_mut new_state_mut, new_state: State); _impl!(get has_more_changes, has_more_changes: bool); _impl!(get_mut created_mut, created: Vec>); _impl!(get_mut updated_mut, updated: Vec>); _impl!(get_mut destroyed_mut, destroyed: Vec>); } /// #`set` /// /// Modifying the state of Foo objects on the server is done via the /// `Foo/set` method. This encompasses creating, updating, and /// destroying Foo records. This allows the server to sort out ordering /// and dependencies that may exist if doing multiple operations at once /// (for example, to ensure there is always a minimum number of a certain /// record type). #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Set where OBJ: Object + std::fmt::Debug + Serialize, { /// o accountId: `Id` /// /// The id of the account to use. pub account_id: Id, /// o ifInState: `String|null` /// /// This is a state string as returned by the `Foo/get` method /// (representing the state of all objects of this type in the /// account). If supplied, the string must match the current state; /// otherwise, the method will be aborted and a `stateMismatch` error /// returned. If null, any changes will be applied to the current /// state. pub if_in_state: Option>, /// o create: `Id[Foo]|null` /// /// A map of a *creation id* (a temporary id set by the client) to Foo /// objects, or null if no objects are to be created. /// /// The Foo object type definition may define default values for /// properties. Any such property may be omitted by the client. /// /// The client MUST omit any properties that may only be set by the /// server (for example, the `id` property on most object types). pub create: Option>, OBJ>>, /// o update: `Id[PatchObject]|null` /// /// A map of an id to a Patch object to apply to the current Foo /// object with that id, or null if no objects are to be updated. /// /// A *PatchObject* is of type `String[*]` and represents an unordered /// set of patches. The keys are a path in JSON Pointer format /// `RFC6901`, with an implicit leading `/` (i.e., prefix each key /// with `/` before applying the JSON Pointer evaluation algorithm). /// /// All paths MUST also conform to the following restrictions; if /// there is any violation, the update MUST be rejected with an /// `invalidPatch` error: /// * The pointer MUST NOT reference inside an array (i.e., you MUST NOT /// insert/delete from an array; the array MUST be replaced in its /// entirety instead). /// /// * All parts prior to the last (i.e., the value after the final slash) /// MUST already exist on the object being patched. /// /// * There MUST NOT be two patches in the PatchObject where the pointer /// of one is the prefix of the pointer of the other, e.g., /// `alerts/1/offset` and `alerts`. /// /// The value associated with each pointer determines how to apply /// that patch: /// /// * If null, set to the default value if specified for this property; /// otherwise, remove the property from the patched object. If the key /// is not present in the parent, this a no-op. /// /// * Anything else: The value to set for this property (this may be a /// replacement or addition to the object being patched). /// /// Any server-set properties MAY be included in the patch if their /// value is identical to the current server value (before applying /// the patches to the object). Otherwise, the update MUST be /// rejected with an `invalidProperties` SetError. /// /// This patch definition is designed such that an entire Foo object /// is also a valid PatchObject. The client may choose to optimise /// network usage by just sending the diff or may send the whole /// object; the server processes it the same either way. pub update: Option>, PatchObject>>, /// o destroy: `Id[]|null` /// /// A list of ids for Foo objects to permanently delete, or null if no /// objects are to be destroyed. pub destroy: Option>>>, } impl Set where OBJ: Object + std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: Id::empty(), if_in_state: None, create: None, update: None, destroy: None, } } _impl!(account_id: Id); _impl!( /// o ifInState: `String|null` /// /// This is a state string as returned by the `Foo/get` method /// (representing the state of all objects of this type in the /// account). If supplied, the string must match the current state; /// otherwise, the method will be aborted and a `stateMismatch` error /// returned. If null, any changes will be applied to the current /// state. if_in_state: Option> ); _impl!(update: Option>, PatchObject>>); _impl!(create: Option>, OBJ>>); _impl!(destroy: Option>>>); } impl Default for Set where OBJ: Object + std::fmt::Debug + Serialize, { fn default() -> Self { Self::new() } } impl Serialize for Set { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { let fields_no = 5; let mut state = serializer.serialize_struct("Set", fields_no)?; state.serialize_field("accountId", &self.account_id)?; state.serialize_field("ifInState", &self.if_in_state)?; state.serialize_field("update", &self.update)?; state.serialize_field("destroy", &self.destroy)?; if let Some(ref m) = self.create { let map = m .into_iter() .map(|(k, v)| { let mut v = serde_json::json!(v); if let Some(ref mut obj) = v.as_object_mut() { obj.remove("id"); } (k, v) }) .collect::>(); state.serialize_field("create", &map)?; } else { state.serialize_field("create", &self.create)?; } state.end() } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SetResponse { /// o accountId: `Id` /// /// The id of the account used for the call. pub account_id: Id, /// o oldState: `String|null` /// /// The state string that would have been returned by `Foo/get` before /// making the requested changes, or null if the server doesn't know /// what the previous state string was. pub old_state: Option>, /// o newState: `String` /// /// The state string that will now be returned by `Foo/get`. pub new_state: State, /// o created: `Id[Foo]|null` /// /// A map of the creation id to an object containing any properties of /// the created Foo object that were not sent by the client. This /// includes all server-set properties (such as the `id` in most /// object types) and any properties that were omitted by the client /// and thus set to a default by the server. /// /// This argument is null if no Foo objects were successfully created. pub created: Option, OBJ>>, /// o updated: `Id[Foo|null]|null` /// /// The keys in this map are the ids of all Foos that were /// successfully updated. /// /// The value for each id is a Foo object containing any property that /// changed in a way *not* explicitly requested by the PatchObject /// sent to the server, or null if none. This lets the client know of /// any changes to server-set or computed properties. /// /// This argument is null if no Foo objects were successfully updated. pub updated: Option, Option>>, /// o destroyed: `Id[]|null` /// /// A list of Foo ids for records that were successfully destroyed, or /// null if none. pub destroyed: Option>>, /// o notCreated: `Id[SetError]|null` /// /// A map of the creation id to a SetError object for each record that /// failed to be created, or null if all successful. pub not_created: Option>, /// o notUpdated: `Id[SetError]|null` /// /// A map of the Foo id to a SetError object for each record that /// failed to be updated, or null if all successful. pub not_updated: Option>, /// o notDestroyed: `Id[SetError]|null` /// /// A map of the Foo id to a SetError object for each record that /// failed to be destroyed, or null if all successful.// pub not_destroyed: Option>, } impl std::convert::TryFrom<&RawValue> for SetResponse { type Error = crate::error::Error; fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/set", OBJ::NAME)); Ok(res.1) } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[serde(tag = "type", content = "description")] pub enum SetError { /// (create; update; destroy). The create/update/destroy would violate an /// ACL or other permissions policy. Forbidden(Option), /// (create; update). The create would exceed a server- defined limit on /// the number or total size of objects of this type. OverQuota(Option), /// (create; update). The create/update would result in an object that /// exceeds a server-defined limit for the maximum size of a single object /// of this type. TooLarge(Option), /// (create). Too many objects of this type have been created recently, and /// a server-defined rate limit has been reached. It may work if tried /// again later. RateLimit(Option), /// (update; destroy). The id given to update/destroy cannot be found. NotFound(Option), /// (update). The PatchObject given to update the record was not a valid /// patch (see the patch description). InvalidPatch(Option), /// (update). The client requested that an object be both updated and /// destroyed in the same /set request, and the server has decided to /// therefore ignore the update. WillDestroy(Option), /// (create; update). The record given is invalid in some way. InvalidProperties { description: Option, properties: Vec, }, /// (create; destroy). This is a singleton type, so you cannot create /// another one or destroy the existing one. Singleton(Option), RequestTooLarge(Option), StateMismatch(Option), } impl std::fmt::Display for SetError { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { use SetError::*; match self { Forbidden(Some(description)) => write!(fmt, "Forbidden: {}", description), Forbidden(None) => write!(fmt, "Forbidden"), OverQuota(Some(description)) => write!(fmt, "OverQuota: {}", description), OverQuota(None) => write!(fmt, "OverQuota"), TooLarge(Some(description)) => write!(fmt, "TooLarge: {}", description), TooLarge(None) => write!(fmt, "TooLarge"), RateLimit(Some(description)) => write!(fmt, "RateLimit: {}", description), RateLimit(None) => write!(fmt, "RateLimit"), NotFound(Some(description)) => write!(fmt, "NotFound: {}", description), NotFound(None) => write!(fmt, "NotFound"), InvalidPatch(Some(description)) => write!(fmt, "InvalidPatch: {}", description), InvalidPatch(None) => write!(fmt, "InvalidPatch"), WillDestroy(Some(description)) => write!(fmt, "WillDestroy: {}", description), WillDestroy(None) => write!(fmt, "WillDestroy"), InvalidProperties { description: Some(description), properties, } => write!( fmt, "InvalidProperties: {}, {}", description, properties.join(",") ), InvalidProperties { description: None, properties, } => write!(fmt, "InvalidProperties: {}", properties.join(",")), Singleton(Some(description)) => write!(fmt, "Singleton: {}", description), Singleton(None) => write!(fmt, "Singleton"), RequestTooLarge(Some(description)) => write!(fmt, "RequestTooLarge: {}", description), RequestTooLarge(None) => write!(fmt, "RequestTooLarge"), StateMismatch(Some(description)) => write!(fmt, "StateMismatch: {}", description), StateMismatch(None) => write!(fmt, "StateMismatch"), } } } pub fn download_request_format( download_url: &Url, account_id: &Id, blob_id: &Id, name: Option, ) -> Result { // https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} let mut ret = String::with_capacity( download_url.as_str().len() + blob_id.len() + name.as_ref().map(|n| n.len()).unwrap_or(0) + account_id.len(), ); let mut prev_pos = 0; while let Some(pos) = download_url.as_str().as_bytes()[prev_pos..].find(b"{") { ret.push_str(&download_url.as_str()[prev_pos..prev_pos + pos]); prev_pos += pos; if download_url.as_str()[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); } else if download_url.as_str()[prev_pos..].starts_with("{blobId}") { ret.push_str(blob_id.as_str()); prev_pos += "{blobId}".len(); } else if download_url.as_str()[prev_pos..].starts_with("{name}") { ret.push_str(name.as_deref().unwrap_or("")); prev_pos += "{name}".len(); } else if download_url.as_str()[prev_pos..].starts_with("{type}") { ret.push_str("application/octet-stream"); prev_pos += "{name}".len(); } else { log::error!( "BUG: unknown parameter in download_url: {}", &download_url.as_str()[prev_pos..] ); return Err(Error::new( "Could not instantiate URL from JMAP server's URL template value", ) .set_details(format!( "`download_url` template returned by server in session object could not be \ instantiated with `accountId`:\ndownload_url: {}\naccountId: {}\nblobId: \ {}\nUnknown parameter found {}\n\nIf you believe these values are correct and \ should have been accepted, please report it as a bug! Otherwise inform the \ server administrator for this protocol violation.", download_url, account_id, blob_id, &download_url.as_str()[prev_pos..] )) .set_kind(ErrorKind::ProtocolError)); } } if prev_pos != download_url.as_str().len() { ret.push_str(&download_url.as_str()[prev_pos..]); } Url::parse(&ret).map_err(|err| { Error::new("Could not instantiate URL from JMAP server's URL template value") .set_details(format!( "`download_url` template returned by server in session object could not be \ instantiated with `accountId`:\ndownload_url: {}\naccountId: {}\nblobId: \ {}\nresult: {ret}\n\nIf you believe these values are correct and should have \ been accepted, please report it as a bug! Otherwise inform the server \ administrator for this protocol violation.", download_url, account_id, blob_id )) .set_kind(ErrorKind::ProtocolError) .set_source(Some(Arc::new(err))) }) } pub fn upload_request_format(upload_url: &Url, account_id: &Id) -> Result { //"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", let mut ret = String::with_capacity(upload_url.as_str().len() + account_id.len()); let mut prev_pos = 0; while let Some(pos) = upload_url.as_str().as_bytes()[prev_pos..].find(b"{") { ret.push_str(&upload_url.as_str()[prev_pos..prev_pos + pos]); prev_pos += pos; if upload_url.as_str()[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); break; } else { ret.push('{'); prev_pos += 1; } } if prev_pos != upload_url.as_str().len() { ret.push_str(&upload_url.as_str()[prev_pos..]); } Url::parse(&ret).map_err(|err| { Error::new("Could not instantiate URL from JMAP server's URL template value") .set_details(format!( "`upload_url` template returned by server in session object could not be \ instantiated with `accountId`:\nupload_url: {}\naccountId: {}\nresult: \ {ret}\n\nIf you believe these values are correct and should have been accepted, \ please report it as a bug! Otherwise inform the server administrator for this \ protocol violation.", upload_url, account_id )) .set_kind(ErrorKind::ProtocolError) .set_source(Some(Arc::new(err))) }) } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct UploadResponse { /// o accountId: `Id` /// /// The id of the account used for the call. pub account_id: Id, /// o blobId: `Id` /// /// The id representing the binary data uploaded. The data for this id is /// immutable. The id *only* refers to the binary data, not any /// metadata. pub blob_id: Id, /// o type: `String` /// /// The media type of the file (as specified in `RFC6838`, /// Section 4.2) as set in the Content-Type header of the upload HTTP /// request. #[serde(rename = "type")] pub _type: String, /// o size: `UnsignedInt` /// /// The size of the file in octets. pub size: usize, } /// #`queryChanges` /// /// The `Foo/queryChanges` method allows a client to efficiently update /// the state of a cached query to match the new state on the server. It /// takes the following arguments: #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueryChanges, OBJ> where OBJ: Object + std::fmt::Debug + Serialize, { pub account_id: Id, pub filter: Option, pub sort: Option>, /// sinceQueryState: `String` /// /// The current state of the query in the client. This is the string /// that was returned as the `queryState` argument in the `Foo/query` /// response with the same sort/filter. The server will return the /// changes made to the query since this state. pub since_query_state: String, /// o maxChanges: `UnsignedInt|null` /// /// The maximum number of changes to return in the response. See /// error descriptions below for more details. pub max_changes: Option, /// o upToId: `Id|null` /// /// The last (highest-index) id the client currently has cached from /// the query results. When there are a large number of results, in a /// common case, the client may have only downloaded and cached a /// small subset from the beginning of the results. If the sort and /// filter are both only on immutable properties, this allows the /// server to omit changes after this point in the results, which can /// significantly increase efficiency. If they are not immutable, /// this argument is ignored. pub up_to_id: Option>, /// o calculateTotal: `Boolean` (default: false) /// /// Does the client wish to know the total number of results now in /// the query? This may be slow and expensive for servers to /// calculate, particularly with complex filters, so clients should /// take care to only request the total when needed. #[serde(default = "bool_false")] pub calculate_total: bool, #[serde(skip)] _ph: PhantomData OBJ>, } impl, OBJ> QueryChanges where OBJ: Object + std::fmt::Debug + Serialize, { pub fn new(account_id: Id, since_query_state: String) -> Self { Self { account_id, filter: None, sort: None, since_query_state, max_changes: None, up_to_id: None, calculate_total: false, _ph: PhantomData, } } _impl!(filter: Option); _impl!(sort: Option>); _impl!(max_changes: Option); _impl!(up_to_id: Option>); _impl!(calculate_total: bool); } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueryChangesResponse { /// The id of the account used for the call. pub account_id: Id, /// This is the `sinceQueryState` argument echoed back; that is, the state /// from which the server is returning changes. pub old_query_state: String, /// This is the state the query will be in after applying the set of changes /// to the old state. pub new_query_state: String, /// The total number of Foos in the results (given the `filter`). This /// argument MUST be omitted if the `calculateTotal` request argument is not /// true. #[serde(default)] pub total: Option, /// The `id` for every Foo that was in the query results in the old /// state and that is not in the results in the new state. /// If the server cannot calculate this exactly, the server MAY return /// the ids of extra Foos in addition that may have been in the old /// results but are not in the new results. /// If the sort and filter are both only on immutable properties and /// an `upToId` is supplied and exists in the results, any ids that /// were removed but have a higher index than `upToId` SHOULD be /// omitted. /// If the `filter` or `sort` includes a mutable property, the server /// MUST include all Foos in the current results for which this /// property may have changed. The position of these may have moved /// in the results, so they must be reinserted by the client to ensure /// its query cache is correct. pub removed: Vec>, /// The id and index in the query results (in the new state) for every /// Foo that has been added to the results since the old state AND /// every Foo in the current results that was included in the /// `removed` array (due to a filter or sort based upon a mutable /// property). /// If the sort and filter are both only on immutable properties and /// an `upToId` is supplied and exists in the results, any ids that /// were added but have a higher index than `upToId` SHOULD be /// omitted. /// The array MUST be sorted in order of index, with the lowest index /// first. /// An *AddedItem* object has the following properties: /// * id: `Id` /// * index: `UnsignedInt` /// The result of this is that if the client has a cached sparse array of /// Foo ids corresponding to the results in the old state, then: /// fooIds = [ `id1`, `id2`, null, null, `id3`, `id4`, null, null, null ] /// If it *splices out* all ids in the removed array that it has in its /// cached results, then: /// removed = [ `id2`, `id31`, ... ]; /// fooIds => [ `id1`, null, null, `id3`, `id4`, null, null, null ] /// and *splices in* (one by one in order, starting with the lowest /// index) all of the ids in the added array: /// added = [{ id: `id5`, index: 0, ... }]; /// fooIds => [ `id5`, `id1`, null, null, `id3`, `id4`, null, null, null ] /// and *truncates* or *extends* to the new total length, then the /// results will now be in the new state. /// Note: splicing in adds the item at the given index, incrementing the /// index of all items previously at that or a higher index. Splicing /// out is the inverse, removing the item and decrementing the index of /// every item after it in the array. pub added: Vec>, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AddedItem { pub id: Id, pub index: usize, }