/* * 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 super::Id; use crate::email::parser::BytesExt; use core::marker::PhantomData; use serde::de::DeserializeOwned; use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_json::{value::RawValue, Value}; mod filters; pub use filters::*; mod comparator; pub use comparator::*; mod argument; pub use argument::*; use super::protocol::Method; use std::collections::HashMap; pub trait Object { const NAME: &'static str; } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct JmapSession { pub capabilities: HashMap, pub accounts: HashMap, pub primary_accounts: HashMap, pub username: String, pub api_url: String, pub download_url: String, pub upload_url: String, pub event_source_url: String, pub state: String, #[serde(flatten)] pub extra_properties: HashMap, } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesObject { #[serde(default)] max_size_upload: u64, #[serde(default)] max_concurrent_upload: u64, #[serde(default)] max_size_request: u64, #[serde(default)] max_concurrent_requests: u64, #[serde(default)] max_calls_in_request: u64, #[serde(default)] max_objects_in_get: u64, #[serde(default)] max_objects_in_set: u64, #[serde(default)] collation_algorithms: Vec, } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Account { name: String, is_personal: bool, is_read_only: bool, account_capabilities: HashMap, #[serde(flatten)] extra_properties: HashMap, } /// #`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(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Get where OBJ: std::fmt::Debug + Serialize, { #[serde(skip_serializing_if = "String::is_empty")] pub account_id: String, #[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<*const OBJ>, } impl Get where OBJ: std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: String::new(), ids: None, properties: None, _ph: PhantomData, } } _impl!( /// - accountId: "Id" /// /// The id of the account to use. /// account_id: String ); _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 Serialize for Get { fn serialize(&self, serializer: S) -> 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(JmapArgument::Value(ref v)) => state.serialize_field("ids", v)?, Some(JmapArgument::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(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MethodResponse<'a> { #[serde(borrow)] pub method_responses: Vec<&'a RawValue>, #[serde(default)] pub created_ids: HashMap, #[serde(default)] pub session_state: String, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct GetResponse { #[serde(skip_serializing_if = "String::is_empty")] pub account_id: String, #[serde(default)] pub state: String, pub list: Vec, pub not_found: Vec, } impl std::convert::TryFrom<&RawValue> for GetResponse { type Error = crate::error::MeliError; fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { let res: (String, GetResponse, String) = serde_json::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: String); _impl!(get_mut state_mut, state: String); _impl!(get_mut list_mut, list: Vec); _impl!(get_mut not_found_mut, not_found: Vec); } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] enum JmapError { RequestTooLarge, InvalidArguments, InvalidResultReference, } #[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Query, OBJ: Object> where OBJ: std::fmt::Debug + Serialize, { account_id: String, filter: Option, sort: Option>, #[serde(default)] position: u64, #[serde(skip_serializing_if = "Option::is_none")] anchor: Option, #[serde(default)] #[serde(skip_serializing_if = "u64_zero")] anchor_offset: u64, #[serde(skip_serializing_if = "Option::is_none")] limit: Option, #[serde(default = "bool_false")] calculate_total: bool, #[serde(skip)] _ph: PhantomData<*const OBJ>, } impl, OBJ: Object> Query where OBJ: std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: String::new(), filter: None, sort: None, position: 0, anchor: None, anchor_offset: 0, limit: None, calculate_total: false, _ph: PhantomData, } } _impl!(account_id: String); _impl!(filter: Option); _impl!(sort: Option>); _impl!(position: u64); _impl!(anchor: Option); _impl!(anchor_offset: u64); _impl!(limit: Option); _impl!(calculate_total: bool); } pub fn u64_zero(num: &u64) -> bool { *num == 0 } pub fn bool_false() -> bool { false } pub fn bool_true() -> bool { true } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct QueryResponse { #[serde(skip_serializing_if = "String::is_empty", default)] pub account_id: String, 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<*const OBJ>, } impl std::convert::TryFrom<&RawValue> for QueryResponse { type Error = crate::error::MeliError; fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { let res: (String, QueryResponse, String) = serde_json::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<*const (OBJ, M)>, } // error[E0723]: trait bounds other than `Sized` on const fn parameters are unstable // --> melib/src/backends/jmap/rfc8620.rs:626:6 // | // 626 | impl, OBJ: Object> ResultField { // | ^ // | // = note: for more information, see issue https://github.com/rust-lang/rust/issues/57563 // = help: add `#![feature(const_fn)]` to the crate attributes to enable // impl, OBJ: Object> ResultField { // pub const fn new(field: &'static str) -> Self { // Self { // field, // _ph: PhantomData, // } // } // } /// #`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(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] /* ch-ch-ch-ch-ch-Changes */ pub struct Changes where OBJ: std::fmt::Debug + Serialize, { #[serde(skip_serializing_if = "String::is_empty")] pub account_id: String, pub since_state: String, #[serde(skip_serializing_if = "Option::is_none")] pub max_changes: Option, #[serde(skip)] _ph: PhantomData<*const OBJ>, } impl Changes where OBJ: std::fmt::Debug + Serialize, { pub fn new() -> Self { Self { account_id: String::new(), since_state: String::new(), max_changes: None, _ph: PhantomData, } } _impl!( /// - accountId: "Id" /// /// The id of the account to use. /// account_id: String ); _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: String ); _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 ); } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ChangesResponse { #[serde(skip_serializing_if = "String::is_empty")] pub account_id: String, pub old_state: String, pub new_state: String, pub has_more_changes: bool, pub created: Vec, pub updated: Vec, pub destroyed: Vec, #[serde(skip)] _ph: PhantomData<*const OBJ>, } impl std::convert::TryFrom<&RawValue> for ChangesResponse { type Error = crate::error::MeliError; fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { let res: (String, ChangesResponse, String) = serde_json::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: String); _impl!(get_mut old_state_mut, old_state: String); _impl!(get_mut new_state_mut, new_state: String); _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); } pub fn download_request_format( session: &JmapSession, account_id: &Id, blob_id: &Id, name: Option, ) -> String { // https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} let mut ret = String::with_capacity( session.download_url.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) = session.download_url.as_bytes()[prev_pos..].find(b"{") { ret.push_str(&session.download_url[prev_pos..prev_pos + pos]); prev_pos += pos; if session.download_url[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id); prev_pos += "{accountId}".len(); } else if session.download_url[prev_pos..].starts_with("{blobId}") { ret.push_str(blob_id); prev_pos += "{blobId}".len(); } else if session.download_url[prev_pos..].starts_with("{name}") { ret.push_str(name.as_ref().map(String::as_str).unwrap_or("")); prev_pos += "{name}".len(); } } if prev_pos != session.download_url.len() { ret.push_str(&session.download_url[prev_pos..]); } ret } pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { //"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", let mut ret = String::with_capacity(session.upload_url.len() + account_id.len()); let mut prev_pos = 0; while let Some(pos) = session.upload_url.as_bytes()[prev_pos..].find(b"{") { ret.push_str(&session.upload_url[prev_pos..prev_pos + pos]); prev_pos += pos; if session.upload_url[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id); prev_pos += "{accountId}".len(); break; } else { ret.push('{'); prev_pos += 1; } } if prev_pos != session.upload_url.len() { ret.push_str(&session.upload_url[prev_pos..]); } ret }