diff options
author | Mauro D <mauro@stalw.art> | 2023-04-14 07:04:36 +0000 |
---|---|---|
committer | Mauro D <mauro@stalw.art> | 2023-04-14 07:04:36 +0000 |
commit | 3751133c7d90436440855a479b7fed6b32ae6945 (patch) | |
tree | 77865424cb83522ec375a76a07485c1541b5de3c /crates/jmap-proto | |
parent | 98e8febbefab44cbbfd660b4df54f16aa9973680 (diff) |
Crates renamed.
Diffstat (limited to 'crates/jmap-proto')
47 files changed, 9021 insertions, 0 deletions
diff --git a/crates/jmap-proto/Cargo.lock b/crates/jmap-proto/Cargo.lock new file mode 100644 index 00000000..4cfd8d9d --- /dev/null +++ b/crates/jmap-proto/Cargo.lock @@ -0,0 +1,159 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jmap-parser" +version = "0.1.0" +dependencies = [ + "ahash", + "fast-float", + "serde", + "serde_json", + "utils", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "utils" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml new file mode 100644 index 00000000..43dc29d2 --- /dev/null +++ b/crates/jmap-proto/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jmap_proto" +version = "0.1.0" +edition = "2021" + +[dependencies] +store = { path = "../store" } +utils = { path = "../utils" } +mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] } +fast-float = "0.2.0" +serde = { version = "1.0", features = ["derive"]} +ahash = { version = "0.8.0", features = ["serde"] } +serde_json = { version = "1.0", features = ["raw_value"] } diff --git a/crates/jmap-proto/src/error/method.rs b/crates/jmap-proto/src/error/method.rs new file mode 100644 index 00000000..e4e31ea9 --- /dev/null +++ b/crates/jmap-proto/src/error/method.rs @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use serde::ser::SerializeMap; +use serde::Serialize; + +#[derive(Debug)] +pub enum MethodError { + InvalidArguments(String), + RequestTooLarge, + StateMismatch, + AnchorNotFound, + UnsupportedFilter(String), + UnsupportedSort(String), + ServerFail(String), + UnknownMethod(String), + ServerUnavailable, + ServerPartialFail, + InvalidResultReference(String), + Forbidden(String), + AccountNotFound, + AccountNotSupportedByMethod, + AccountReadOnly, + NotFound, +} + +impl Display for MethodError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MethodError::InvalidArguments(err) => write!(f, "Invalid arguments: {}", err), + MethodError::RequestTooLarge => write!(f, "Request too large"), + MethodError::StateMismatch => write!(f, "State mismatch"), + MethodError::AnchorNotFound => write!(f, "Anchor not found"), + MethodError::UnsupportedFilter(err) => write!(f, "Unsupported filter: {}", err), + MethodError::UnsupportedSort(err) => write!(f, "Unsupported sort: {}", err), + MethodError::ServerFail(err) => write!(f, "Server error: {}", err), + MethodError::UnknownMethod(err) => write!(f, "Unknown method: {}", err), + MethodError::ServerUnavailable => write!(f, "Server unavailable"), + MethodError::ServerPartialFail => write!(f, "Server partial fail"), + MethodError::InvalidResultReference(err) => { + write!(f, "Invalid result reference: {}", err) + } + MethodError::Forbidden(err) => write!(f, "Forbidden: {}", err), + MethodError::AccountNotFound => write!(f, "Account not found"), + MethodError::AccountNotSupportedByMethod => { + write!(f, "Account not supported by method") + } + MethodError::AccountReadOnly => write!(f, "Account read only"), + MethodError::NotFound => write!(f, "Not found"), + } + } +} + +impl Serialize for MethodError { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(2.into())?; + + let (error_type, description) = match self { + MethodError::InvalidArguments(description) => { + ("invalidArguments", description.as_str()) + } + MethodError::RequestTooLarge => ( + "requestTooLarge", + concat!( + "The number of ids requested by the client exceeds the maximum number ", + "the server is willing to process in a single method call." + ), + ), + MethodError::StateMismatch => ( + "stateMismatch", + concat!( + "An \"ifInState\" argument was supplied, but ", + "it does not match the current state." + ), + ), + MethodError::AnchorNotFound => ( + "anchorNotFound", + concat!( + "An anchor argument was supplied, but it ", + "cannot be found in the results of the query." + ), + ), + MethodError::UnsupportedFilter(description) => { + ("unsupportedFilter", description.as_str()) + } + MethodError::UnsupportedSort(description) => ("unsupportedSort", description.as_str()), + MethodError::ServerFail(_) => ("serverFail", { + concat!( + "An unexpected error occurred while processing ", + "this call, please contact the system administrator." + ) + }), + MethodError::NotFound => ("serverPartialFail", { + concat!( + "One or more items are no longer available on the ", + "server, please try again." + ) + }), + MethodError::UnknownMethod(description) => ("unknownMethod", description.as_str()), + MethodError::ServerUnavailable => ( + "serverUnavailable", + concat!( + "This server is temporarily unavailable. ", + "Attempting this same operation later may succeed." + ), + ), + MethodError::ServerPartialFail => ( + "serverPartialFail", + concat!( + "Some, but not all, expected changes described by the method ", + "occurred. Please resynchronize to determine server state." + ), + ), + MethodError::InvalidResultReference(description) => { + ("invalidResultReference", description.as_str()) + } + MethodError::Forbidden(description) => ("forbidden", description.as_str()), + MethodError::AccountNotFound => ( + "accountNotFound", + "The accountId does not correspond to a valid account", + ), + MethodError::AccountNotSupportedByMethod => ( + "accountNotSupportedByMethod", + concat!( + "The accountId given corresponds to a valid account, ", + "but the account does not support this method or data type." + ), + ), + MethodError::AccountReadOnly => ( + "accountReadOnly", + "This method modifies state, but the account is read-only.", + ), + }; + + map.serialize_entry("type", error_type)?; + if !description.is_empty() { + map.serialize_entry("description", description)?; + } + map.end() + } +} + +impl From<store::Error> for MethodError { + fn from(_value: store::Error) -> Self { + let log = "true"; + MethodError::ServerPartialFail + } +} diff --git a/crates/jmap-proto/src/error/mod.rs b/crates/jmap-proto/src/error/mod.rs new file mode 100644 index 00000000..a5bd9d23 --- /dev/null +++ b/crates/jmap-proto/src/error/mod.rs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod method; +pub mod request; +pub mod set; diff --git a/crates/jmap-proto/src/error/request.rs b/crates/jmap-proto/src/error/request.rs new file mode 100644 index 00000000..102224f1 --- /dev/null +++ b/crates/jmap-proto/src/error/request.rs @@ -0,0 +1,187 @@ +use std::{borrow::Cow, fmt::Display}; + +#[derive(Debug, Clone, Copy, serde::Serialize)] +pub enum RequestLimitError { + #[serde(rename(serialize = "maxSizeRequest"))] + Size, + #[serde(rename(serialize = "maxCallsInRequest"))] + CallsIn, + #[serde(rename(serialize = "maxConcurrentRequests"))] + Concurrent, +} + +#[derive(Debug, serde::Serialize)] +pub enum RequestErrorType { + #[serde(rename(serialize = "urn:ietf:params:jmap:error:unknownCapability"))] + UnknownCapability, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:notJSON"))] + NotJSON, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:notRequest"))] + NotRequest, + #[serde(rename(serialize = "urn:ietf:params:jmap:error:limit"))] + Limit, + #[serde(rename(serialize = "about:blank"))] + Other, +} + +#[derive(Debug, serde::Serialize)] +pub struct RequestError { + #[serde(rename(serialize = "type"))] + pub p_type: RequestErrorType, + pub status: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option<Cow<'static, str>>, + pub detail: Cow<'static, str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option<RequestLimitError>, +} + +impl RequestError { + pub fn blank( + status: u16, + title: impl Into<Cow<'static, str>>, + detail: impl Into<Cow<'static, str>>, + ) -> Self { + RequestError { + p_type: RequestErrorType::Other, + status, + title: Some(title.into()), + detail: detail.into(), + limit: None, + } + } + + pub fn internal_server_error() -> Self { + RequestError::blank( + 500, + "Internal Server Error", + concat!( + "There was a problem while processing your request. ", + "Please contact the system administrator." + ), + ) + } + + pub fn unavailable() -> Self { + RequestError::blank( + 503, + "Temporarily Unavailable", + concat!( + "There was a temporary problem while processing your request. ", + "Please try again in a few moments." + ), + ) + } + + pub fn invalid_parameters() -> Self { + RequestError::blank( + 400, + "Invalid Parameters", + "One or multiple parameters could not be parsed.", + ) + } + + pub fn forbidden() -> Self { + RequestError::blank( + 403, + "Forbidden", + "You do not have enough permissions to access this resource.", + ) + } + + pub fn too_many_requests() -> Self { + RequestError::blank( + 429, + "Too Many Requests", + "Your request has been rate limited. Please try again in a few seconds.", + ) + } + + pub fn too_many_auth_attempts() -> Self { + RequestError::blank( + 429, + "Too Many Authentication Attempts", + "Your request has been rate limited. Please try again in a few minutes.", + ) + } + + pub fn limit(limit_type: RequestLimitError) -> Self { + RequestError { + p_type: RequestErrorType::Limit, + status: 400, + title: None, + detail: match limit_type { + RequestLimitError::Size => concat!( + "The request is larger than the server ", + "is willing to process." + ), + RequestLimitError::CallsIn => concat!( + "The request exceeds the maximum number ", + "of calls in a single request." + ), + RequestLimitError::Concurrent => concat!( + "The request exceeds the maximum number ", + "of concurrent requests." + ), + } + .into(), + limit: Some(limit_type), + } + } + + pub fn not_found() -> Self { + RequestError::blank( + 404, + "Not Found", + "The requested resource does not exist on this server.", + ) + } + + pub fn unauthorized() -> Self { + RequestError::blank(401, "Unauthorized", "You have to authenticate first.") + } + + pub fn unknown_capability(capability: &str) -> RequestError { + RequestError { + p_type: RequestErrorType::UnknownCapability, + limit: None, + title: None, + status: 400, + detail: format!( + concat!( + "The Request object used capability ", + "'{}', which is not supported", + "by this server." + ), + capability + ) + .into(), + } + } + + pub fn not_json(detail: &str) -> RequestError { + RequestError { + p_type: RequestErrorType::NotJSON, + limit: None, + title: None, + status: 400, + detail: format!("Failed to parse JSON: {detail}").into(), + } + } + + pub fn not_request(detail: impl Into<Cow<'static, str>>) -> RequestError { + RequestError { + p_type: RequestErrorType::NotRequest, + limit: None, + title: None, + status: 400, + detail: detail.into(), + } + } +} + +impl Display for RequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.detail) + } +} diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs new file mode 100644 index 00000000..d7a9e267 --- /dev/null +++ b/crates/jmap-proto/src/error/set.rs @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::borrow::Cow; + +use crate::types::{id::Id, property::Property}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SetError { + #[serde(rename = "type")] + pub type_: SetErrorType, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option<Cow<'static, str>>, + + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option<Vec<Property>>, + + #[serde(rename = "existingId")] + #[serde(skip_serializing_if = "Option::is_none")] + existing_id: Option<Id>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub enum SetErrorType { + #[serde(rename = "forbidden")] + Forbidden, + #[serde(rename = "overQuota")] + OverQuota, + #[serde(rename = "tooLarge")] + TooLarge, + #[serde(rename = "rateLimit")] + RateLimit, + #[serde(rename = "notFound")] + NotFound, + #[serde(rename = "invalidPatch")] + InvalidPatch, + #[serde(rename = "willDestroy")] + WillDestroy, + #[serde(rename = "invalidProperties")] + InvalidProperties, + #[serde(rename = "singleton")] + Singleton, + #[serde(rename = "mailboxHasChild")] + MailboxHasChild, + #[serde(rename = "mailboxHasEmail")] + MailboxHasEmail, + #[serde(rename = "blobNotFound")] + BlobNotFound, + #[serde(rename = "tooManyKeywords")] + TooManyKeywords, + #[serde(rename = "tooManyMailboxes")] + TooManyMailboxes, + #[serde(rename = "forbiddenFrom")] + ForbiddenFrom, + #[serde(rename = "invalidEmail")] + InvalidEmail, + #[serde(rename = "tooManyRecipients")] + TooManyRecipients, + #[serde(rename = "noRecipients")] + NoRecipients, + #[serde(rename = "invalidRecipients")] + InvalidRecipients, + #[serde(rename = "forbiddenMailFrom")] + ForbiddenMailFrom, + #[serde(rename = "forbiddenToSend")] + ForbiddenToSend, + #[serde(rename = "cannotUnsend")] + CannotUnsend, + #[serde(rename = "alreadyExists")] + AlreadyExists, + #[serde(rename = "invalidScript")] + InvalidScript, + #[serde(rename = "scriptIsActive")] + ScriptIsActive, +} + +impl SetErrorType { + pub fn as_str(&self) -> &'static str { + match self { + SetErrorType::Forbidden => "forbidden", + SetErrorType::OverQuota => "overQuota", + SetErrorType::TooLarge => "tooLarge", + SetErrorType::RateLimit => "rateLimit", + SetErrorType::NotFound => "notFound", + SetErrorType::InvalidPatch => "invalidPatch", + SetErrorType::WillDestroy => "willDestroy", + SetErrorType::InvalidProperties => "invalidProperties", + SetErrorType::Singleton => "singleton", + SetErrorType::BlobNotFound => "blobNotFound", + SetErrorType::MailboxHasChild => "mailboxHasChild", + SetErrorType::MailboxHasEmail => "mailboxHasEmail", + SetErrorType::TooManyKeywords => "tooManyKeywords", + SetErrorType::TooManyMailboxes => "tooManyMailboxes", + SetErrorType::ForbiddenFrom => "forbiddenFrom", + SetErrorType::InvalidEmail => "invalidEmail", + SetErrorType::TooManyRecipients => "tooManyRecipients", + SetErrorType::NoRecipients => "noRecipients", + SetErrorType::InvalidRecipients => "invalidRecipients", + SetErrorType::ForbiddenMailFrom => "forbiddenMailFrom", + SetErrorType::ForbiddenToSend => "forbiddenToSend", + SetErrorType::CannotUnsend => "cannotUnsend", + SetErrorType::AlreadyExists => "alreadyExists", + SetErrorType::InvalidScript => "invalidScript", + SetErrorType::ScriptIsActive => "scriptIsActive", + } + } +} + +impl SetError { + pub fn new(type_: SetErrorType) -> Self { + SetError { + type_, + description: None, + properties: None, + existing_id: None, + } + } + + pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self { + self.description = description.into().into(); + self + } + + pub fn with_property(mut self, property: Property) -> Self { + self.properties = vec![property].into(); + self + } + + pub fn with_properties(mut self, properties: impl IntoIterator<Item = Property>) -> Self { + self.properties = properties.into_iter().collect::<Vec<_>>().into(); + self + } + + pub fn with_existing_id(mut self, id: Id) -> Self { + self.existing_id = id.into(); + self + } + + pub fn invalid_properties() -> Self { + Self::new(SetErrorType::InvalidProperties) + } + + pub fn forbidden() -> Self { + Self::new(SetErrorType::Forbidden) + } + + pub fn already_exists() -> Self { + Self::new(SetErrorType::AlreadyExists) + } +} + +pub type Result<T> = std::result::Result<T, SetError>; diff --git a/crates/jmap-proto/src/lib.rs b/crates/jmap-proto/src/lib.rs new file mode 100644 index 00000000..b34bcb30 --- /dev/null +++ b/crates/jmap-proto/src/lib.rs @@ -0,0 +1,112 @@ +pub mod error; +pub mod method; +pub mod object; +pub mod parser; +pub mod request; +pub mod response; +pub mod types; + +/* +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, sync::Arc}; + + #[test] + fn gen_hash() { + //let mut table = BTreeMap::new(); + for value in ["blobIds", "ifInState", "emails"] { + let mut hash = 0; + let mut shift = 0; + let lower_first = false; + + for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() { + if pos == 0 && lower_first { + hash |= (ch.to_ascii_lowercase() as u128) << shift; + } else { + hash |= (ch as u128) << shift; + } + shift += 8; + } + + shift = 0; + let mut hash2 = 0; + for &ch in value.as_bytes().iter().skip(16).take(16) { + hash2 |= (ch as u128) << shift; + shift += 8; + } + + println!( + "0x{} => {{}} // {}", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::<Vec<_>>() + .join("_"), + value + ); + /*println!( + "(0x{}, 0x{}) => Filter::{}(),", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::<Vec<_>>() + .join("_"), + format!("{hash2:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::<Vec<_>>() + .join("_"), + value + );*/ + + /*let mut hash = 0; + let mut shift = 0; + let mut first_ch = 0; + let mut name = Vec::new(); + + for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() { + if pos == 0 { + first_ch = ch.to_ascii_lowercase(); + name.push(ch.to_ascii_uppercase()); + } else { + hash |= (ch as u128) << shift; + shift += 8; + name.push(ch); + } + } + + //println!("Property::{} => {{}}", std::str::from_utf8(&name).unwrap()); + + table + .entry(first_ch) + .or_insert_with(|| vec![]) + .push((hash, name));*/ + } + + /*for (k, v) in table { + println!("b'{}' => match hash {{", k as char); + for (hash, value) in v { + println!( + " 0x{} => Property::{},", + format!("{hash:x}") + .as_bytes() + .chunks(4) + .into_iter() + .map(|s| std::str::from_utf8(s).unwrap()) + .collect::<Vec<_>>() + .join("_"), + std::str::from_utf8(&value).unwrap() + ); + } + println!(" _ => parser.invalid_property()?,"); + println!("}}"); + }*/ + } +} +*/ diff --git a/crates/jmap-proto/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs new file mode 100644 index 00000000..8580e1c4 --- /dev/null +++ b/crates/jmap-proto/src/method/changes.rs @@ -0,0 +1,105 @@ +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty}, + types::{id::Id, property::Property, state::State}, +}; + +#[derive(Debug, Clone)] +pub struct ChangesRequest { + pub account_id: Id, + pub since_state: State, + pub max_changes: Option<usize>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ChangesResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + pub old_state: State, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "hasMoreChanges")] + pub has_more_changes: bool, + + pub created: Vec<Id>, + + pub updated: Vec<Id>, + + pub destroyed: Vec<Id>, + + #[serde(rename = "updatedProperties")] + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_properties: Option<Vec<Property>>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub enum RequestArguments { + Email, + Mailbox, + Thread, + Identity, + EmailSubmission, +} + +impl JsonObjectParser for ChangesRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = ChangesRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + MethodObject::Mailbox => RequestArguments::Mailbox, + MethodObject::Thread => RequestArguments::Thread, + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/changes", + parser.ctx + )))) + } + }, + account_id: Id::default(), + since_state: State::Initial, + max_changes: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6574_6174_5365_636e_6973 => { + request.since_state = parser + .next_token::<State>()? + .unwrap_string("sinceQueryState")?; + } + 0x7365_676e_6168_4378_616d => { + request.max_changes = parser + .next_token::<Ignore>()? + .unwrap_usize_or_null("maxChanges")?; + } + + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs new file mode 100644 index 00000000..d103c8d3 --- /dev/null +++ b/crates/jmap-proto/src/method/copy.rs @@ -0,0 +1,195 @@ +use serde::Serialize; +use utils::map::vec_map::VecMap; + +use crate::{ + error::{method::MethodError, set::SetError}, + object::Object, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{method::MethodObject, reference::MaybeReference, RequestProperty}, + types::{ + blob::BlobId, + id::Id, + state::State, + value::{SetValue, Value}, + }, +}; + +#[derive(Debug, Clone)] +pub struct CopyRequest { + pub from_account_id: Id, + pub if_from_in_state: Option<State>, + pub account_id: Id, + pub if_in_state: Option<State>, + pub create: VecMap<MaybeReference<Id, String>, Object<SetValue>>, + pub on_success_destroy_original: Option<bool>, + pub destroy_from_if_in_state: Option<State>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct CopyResponse { + #[serde(rename = "fromAccountId")] + pub from_account_id: Id, + + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + pub old_state: State, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub created: VecMap<Id, Object<Value>>, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_created: VecMap<Id, SetError>, +} + +#[derive(Debug, Clone)] +pub struct CopyBlobRequest { + pub from_account_id: Id, + pub account_id: Id, + pub blob_ids: Vec<BlobId>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CopyBlobResponse { + #[serde(rename = "fromAccountId")] + pub from_account_id: Id, + + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "copied")] + #[serde(skip_serializing_if = "Option::is_none")] + pub copied: Option<VecMap<BlobId, BlobId>>, + + #[serde(rename = "notCopied")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_copied: Option<VecMap<BlobId, SetError>>, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email, +} + +impl JsonObjectParser for CopyRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = CopyRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/copy", + parser.ctx + )))) + } + }, + account_id: Id::default(), + if_in_state: None, + from_account_id: Id::default(), + if_from_in_state: None, + create: VecMap::default(), + on_success_destroy_original: None, + destroy_from_if_in_state: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6574_6165_7263 => { + request.create = + <VecMap<MaybeReference<Id, String>, Object<SetValue>>>::parse(parser)?; + } + 0x6449_746e_756f_6363_416d_6f72_66 => { + request.from_account_id = + parser.next_token::<Id>()?.unwrap_string("fromAccountId")?; + } + 0x6574_6174_536e_496d_6f72_4666_69 => { + request.if_from_in_state = parser + .next_token::<State>()? + .unwrap_string_or_null("ifFromInState")?; + } + 0x796f_7274_7365_4473_7365_6363_7553_6e6f => { + request.on_success_destroy_original = parser + .next_token::<String>()? + .unwrap_bool_or_null("onSuccessDestroyOriginal")?; + } + 0x536e_4966_496d_6f72_4679_6f72_7473_6564 => { + request.destroy_from_if_in_state = parser + .next_token::<State>()? + .unwrap_string_or_null("destroyFromIfInState")?; + } + 0x6574_6174_536e_4966_69 => { + request.if_in_state = parser + .next_token::<State>()? + .unwrap_string_or_null("ifInState")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for CopyBlobRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = CopyBlobRequest { + account_id: Id::default(), + from_account_id: Id::default(), + blob_ids: Vec::new(), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6449_746e_756f_6363_416d_6f72_66 => { + request.from_account_id = + parser.next_token::<Id>()?.unwrap_string("fromAccountId")?; + } + 0x7364_4962_6f6c_62 => { + request.blob_ids = parser + .next_token::<Vec<BlobId>>()? + .unwrap_string("blobIds")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs new file mode 100644 index 00000000..ed3c4b41 --- /dev/null +++ b/crates/jmap-proto/src/method/get.rs @@ -0,0 +1,141 @@ +use crate::{ + error::method::MethodError, + object::{email, Object}, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{ + method::MethodObject, + reference::{MaybeReference, ResultReference}, + RequestProperty, RequestPropertyParser, + }, + types::{id::Id, property::Property, state::State, value::Value}, +}; + +#[derive(Debug, Clone)] +pub struct GetRequest<T> { + pub account_id: Id, + pub ids: Option<MaybeReference<Vec<Id>, ResultReference>>, + pub properties: Option<MaybeReference<Vec<Property>, ResultReference>>, + pub arguments: T, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email(email::GetArguments), + Mailbox, + Thread, + Identity, + EmailSubmission, + PushSubscription, + SieveScript, + VacationResponse, + Principal, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct GetResponse { + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option<Id>, + + pub state: State, + + pub list: Vec<Object<Value>>, + + #[serde(rename = "notFound")] + pub not_found: Vec<Id>, +} + +impl JsonObjectParser for GetRequest<RequestArguments> { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = GetRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox, + MethodObject::Thread => RequestArguments::Thread, + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + MethodObject::PushSubscription => RequestArguments::PushSubscription, + MethodObject::SieveScript => RequestArguments::SieveScript, + MethodObject::VacationResponse => RequestArguments::VacationResponse, + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/get", + parser.ctx + )))) + } + }, + account_id: Id::default(), + ids: None, + properties: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x7364_69 => { + request.ids = if !property.is_ref { + <Option<Vec<Id>>>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + 0x7365_6974_7265_706f_7270 => { + request.properties = if !property.is_ref { + <Option<Vec<Property>>>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + if let RequestArguments::Email(arguments) = self { + arguments.parse(parser, property) + } else { + Ok(false) + } + } +} + +impl GetRequest<RequestArguments> { + pub fn take_arguments(&mut self) -> RequestArguments { + std::mem::replace(&mut self.arguments, RequestArguments::Principal) + } + + pub fn with_arguments<T>(self, arguments: T) -> GetRequest<T> { + GetRequest { + arguments, + account_id: self.account_id, + ids: self.ids, + properties: self.properties, + } + } +} diff --git a/crates/jmap-proto/src/method/import.rs b/crates/jmap-proto/src/method/import.rs new file mode 100644 index 00000000..3e7af966 --- /dev/null +++ b/crates/jmap-proto/src/method/import.rs @@ -0,0 +1,147 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + error::set::SetError, + object::Object, + parser::{json::Parser, JsonObjectParser, Token}, + request::{ + reference::{MaybeReference, ResultReference}, + RequestProperty, + }, + types::{ + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + state::State, + value::{SetValueMap, Value}, + }, +}; + +#[derive(Debug, Clone)] +pub struct ImportEmailRequest { + pub account_id: Id, + pub if_in_state: Option<State>, + pub emails: VecMap<String, ImportEmail>, +} + +#[derive(Debug, Clone)] +pub struct ImportEmail { + pub blob_id: BlobId, + pub mailbox_ids: MaybeReference<Vec<MaybeReference<Id, String>>, ResultReference>, + pub keywords: Vec<Keyword>, + pub received_at: Option<UTCDate>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ImportEmailResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub old_state: Option<State>, + + #[serde(rename = "newState")] + pub new_state: State, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option<VecMap<String, Object<Value>>>, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_created: Option<VecMap<String, SetError>>, +} + +impl JsonObjectParser for ImportEmailRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = ImportEmailRequest { + account_id: Id::default(), + if_in_state: None, + emails: VecMap::new(), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6574_6174_536e_4966_69 if !property.is_ref => { + request.if_in_state = parser + .next_token::<State>()? + .unwrap_string_or_null("ifInState")?; + } + 0x736c_6961_6d65 if !property.is_ref => { + request.emails = <VecMap<String, ImportEmail>>::parse(parser)?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for ImportEmail { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = ImportEmail { + blob_id: BlobId::default(), + mailbox_ids: MaybeReference::Value(vec![]), + keywords: vec![], + received_at: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_626f_6c62 if !property.is_ref => { + request.blob_id = parser.next_token::<BlobId>()?.unwrap_string("blobId")?; + } + 0x7364_4978_6f62_6c69_616d => { + request.mailbox_ids = if !property.is_ref { + MaybeReference::Value( + <SetValueMap<MaybeReference<Id, String>>>::parse(parser)?.values, + ) + } else { + MaybeReference::Reference(ResultReference::parse(parser)?) + }; + } + 0x7364_726f_7779_656b if !property.is_ref => { + request.keywords = <SetValueMap<Keyword>>::parse(parser)?.values; + } + 0x7441_6465_7669_6563_6572 if !property.is_ref => { + request.received_at = parser + .next_token::<UTCDate>()? + .unwrap_string_or_null("receivedAt")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs new file mode 100644 index 00000000..3c10e859 --- /dev/null +++ b/crates/jmap-proto/src/method/mod.rs @@ -0,0 +1,17 @@ +use ahash::AHashMap; + +pub mod changes; +pub mod copy; +pub mod get; +pub mod import; +pub mod parse; +pub mod query; +pub mod query_changes; +pub mod search_snippet; +pub mod set; +pub mod validate; + +#[inline(always)] +pub fn ahash_is_empty<K, V>(map: &AHashMap<K, V>) -> bool { + map.is_empty() +} diff --git a/crates/jmap-proto/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs new file mode 100644 index 00000000..0232178f --- /dev/null +++ b/crates/jmap-proto/src/method/parse.rs @@ -0,0 +1,105 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + object::Object, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::RequestProperty, + types::{blob::BlobId, id::Id, property::Property, value::Value}, +}; + +#[derive(Debug, Clone)] +pub struct ParseEmailRequest { + pub account_id: Id, + blob_ids: Vec<BlobId>, + properties: Option<Vec<Property>>, + body_properties: Option<Vec<Property>>, + fetch_text_body_values: Option<bool>, + fetch_html_body_values: Option<bool>, + fetch_all_body_values: Option<bool>, + max_body_value_bytes: Option<usize>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ParseEmailResponse { + #[serde(rename = "accountId")] + account_id: Id, + + #[serde(rename = "parsed")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + parsed: VecMap<BlobId, Object<Value>>, + + #[serde(rename = "notParsable")] + #[serde(skip_serializing_if = "Vec::is_empty")] + not_parsable: Vec<BlobId>, + + #[serde(rename = "notFound")] + #[serde(skip_serializing_if = "Vec::is_empty")] + not_found: Vec<BlobId>, +} + +impl JsonObjectParser for ParseEmailRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = ParseEmailRequest { + account_id: Id::default(), + properties: None, + blob_ids: vec![], + body_properties: None, + fetch_text_body_values: None, + fetch_html_body_values: None, + fetch_all_body_values: None, + max_body_value_bytes: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match (&property.hash[0], &property.hash[1]) { + (0x6449_746e_756f_6363_61, _) if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + (0x7364_4962_6f6c_62, _) => { + request.blob_ids = <Vec<BlobId>>::parse(parser)?; + } + (0x7365_6974_7265_706f_7270, _) => { + request.properties = <Option<Vec<Property>>>::parse(parser)?; + } + (0x7365_6974_7265_706f_7250_7964_6f62, _) => { + request.body_properties = <Option<Vec<Property>>>::parse(parser)?; + } + (0x6c61_5679_646f_4274_7865_5468_6374_6566, 0x7365_75) => { + request.fetch_text_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchTextBodyValues")?; + } + (0x6c61_5679_646f_424c_4d54_4868_6374_6566, 0x7365_75) => { + request.fetch_html_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchHTMLBodyValues")?; + } + (0x756c_6156_7964_6f42_6c6c_4168_6374_6566, 0x7365) => { + request.fetch_all_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchAllBodyValues")?; + } + (0x6574_7942_6575_6c61_5679_646f_4278_616d, 0x73) => { + request.max_body_value_bytes = parser + .next_token::<Ignore>()? + .unwrap_usize_or_null("maxBodyValueBytes")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs new file mode 100644 index 00000000..8f1c0b59 --- /dev/null +++ b/crates/jmap-proto/src/method/query.rs @@ -0,0 +1,688 @@ +use std::fmt::Display; + +use crate::{ + error::method::MethodError, + object::{email, mailbox}, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty, RequestPropertyParser}, + types::{date::UTCDate, id::Id, keyword::Keyword, state::State}, +}; + +#[derive(Debug, Clone)] +pub struct QueryRequest<T> { + pub account_id: Id, + pub filter: Vec<Filter>, + pub sort: Option<Vec<Comparator>>, + pub position: Option<i32>, + pub anchor: Option<Id>, + pub anchor_offset: Option<i32>, + pub limit: Option<usize>, + pub calculate_total: Option<bool>, + pub arguments: T, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct QueryResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "queryState")] + pub query_state: State, + + #[serde(rename = "canCalculateChanges")] + pub can_calculate_changes: bool, + + #[serde(rename = "position")] + pub position: i32, + + #[serde(rename = "ids")] + pub ids: Vec<Id>, + + #[serde(rename = "total")] + #[serde(skip_serializing_if = "Option::is_none")] + pub total: Option<usize>, + + #[serde(rename = "limit")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option<usize>, +} + +#[derive(Clone, Debug)] +pub enum Filter { + Email(String), + Name(String), + DomainName(String), + Text(String), + Type(String), + Timezone(String), + Members(Id), + QuotaLt(u32), + QuotaGt(u32), + IdentityIds(Vec<Id>), + EmailIds(Vec<Id>), + ThreadIds(Vec<Id>), + UndoStatus(String), + Before(UTCDate), + After(UTCDate), + InMailbox(Id), + InMailboxOtherThan(Vec<Id>), + MinSize(u32), + MaxSize(u32), + AllInThreadHaveKeyword(Keyword), + SomeInThreadHaveKeyword(Keyword), + NoneInThreadHaveKeyword(Keyword), + HasKeyword(Keyword), + NotKeyword(Keyword), + HasAttachment(bool), + From(String), + To(String), + Cc(String), + Bcc(String), + Subject(String), + Body(String), + Header(Vec<String>), + Id(Vec<Id>), + SentBefore(UTCDate), + SentAfter(UTCDate), + InThread(Id), + ParentId(Option<Id>), + Role(Option<String>), + HasAnyRole(bool), + IsSubscribed(bool), + IsActive(bool), + _T(String), + + And, + Or, + Not, + Close, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Comparator { + pub is_ascending: bool, + pub collation: Option<String>, + pub property: SortProperty, + pub keyword: Option<Keyword>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SortProperty { + Type, + Name, + Email, + EmailId, + ThreadId, + SentAt, + ReceivedAt, + Size, + From, + To, + Subject, + Cc, + SortOrder, + ParentId, + IsActive, + HasKeyword, + AllInThreadHaveKeyword, + SomeInThreadHaveKeyword, + _T(String), +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email(email::QueryArguments), + Mailbox(mailbox::QueryArguments), + EmailSubmission, + SieveScript, + Principal, +} + +impl JsonObjectParser for QueryRequest<RequestArguments> { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = QueryRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + MethodObject::SieveScript => RequestArguments::SieveScript, + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/query", + parser.ctx + )))) + } + }, + filter: vec![], + sort: None, + position: None, + anchor: None, + anchor_offset: None, + limit: None, + calculate_total: None, + account_id: Id::default(), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 => match parser.next_token::<Ignore>()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7472_6f73 => { + request.sort = <Option<Vec<Comparator>>>::parse(parser)?; + } + 0x6e6f_6974_6973_6f70 => { + request.position = parser + .next_token::<Ignore>()? + .unwrap_ints_or_null("position")?; + } + 0x726f_6863_6e61 => { + request.anchor = parser.next_token::<Id>()?.unwrap_string_or_null("anchor")?; + } + 0x7465_7366_664f_726f_6863_6e61 => { + request.anchor_offset = parser + .next_token::<Ignore>()? + .unwrap_ints_or_null("anchorOffset")?; + } + 0x7469_6d69_6c => { + request.limit = parser + .next_token::<Ignore>()? + .unwrap_usize_or_null("limit")?; + } + 0x6c61_746f_5465_7461_6c75_636c_6163 => { + request.calculate_total = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("calculateTotal")?; + } + + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> { + let mut filter = vec![Filter::Close]; + let mut pos_stack = vec![0]; + + loop { + match parser.next_token::<RequestProperty>()? { + Token::String(property) => { + parser.next_token::<Ignore>()?.assert(Token::Colon)?; + filter[*pos_stack.last().unwrap()] = match &property.hash[0] { + 0x726f_7461_7265_706f => { + match parser.next_token::<u64>()?.unwrap_string("operator")? { + 0x444e_41 => Filter::And, + 0x524f => Filter::Or, + 0x544f_4e => Filter::Not, + _ => return Err(parser.error_value()), + } + } + 0x736e_6f69_7469_646e_6f63 => { + parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?; + continue; + } + _ => match (&property.hash[0], &property.hash[1]) { + (0x6c69_616d_65, _) => { + Filter::Email(parser.next_token::<String>()?.unwrap_string("email")?) + } + (0x656d_616e, _) => { + Filter::Name(parser.next_token::<String>()?.unwrap_string("name")?) + } + (0x656d_614e_6e69_616d_6f64, _) => Filter::DomainName( + parser.next_token::<String>()?.unwrap_string("domainName")?, + ), + (0x7478_6574, _) => { + Filter::Text(parser.next_token::<String>()?.unwrap_string("text")?) + } + (0x6570_7974, _) => { + Filter::Type(parser.next_token::<String>()?.unwrap_string("type")?) + } + (0x656e_6f7a_656d_6974, _) => Filter::Timezone( + parser.next_token::<String>()?.unwrap_string("timezone")?, + ), + (0x7372_6562_6d65_6d, _) => { + Filter::Members(parser.next_token::<Id>()?.unwrap_string("members")?) + } + (0x6e61_6854_7265_776f_4c61_746f_7571, _) => Filter::QuotaLt( + parser + .next_token::<String>()? + .unwrap_uint_or_null("quotaLowerThan")? + .unwrap_or_default() as u32, + ), + (0x6e61_6854_7265_7461_6572_4761_746f_7571, _) => Filter::QuotaGt( + parser + .next_token::<String>()? + .unwrap_uint_or_null("quotaGreaterThan")? + .unwrap_or_default() as u32, + ), + (0x7364_4979_7469_746e_6564_69, _) => { + Filter::IdentityIds(<Vec<Id>>::parse(parser)?) + } + (0x7364_496c_6961_6d65, _) => Filter::EmailIds(<Vec<Id>>::parse(parser)?), + (0x7364_4964_6165_7268_74, _) => { + Filter::ThreadIds(<Vec<Id>>::parse(parser)?) + } + (0x7375_7461_7453_6f64_6e75, _) => Filter::UndoStatus( + parser.next_token::<String>()?.unwrap_string("undoStatus")?, + ), + (0x6572_6f66_6562, _) => { + Filter::Before(parser.next_token::<UTCDate>()?.unwrap_string("before")?) + } + (0x7265_7466_61, _) => { + Filter::After(parser.next_token::<UTCDate>()?.unwrap_string("after")?) + } + (0x786f_626c_6961_4d6e_69, _) => Filter::InMailbox( + parser.next_token::<Id>()?.unwrap_string("inMailbox")?, + ), + (0x6854_7265_6874_4f78_6f62_6c69_614d_6e69, 0x6e61) => { + Filter::InMailboxOtherThan(<Vec<Id>>::parse(parser)?) + } + (0x657a_6953_6e69_6d, _) => Filter::MinSize( + parser + .next_token::<String>()? + .unwrap_uint_or_null("minSize")? + .unwrap_or_default() as u32, + ), + (0x657a_6953_7861_6d, _) => Filter::MaxSize( + parser + .next_token::<String>()? + .unwrap_uint_or_null("maxSize")? + .unwrap_or_default() as u32, + ), + (0x4b65_7661_4864_6165_7268_546e_496c_6c61, 0x6472_6f77_7965) => { + Filter::AllInThreadHaveKeyword( + parser + .next_token::<Keyword>()? + .unwrap_string("allInThreadHaveKeyword")?, + ) + } + (0x6576_6148_6461_6572_6854_6e49_656d_6f73, 0x6472_6f77_7965_4b) => { + Filter::SomeInThreadHaveKeyword( + parser + .next_token::<Keyword>()? + .unwrap_string("someInThreadHaveKeyword")?, + ) + } + (0x6576_6148_6461_6572_6854_6e49_656e_6f6e, 0x6472_6f77_7965_4b) => { + Filter::NoneInThreadHaveKeyword( + parser + .next_token::<Keyword>()? + .unwrap_string("noneInThreadHaveKeyword")?, + ) + } + (0x6472_6f77_7965_4b73_6168, _) => Filter::HasKeyword( + parser + .next_token::<Keyword>()? + .unwrap_string("hasKeyword")?, + ), + (0x6472_6f77_7965_4b74_6f6e, _) => Filter::NotKeyword( + parser + .next_token::<Keyword>()? + .unwrap_string("notKeyword")?, + ), + (0x746e_656d_6863_6174_7441_7361_68, _) => Filter::HasAttachment( + parser + .next_token::<String>()? + .unwrap_bool("hasAttachment")?, + ), + (0x6d6f_7266, _) => { + Filter::From(parser.next_token::<String>()?.unwrap_string("from")?) + } + (0x6f74, _) => { + Filter::To(parser.next_token::<String>()?.unwrap_string("to")?) + } + (0x6363, _) => { + Filter::Cc(parser.next_token::<String>()?.unwrap_string("cc")?) + } + (0x6363_62, _) => { + Filter::Bcc(parser.next_token::<String>()?.unwrap_string("bcc")?) + } + (0x7463_656a_6275_73, _) => Filter::Subject( + parser.next_token::<String>()?.unwrap_string("subject")?, + ), + (0x7964_6f62, _) => { + Filter::Body(parser.next_token::<String>()?.unwrap_string("body")?) + } + (0x7265_6461_6568, _) => Filter::Header(<Vec<String>>::parse(parser)?), + (0x6469, _) => Filter::Id(<Vec<Id>>::parse(parser)?), + (0x6572_6f66_6542_746e_6573, _) => Filter::SentBefore( + parser + .next_token::<UTCDate>()? + .unwrap_string("sentBefore")?, + ), + (0x7265_7466_4174_6e65_73, _) => Filter::SentAfter( + parser.next_token::<UTCDate>()?.unwrap_string("sentAfter")?, + ), + (0x6461_6572_6854_6e69, _) => { + Filter::InThread(parser.next_token::<Id>()?.unwrap_string("inThread")?) + } + (0x6449_746e_6572_6170, _) => Filter::ParentId( + parser + .next_token::<Id>()? + .unwrap_string_or_null("parentId")?, + ), + (0x656c_6f72, _) => Filter::Role( + parser + .next_token::<String>()? + .unwrap_string_or_null("role")?, + ), + (0x656c_6f52_796e_4173_6168, _) => Filter::HasAnyRole( + parser.next_token::<String>()?.unwrap_bool("hasAnyRole")?, + ), + (0x6465_6269_7263_7362_7553_7369, _) => Filter::IsSubscribed( + parser.next_token::<String>()?.unwrap_bool("isSubscribed")?, + ), + (0x6576_6974_6341_7369, _) => Filter::IsActive( + parser.next_token::<String>()?.unwrap_bool("isActive")?, + ), + _ => { + if parser.is_eof || parser.skip_string() { + let filter = Filter::_T( + String::from_utf8_lossy( + parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), + ) + .into_owned(), + ); + parser.skip_token(parser.depth_array, parser.depth_dict)?; + filter + } else { + return Err(parser.error_unterminated()); + } + } + }, + }; + } + Token::DictStart => { + pos_stack.push(filter.len()); + filter.push(Filter::Close); + } + Token::DictEnd => { + if !matches!(filter[pos_stack.pop().unwrap()], Filter::Close) { + if pos_stack.is_empty() { + break; + } + } else { + return Err(Error::Method(MethodError::InvalidArguments( + "Malformed filter".to_string(), + ))); + } + } + Token::ArrayEnd => { + filter.push(Filter::Close); + } + Token::Comma => (), + token => { + return Err(token.error("filter", "object or array")); + } + } + } + + Ok(filter) +} + +impl JsonObjectParser for Comparator { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut comp = Comparator { + is_ascending: true, + collation: None, + property: SortProperty::Type, + keyword: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + match parser.next_dict_key::<u128>()? { + 0x676e_6964_6e65_6373_4173_69 => { + comp.is_ascending = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("isAscending")? + .unwrap_or_default(); + } + 0x6e6f_6974_616c_6c6f_63 => { + comp.collation = parser + .next_token::<String>()? + .unwrap_string_or_null("collation")?; + } + 0x7974_7265_706f_7270 => { + comp.property = parser + .next_token::<SortProperty>()? + .unwrap_string("property")?; + } + 0x6472_6f77_7965_6b => { + comp.keyword = parser + .next_token::<Keyword>()? + .unwrap_string_or_null("keyword")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(comp) + } +} + +impl JsonObjectParser for SortProperty { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if ch.is_ascii_alphabetic() { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + break; + } + } else { + hash = 0; + break; + } + } + + match hash { + 0x6570_7974 => Ok(SortProperty::Type), + 0x656d_616e => Ok(SortProperty::Name), + 0x6c69_616d_65 => Ok(SortProperty::Email), + 0x6449_6c69_616d_65 => Ok(SortProperty::EmailId), + 0x6449_6461_6572_6874 => Ok(SortProperty::ThreadId), + 0x7441_746e_6573 => Ok(SortProperty::SentAt), + 0x7441_6465_7669_6563_6572 => Ok(SortProperty::ReceivedAt), + 0x657a_6973 => Ok(SortProperty::Size), + 0x6d6f_7266 => Ok(SortProperty::From), + 0x6f74 => Ok(SortProperty::To), + 0x7463_656a_6275_73 => Ok(SortProperty::Subject), + 0x6363 => Ok(SortProperty::Cc), + 0x7265_6472_4f74_726f_73 => Ok(SortProperty::SortOrder), + 0x6449_746e_6572_6170 => Ok(SortProperty::ParentId), + 0x6576_6974_6341_7369 => Ok(SortProperty::IsActive), + 0x6472_6f77_7965_4b73_6168 => Ok(SortProperty::HasKeyword), + 0x4b65_7661_4864_6165_7268_546e_496c_6c61 => Ok(SortProperty::AllInThreadHaveKeyword), + 0x6576_6148_6461_6572_6854_6e49_656d_6f73 => Ok(SortProperty::SomeInThreadHaveKeyword), + _ => { + if parser.is_eof || parser.skip_string() { + Ok(SortProperty::_T( + String::from_utf8_lossy( + parser.bytes[parser.pos_marker..parser.pos - 1].as_ref(), + ) + .into_owned(), + )) + } else { + Err(parser.error_unterminated()) + } + } + } + } +} + +impl Display for Filter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Filter::Email(_) => "email", + Filter::Name(_) => "name", + Filter::DomainName(_) => "domainName", + Filter::Text(_) => "text", + Filter::Type(_) => "type", + Filter::Timezone(_) => "timezone", + Filter::Members(_) => "members", + Filter::QuotaLt(_) => "quotaLt", + Filter::QuotaGt(_) => "quotaGt", + Filter::IdentityIds(_) => "identityIds", + Filter::EmailIds(_) => "emailIds", + Filter::ThreadIds(_) => "threadIds", + Filter::UndoStatus(_) => "undoStatus", + Filter::Before(_) => "before", + Filter::After(_) => "after", + Filter::InMailbox(_) => "inMailbox", + Filter::InMailboxOtherThan(_) => "inMailboxOtherThan", + Filter::MinSize(_) => "minSize", + Filter::MaxSize(_) => "maxSize", + Filter::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword", + Filter::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword", + Filter::NoneInThreadHaveKeyword(_) => "noneInThreadHaveKeyword", + Filter::HasKeyword(_) => "hasKeyword", + Filter::NotKeyword(_) => "notKeyword", + Filter::HasAttachment(_) => "hasAttachment", + Filter::From(_) => "from", + Filter::To(_) => "to", + Filter::Cc(_) => "cc", + Filter::Bcc(_) => "bcc", + Filter::Subject(_) => "subject", + Filter::Body(_) => "body", + Filter::Header(_) => "header", + Filter::Id(_) => "id", + Filter::SentBefore(_) => "sentBefore", + Filter::SentAfter(_) => "sentAfter", + Filter::InThread(_) => "inThread", + Filter::ParentId(_) => "parentId", + Filter::Role(_) => "role", + Filter::HasAnyRole(_) => "hasAnyRole", + Filter::IsSubscribed(_) => "isSubscribed", + Filter::IsActive(_) => "isActive", + Filter::_T(v) => v.as_str(), + Filter::And => "and", + Filter::Or => "or", + Filter::Not => "not", + Filter::Close => "close", + }) + } +} + +impl Display for SortProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SortProperty::Type => "type", + SortProperty::Name => "name", + SortProperty::Email => "email", + SortProperty::EmailId => "emailId", + SortProperty::ThreadId => "threadId", + SortProperty::SentAt => "sentAt", + SortProperty::ReceivedAt => "receivedAt", + SortProperty::Size => "size", + SortProperty::From => "from", + SortProperty::To => "to", + SortProperty::Subject => "subject", + SortProperty::Cc => "cc", + SortProperty::SortOrder => "sortOrder", + SortProperty::ParentId => "parentId", + SortProperty::IsActive => "isActive", + SortProperty::HasKeyword => "hasKeyword", + SortProperty::AllInThreadHaveKeyword => "allInThreadHaveKeyword", + SortProperty::SomeInThreadHaveKeyword => "someInThreadHaveKeyword", + SortProperty::_T(s) => s, + }) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + match self { + RequestArguments::Email(args) => args.parse(parser, property), + RequestArguments::Mailbox(args) => args.parse(parser, property), + _ => Ok(false), + } + } +} + +impl Comparator { + pub fn descending(property: SortProperty) -> Self { + Self { + property, + is_ascending: false, + collation: None, + keyword: None, + } + } + pub fn ascending(property: SortProperty) -> Self { + Self { + property, + is_ascending: true, + collation: None, + keyword: None, + } + } +} + +impl QueryRequest<RequestArguments> { + pub fn take_arguments(&mut self) -> RequestArguments { + std::mem::replace(&mut self.arguments, RequestArguments::Principal) + } + + pub fn with_arguments<T>(self, arguments: T) -> QueryRequest<T> { + QueryRequest { + arguments, + account_id: self.account_id, + filter: self.filter, + sort: self.sort, + position: self.position, + anchor: self.anchor, + anchor_offset: self.anchor_offset, + limit: self.limit, + calculate_total: self.calculate_total, + } + } +} diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs new file mode 100644 index 00000000..5bc13234 --- /dev/null +++ b/crates/jmap-proto/src/method/query_changes.rs @@ -0,0 +1,136 @@ +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + request::{method::MethodObject, RequestProperty, RequestPropertyParser}, + types::{id::Id, state::State}, +}; + +use super::query::{parse_filter, Comparator, Filter, RequestArguments}; + +#[derive(Debug, Clone)] +pub struct QueryChangesRequest { + pub account_id: Id, + pub filter: Vec<Filter>, + pub sort: Option<Vec<Comparator>>, + pub since_query_state: State, + pub max_changes: Option<usize>, + pub up_to_id: Option<Id>, + pub calculate_total: Option<bool>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct QueryChangesResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "oldQueryState")] + pub old_query_state: State, + + #[serde(rename = "newQueryState")] + pub new_query_state: State, + + #[serde(rename = "total")] + #[serde(skip_serializing_if = "Option::is_none")] + pub total: Option<usize>, + + #[serde(rename = "removed")] + pub removed: Vec<Id>, + + #[serde(rename = "added")] + pub added: Vec<AddedItem>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct AddedItem { + pub id: Id, + pub index: usize, +} + +impl AddedItem { + pub fn new(id: Id, index: usize) -> Self { + Self { id, index } + } +} + +impl JsonObjectParser for QueryChangesRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = QueryChangesRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email(Default::default()), + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::EmailSubmission => RequestArguments::EmailSubmission, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/queryChanges", + parser.ctx + )))) + } + }, + filter: vec![], + sort: None, + calculate_total: None, + account_id: Id::default(), + since_query_state: State::Initial, + max_changes: None, + up_to_id: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 => match parser.next_token::<Ignore>()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7472_6f73 => { + request.sort = <Option<Vec<Comparator>>>::parse(parser)?; + } + 0x6574_6174_5379_7265_7551_6563_6e69_73 => { + request.since_query_state = parser + .next_token::<State>()? + .unwrap_string("sinceQueryState")?; + } + 0x7365_676e_6168_4378_616d => { + request.max_changes = parser + .next_token::<Ignore>()? + .unwrap_usize_or_null("maxChanges")?; + } + 0x6449_6f54_7075 => { + request.up_to_id = + parser.next_token::<Id>()?.unwrap_string_or_null("upToId")?; + } + 0x6c61_746f_5465_7461_6c75_636c_6163 => { + request.calculate_total = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("calculateTotal")?; + } + + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs new file mode 100644 index 00000000..91bba07c --- /dev/null +++ b/crates/jmap-proto/src/method/search_snippet.rs @@ -0,0 +1,91 @@ +use crate::{ + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::{ + reference::{MaybeReference, ResultReference}, + RequestProperty, + }, + types::id::Id, +}; + +use super::query::{parse_filter, Filter}; + +#[derive(Debug, Clone)] +pub struct GetSearchSnippetRequest { + pub account_id: Id, + pub filter: Vec<Filter>, + pub email_ids: MaybeReference<Vec<Id>, ResultReference>, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct GetSearchSnippetResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + + #[serde(rename = "list")] + pub list: Vec<SearchSnippet>, + + #[serde(rename = "notFound")] + #[serde(skip_serializing_if = "Option::is_none")] + pub not_found: Option<Vec<Id>>, +} + +#[derive(serde::Serialize, Clone, Debug)] +pub struct SearchSnippet { + #[serde(rename = "emailId")] + pub email_id: Id, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option<String>, +} + +impl JsonObjectParser for GetSearchSnippetRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = GetSearchSnippetRequest { + account_id: Id::default(), + filter: vec![], + email_ids: MaybeReference::Value(vec![]), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x7265_746c_6966 if !property.is_ref => match parser.next_token::<Ignore>()? { + Token::DictStart => { + request.filter = parse_filter(parser)?; + } + Token::Null => (), + token => { + return Err(token.error("filter", "object or null")); + } + }, + 0x7364_496c_6961_6d65 => { + request.email_ids = if !property.is_ref { + MaybeReference::Value(<Vec<Id>>::parse(parser)?) + } else { + MaybeReference::Reference(ResultReference::parse(parser)?) + }; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs new file mode 100644 index 00000000..40c25a8a --- /dev/null +++ b/crates/jmap-proto/src/method/set.rs @@ -0,0 +1,340 @@ +use ahash::AHashMap; +use utils::map::vec_map::VecMap; + +use crate::{ + error::{method::MethodError, set::SetError}, + object::{email_submission, mailbox, sieve, Object}, + parser::{json::Parser, Error, JsonObjectParser, Token}, + request::{ + method::MethodObject, + reference::{MaybeReference, ResultReference}, + RequestProperty, RequestPropertyParser, + }, + types::{ + acl::Acl, + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + property::{HeaderForm, ObjectProperty, Property, SetProperty}, + state::State, + type_state::TypeState, + value::{SetValue, SetValueMap, Value}, + }, +}; + +use super::ahash_is_empty; + +#[derive(Debug, Clone)] +pub struct SetRequest { + pub account_id: Id, + pub if_in_state: Option<State>, + pub create: Option<VecMap<String, Object<SetValue>>>, + pub update: Option<VecMap<Id, Object<SetValue>>>, + pub destroy: Option<MaybeReference<Vec<Id>, ResultReference>>, + pub arguments: RequestArguments, +} + +#[derive(Debug, Clone)] +pub enum RequestArguments { + Email, + Mailbox(mailbox::SetArguments), + Identity, + EmailSubmission(email_submission::SetArguments), + PushSubscription, + SieveScript(sieve::SetArguments), + VacationResponse, + Principal, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +pub struct SetResponse { + #[serde(rename = "accountId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option<Id>, + + #[serde(rename = "oldState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub old_state: Option<State>, + + #[serde(rename = "newState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub new_state: Option<State>, + + #[serde(rename = "created")] + #[serde(skip_serializing_if = "ahash_is_empty")] + pub created: AHashMap<String, Object<Value>>, + + #[serde(rename = "updated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub updated: VecMap<Id, Option<Object<Value>>>, + + #[serde(rename = "destroyed")] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub destroyed: Vec<Id>, + + #[serde(rename = "notCreated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_created: VecMap<String, SetError>, + + #[serde(rename = "notUpdated")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_updated: VecMap<Id, SetError>, + + #[serde(rename = "notDestroyed")] + #[serde(skip_serializing_if = "VecMap::is_empty")] + pub not_destroyed: VecMap<Id, SetError>, +} + +impl JsonObjectParser for SetRequest { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = SetRequest { + arguments: match &parser.ctx { + MethodObject::Email => RequestArguments::Email, + MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()), + MethodObject::Identity => RequestArguments::Identity, + MethodObject::EmailSubmission => { + RequestArguments::EmailSubmission(Default::default()) + } + MethodObject::PushSubscription => RequestArguments::PushSubscription, + MethodObject::VacationResponse => RequestArguments::VacationResponse, + MethodObject::SieveScript => RequestArguments::SieveScript(Default::default()), + MethodObject::Principal => RequestArguments::Principal, + _ => { + return Err(Error::Method(MethodError::UnknownMethod(format!( + "{}/set", + parser.ctx + )))) + } + }, + account_id: Id::default(), + if_in_state: None, + create: None, + update: None, + destroy: None, + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6574_6165_7263 if !property.is_ref => { + request.create = <Option<VecMap<String, Object<SetValue>>>>::parse(parser)?; + } + 0x6574_6164_7075 if !property.is_ref => { + request.update = <Option<VecMap<Id, Object<SetValue>>>>::parse(parser)?; + } + 0x0079_6f72_7473_6564 => { + request.destroy = if !property.is_ref { + <Option<Vec<Id>>>::parse(parser)?.map(MaybeReference::Value) + } else { + Some(MaybeReference::Reference(ResultReference::parse(parser)?)) + }; + } + 0x6574_6174_536e_4966_69 if !property.is_ref => { + request.if_in_state = parser + .next_token::<State>()? + .unwrap_string_or_null("ifInState")?; + } + _ => { + if !request.arguments.parse(parser, property)? { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} + +impl JsonObjectParser for Object<SetValue> { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut obj = Object { + properties: VecMap::with_capacity(8), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let mut property = parser.next_dict_key::<SetProperty>()?; + let value = if !property.is_ref { + match &property.property { + Property::Id | Property::ThreadId => parser + .next_token::<Id>()? + .unwrap_string_or_null("")? + .map(|id| SetValue::Value(Value::Id(id))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::BlobId | Property::Picture => parser + .next_token::<BlobId>()? + .unwrap_string_or_null("")? + .map(|id| SetValue::Value(Value::BlobId(id))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::SentAt + | Property::ReceivedAt + | Property::Expires + | Property::FromDate + | Property::ToDate => parser + .next_token::<UTCDate>()? + .unwrap_string_or_null("")? + .map(|date| SetValue::Value(Value::Date(date))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::Subject + | Property::Preview + | Property::Name + | Property::Description + | Property::Timezone + | Property::Email + | Property::Secret + | Property::DeviceClientId + | Property::Url + | Property::VerificationCode + | Property::HtmlSignature + | Property::TextSignature + | Property::Type + | Property::Charset + | Property::Disposition + | Property::Language + | Property::Location + | Property::Cid + | Property::Role + | Property::PartId => parser + .next_token::<String>()? + .unwrap_string_or_null("")? + .map(|text| SetValue::Value(Value::Text(text))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::TextBody | Property::HtmlBody => { + if let MethodObject::Email = &parser.ctx { + SetValue::Value(Value::parse::<ObjectProperty, String>(parser)?) + } else { + parser + .next_token::<String>()? + .unwrap_string_or_null("")? + .map(|text| SetValue::Value(Value::Text(text))) + .unwrap_or(SetValue::Value(Value::Null)) + } + } + Property::HasAttachment + | Property::IsSubscribed + | Property::IsEnabled + | Property::IsActive => parser + .next_token::<String>()? + .unwrap_bool_or_null("")? + .map(|bool| SetValue::Value(Value::Bool(bool))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::Size | Property::SortOrder | Property::Quota => parser + .next_token::<String>()? + .unwrap_uint_or_null("")? + .map(|uint| SetValue::Value(Value::UnsignedInt(uint))) + .unwrap_or(SetValue::Value(Value::Null)), + Property::ParentId | Property::EmailId | Property::IdentityId => parser + .next_token::<MaybeReference<Id, String>>()? + .unwrap_string_or_null("")? + .map(SetValue::IdReference) + .unwrap_or(SetValue::Value(Value::Null)), + Property::MailboxIds => { + if property.patch.is_empty() { + SetValue::IdReferences( + <SetValueMap<MaybeReference<Id, String>>>::parse(parser)?.values, + ) + } else { + property.patch.push(Value::Bool(bool::parse(parser)?)); + SetValue::Patch(property.patch) + } + } + Property::Keywords => { + if property.patch.is_empty() { + SetValue::Value(Value::List( + <SetValueMap<Keyword>>::parse(parser)? + .values + .into_iter() + .map(Value::Keyword) + .collect(), + )) + } else { + property.patch.push(Value::Bool(bool::parse(parser)?)); + SetValue::Patch(property.patch) + } + } + + Property::Acl => SetValue::Value(Value::parse::<String, Acl>(parser)?), + Property::Aliases + | Property::Attachments + | Property::Bcc + | Property::BodyStructure + | Property::BodyValues + | Property::Capabilities + | Property::Cc + | Property::Envelope + | Property::From + | Property::Headers + | Property::InReplyTo + | Property::Keys + | Property::MessageId + | Property::References + | Property::ReplyTo + | Property::Sender + | Property::SubParts + | Property::To + | Property::UndoStatus => { + SetValue::Value(Value::parse::<ObjectProperty, String>(parser)?) + } + Property::Members => { + SetValue::Value(Value::parse::<ObjectProperty, Id>(parser)?) + } + Property::Header(h) => SetValue::Value(if matches!(h.form, HeaderForm::Date) { + Value::parse::<ObjectProperty, UTCDate>(parser) + } else { + Value::parse::<ObjectProperty, String>(parser) + }?), + Property::Types => { + SetValue::Value(Value::parse::<ObjectProperty, TypeState>(parser)?) + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + SetValue::Value(Value::Null) + } + } + } else { + SetValue::ResultReference(ResultReference::parse(parser)?) + }; + + obj.properties.append(property.property, value); + + !parser.is_dict_end()? + } {} + + Ok(obj) + } +} + +impl RequestPropertyParser for RequestArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + match self { + RequestArguments::Mailbox(args) => args.parse(parser, property), + RequestArguments::EmailSubmission(args) => args.parse(parser, property), + RequestArguments::SieveScript(args) => args.parse(parser, property), + _ => Ok(false), + } + } +} diff --git a/crates/jmap-proto/src/method/validate.rs b/crates/jmap-proto/src/method/validate.rs new file mode 100644 index 00000000..880fb7a4 --- /dev/null +++ b/crates/jmap-proto/src/method/validate.rs @@ -0,0 +1,56 @@ +use serde::Serialize; + +use crate::{ + error::set::SetError, + parser::{json::Parser, JsonObjectParser, Token}, + request::RequestProperty, + types::{blob::BlobId, id::Id}, +}; + +#[derive(Debug, Clone)] +pub struct ValidateSieveScriptRequest { + pub account_id: Id, + pub blob_id: BlobId, +} + +#[derive(Debug, Serialize)] +pub struct ValidateSieveScriptResponse { + #[serde(rename = "accountId")] + pub account_id: Id, + pub error: Option<SetError>, +} + +impl JsonObjectParser for ValidateSieveScriptRequest { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut request = ValidateSieveScriptRequest { + account_id: Id::default(), + blob_id: BlobId::default(), + }; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + let property = parser.next_dict_key::<RequestProperty>()?; + match &property.hash[0] { + 0x6449_746e_756f_6363_61 if !property.is_ref => { + request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?; + } + 0x6449_626f_6c62 if !property.is_ref => { + request.blob_id = parser.next_token::<BlobId>()?.unwrap_string("blobId")?; + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + Ok(request) + } +} diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs new file mode 100644 index 00000000..914760da --- /dev/null +++ b/crates/jmap-proto/src/object/email.rs @@ -0,0 +1,73 @@ +use crate::{ + parser::{json::Parser, Ignore, JsonObjectParser}, + request::{RequestProperty, RequestPropertyParser}, + types::property::Property, +}; + +#[derive(Debug, Clone, Default)] +pub struct GetArguments { + pub body_properties: Option<Vec<Property>>, + pub fetch_text_body_values: Option<bool>, + pub fetch_html_body_values: Option<bool>, + pub fetch_all_body_values: Option<bool>, + pub max_body_value_bytes: Option<usize>, +} + +#[derive(Debug, Clone, Default)] +pub struct QueryArguments { + pub collapse_threads: Option<bool>, +} + +impl RequestPropertyParser for GetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + match (&property.hash[0], &property.hash[1]) { + (0x7365_6974_7265_706f_7250_7964_6f62, _) => { + self.body_properties = <Option<Vec<Property>>>::parse(parser)?; + } + (0x6c61_5679_646f_4274_7865_5468_6374_6566, 0x7365_75) => { + self.fetch_text_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchTextBodyValues")?; + } + (0x6c61_5679_646f_424c_4d54_4868_6374_6566, 0x7365_75) => { + self.fetch_html_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchHTMLBodyValues")?; + } + (0x756c_6156_7964_6f42_6c6c_4168_6374_6566, 0x7365) => { + self.fetch_all_body_values = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("fetchAllBodyValues")?; + } + (0x6574_7942_6575_6c61_5679_646f_4278_616d, 0x73) => { + self.max_body_value_bytes = parser + .next_token::<Ignore>()? + .unwrap_usize_or_null("maxBodyValueBytes")?; + } + _ => return Ok(false), + } + + Ok(true) + } +} + +impl RequestPropertyParser for QueryArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + if property.hash[0] == 0x7364_6165_7268_5465_7370_616c_6c6f_63 { + self.collapse_threads = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("collapseThreads")?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs new file mode 100644 index 00000000..62ab0326 --- /dev/null +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -0,0 +1,39 @@ +use utils::map::vec_map::VecMap; + +use crate::{ + parser::{json::Parser, JsonObjectParser}, + request::{reference::MaybeReference, RequestProperty, RequestPropertyParser}, + types::{id::Id, value::SetValue}, +}; + +use super::Object; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + pub on_success_update_email: Option<VecMap<MaybeReference<Id, String>, Object<SetValue>>>, + pub on_success_destroy_email: Option<Vec<MaybeReference<Id, String>>>, +} + +impl RequestPropertyParser for SetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + if property.hash[0] == 0x4565_7461_6470_5573_7365_6363_7553_6e6f + && property.hash[1] == 0x6c69_616d + { + self.on_success_update_email = + <Option<VecMap<MaybeReference<Id, String>, Object<SetValue>>>>::parse(parser)?; + Ok(true) + } else if property.hash[0] == 0x796f_7274_7365_4473_7365_6363_7553_6e6f + && property.hash[1] == 0x6c69_616d_45 + { + self.on_success_destroy_email = + <Option<Vec<MaybeReference<Id, String>>>>::parse(parser)?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs new file mode 100644 index 00000000..0141e983 --- /dev/null +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -0,0 +1,58 @@ +use crate::{ + parser::{json::Parser, Ignore}, + request::{RequestProperty, RequestPropertyParser}, +}; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + pub on_destroy_remove_emails: Option<bool>, +} + +#[derive(Debug, Clone, Default)] +pub struct QueryArguments { + sort_as_tree: Option<bool>, + filter_as_tree: Option<bool>, +} + +impl RequestPropertyParser for SetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + if property.hash[0] == 0x4565_766f_6d65_5279_6f72_7473_6544_6e6f + && property.hash[1] == 0x736c_6961_6d + { + self.on_destroy_remove_emails = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("onDestroyRemoveEmails")?; + Ok(true) + } else { + Ok(false) + } + } +} + +impl RequestPropertyParser for QueryArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + match &property.hash[0] { + 0x6565_7254_7341_7472_6f73 => { + self.sort_as_tree = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("sortAsTree")?; + } + 0x6565_7254_7341_7265_746c_6966 => { + self.filter_as_tree = parser + .next_token::<Ignore>()? + .unwrap_bool_or_null("filterAsTree")?; + } + _ => return Ok(false), + } + + Ok(true) + } +} diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs new file mode 100644 index 00000000..d6e2d1d4 --- /dev/null +++ b/crates/jmap-proto/src/object/mod.rs @@ -0,0 +1,319 @@ +pub mod email; +pub mod email_submission; +pub mod mailbox; +pub mod sieve; + +use std::slice::Iter; + +use store::{ + write::{IntoBitmap, Operation, ToBitmaps}, + BlobHash, Deserialize, Serialize, +}; +use utils::{ + codec::leb128::{Leb128Iterator, Leb128Vec}, + map::vec_map::VecMap, +}; + +use crate::types::{ + acl::Acl, + blob::{BlobId, BlobSection}, + date::UTCDate, + id::Id, + keyword::Keyword, + property::Property, + type_state::TypeState, + value::Value, +}; + +#[derive(Debug, Clone, Default, serde::Serialize, PartialEq, Eq)] +pub struct Object<T> { + pub properties: VecMap<Property, T>, +} + +impl Object<Value> { + pub fn with_capacity(capacity: usize) -> Self { + Self { + properties: VecMap::with_capacity(capacity), + } + } + + pub fn set(&mut self, property: Property, value: impl Into<Value>) -> bool { + self.properties.set(property, value.into()) + } + + pub fn append(&mut self, property: Property, value: impl Into<Value>) { + self.properties.append(property, value.into()); + } + + pub fn with_property(mut self, property: Property, value: impl Into<Value>) -> Self { + self.properties.append(property, value.into()); + self + } + + pub fn remove(&mut self, property: &Property) -> Value { + self.properties.remove(property).unwrap_or(Value::Null) + } + + pub fn get(&self, property: &Property) -> &Value { + self.properties.get(property).unwrap_or(&Value::Null) + } +} + +impl ToBitmaps for Value { + fn to_bitmaps(&self, ops: &mut Vec<store::write::Operation>, field: u8, set: bool) { + match self { + Value::Text(text) => text.as_str().to_bitmaps(ops, field, set), + Value::Keyword(keyword) => { + let (key, family) = keyword.into_bitmap(); + ops.push(Operation::Bitmap { + family, + field, + key, + set, + }); + } + Value::UnsignedInt(int) => { + let (key, family) = (*int as u32).into_bitmap(); + ops.push(Operation::Bitmap { + family, + field, + key, + set, + }); + } + Value::List(items) => { + for item in items { + match item { + Value::Text(text) => text.as_str().to_bitmaps(ops, field, set), + Value::UnsignedInt(int) => { + let (key, family) = (*int as u32).into_bitmap(); + ops.push(Operation::Bitmap { + family, + field, + key, + set, + }); + } + Value::Keyword(keyword) => { + let (key, family) = keyword.into_bitmap(); + ops.push(Operation::Bitmap { + family, + field, + key, + set, + }) + } + _ => (), + } + } + } + _ => (), + } + } +} + +const TEXT: u8 = 0; +const UNSIGNED_INT: u8 = 1; +const BOOL_TRUE: u8 = 2; +const BOOL_FALSE: u8 = 3; +const ID: u8 = 4; +const DATE: u8 = 5; +const BLOB_ID: u8 = 6; +const KEYWORD: u8 = 7; +const TYPE_STATE: u8 = 8; +const ACL: u8 = 9; +const LIST: u8 = 10; +const OBJECT: u8 = 11; +const NULL: u8 = 12; + +impl Serialize for Value { + fn serialize(self) -> Vec<u8> { + let mut buf = Vec::with_capacity(1024); + self.serialize_value(&mut buf); + buf + } +} + +impl Deserialize for Value { + fn deserialize(bytes: &[u8]) -> store::Result<Self> { + Self::deserialize_value(&mut bytes.iter()) + .ok_or_else(|| store::Error::InternalError("Failed to deserialize value.".to_string())) + } +} + +impl Serialize for Object<Value> { + fn serialize(self) -> Vec<u8> { + let mut buf = Vec::with_capacity(1024); + self.serialize_into(&mut buf); + buf + } +} + +impl Deserialize for Object<Value> { + fn deserialize(bytes: &[u8]) -> store::Result<Self> { + Object::deserialize_from(&mut bytes.iter()) + .ok_or_else(|| store::Error::InternalError("Failed to deserialize object.".to_string())) + } +} + +impl Object<Value> { + fn serialize_into(self, buf: &mut Vec<u8>) { + buf.push_leb128(self.properties.len()); + for (k, v) in self.properties { + k.serialize_value(buf); + v.serialize_value(buf); + } + } + + fn deserialize_from(bytes: &mut Iter<'_, u8>) -> Option<Object<Value>> { + let len = bytes.next_leb128()?; + let mut properties = VecMap::with_capacity(len); + for _ in 0..len { + let key = Property::deserialize_value(bytes)?; + let value = Value::deserialize_value(bytes)?; + properties.append(key, value); + } + Some(Object { properties }) + } +} + +pub trait SerializeValue { + fn serialize_value(self, buf: &mut Vec<u8>); +} + +pub trait DeserializeValue: Sized { + fn deserialize_value(bytes: &mut Iter<'_, u8>) -> Option<Self>; +} + +impl SerializeValue for Value { + fn serialize_value(self, buf: &mut Vec<u8>) { + match self { + Value::Text(v) => { + buf.push(TEXT); + v.serialize_value(buf); + } + Value::UnsignedInt(v) => { + buf.push(UNSIGNED_INT); + v.serialize_value(buf); + } + Value::Bool(v) => { + buf.push(if v { BOOL_TRUE } else { BOOL_FALSE }); + } + Value::Id(v) => { + buf.push(ID); + v.id().serialize_value(buf); + } + Value::Date(v) => { + buf.push(DATE); + (v.timestamp() as u64).serialize_value(buf); + } + Value::BlobId(v) => { + buf.push(BLOB_ID); + buf.extend_from_slice(&v.hash.hash); + buf.push_leb128(v.section.as_ref().map_or(0, |s| s.offset_start)); + } + Value::Keyword(v) => { + buf.push(KEYWORD); + v.serialize_value(buf); + } + Value::TypeState(v) => { + buf.push(TYPE_STATE); + v.serialize_value(buf); + } + Value::Acl(v) => { + buf.push(ACL); + v.serialize_value(buf); + } + Value::List(v) => { + buf.push(LIST); + buf.push_leb128(v.len()); + for i in v { + i.serialize_value(buf); + } + } + Value::Object(v) => { + buf.push(OBJECT); + v.serialize_into(buf); + } + Value::Null => { + buf.push(NULL); + } + } + } +} + +impl DeserializeValue for Value { + fn deserialize_value(bytes: &mut Iter<'_, u8>) -> Option<Self> { + match *bytes.next()? { + TEXT => Some(Value::Text(String::deserialize_value(bytes)?)), + UNSIGNED_INT => Some(Value::UnsignedInt(bytes.next_leb128()?)), + BOOL_TRUE => Some(Value::Bool(true)), + BOOL_FALSE => Some(Value::Bool(false)), + ID => Some(Value::Id(Id::new(bytes.next_leb128()?))), + DATE => Some(Value::Date(UTCDate::from_timestamp( + bytes.next_leb128::<u64>()? as i64, + ))), + BLOB_ID => { + let mut hash = BlobHash::default(); + for byte in hash.hash.iter_mut() { + *byte = *bytes.next()?; + } + let offset_start = bytes.next_leb128::<usize>()?; + Some(Value::BlobId(BlobId { + hash, + section: Some(BlobSection { + offset_start, + size: 0, + encoding: 0, + }), + })) + } + KEYWORD => Some(Value::Keyword(Keyword::deserialize_value(bytes)?)), + TYPE_STATE => Some(Value::TypeState(TypeState::deserialize_value(bytes)?)), + ACL => Some(Value::Acl(Acl::deserialize_value(bytes)?)), + LIST => { + let len = bytes.next_leb128()?; + let mut items = Vec::with_capacity(len); + for _ in 0..len { + items.push(Value::deserialize_value(bytes)?); + } + Some(Value::List(items)) + } + OBJECT => Some(Value::Object(Object::deserialize_from(bytes)?)), + NULL => Some(Value::Null), + _ => None, + } + } +} + +impl SerializeValue for String { + fn serialize_value(self, buf: &mut Vec<u8>) { + buf.push_leb128(self.len()); + if !self.is_empty() { + buf.extend_from_slice(self.as_bytes()); + } + } +} + +impl DeserializeValue for String { + fn deserialize_value(bytes: &mut Iter<'_, u8>) -> Option<Self> { + let len: usize = bytes.next_leb128()?; + let mut s = Vec::with_capacity(len); + for _ in 0..len { + s.push(*bytes.next()?); + } + String::from_utf8(s).ok() + } +} + +impl SerializeValue for u64 { + fn serialize_value(self, buf: &mut Vec<u8>) { + buf.push_leb128(self); + } +} + +impl DeserializeValue for u64 { + fn deserialize_value(bytes: &mut Iter<'_, u8>) -> Option<Self> { + bytes.next_leb128() + } +} diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs new file mode 100644 index 00000000..46970a8c --- /dev/null +++ b/crates/jmap-proto/src/object/sieve.rs @@ -0,0 +1,37 @@ +use crate::{ + parser::json::Parser, + request::{reference::MaybeReference, RequestProperty, RequestPropertyParser}, + types::id::Id, +}; + +#[derive(Debug, Clone, Default)] +pub struct SetArguments { + pub on_success_activate_script: Option<MaybeReference<Id, String>>, + pub on_success_deactivate_script: Option<bool>, +} + +impl RequestPropertyParser for SetArguments { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool> { + if property.hash[0] == 0x7461_7669_7463_4173_7365_6363_7553_6e6f + && property.hash[1] == 0x7470_6972_6353_65 + { + self.on_success_activate_script = parser + .next_token::<MaybeReference<Id, String>>()? + .unwrap_string_or_null("onSuccessActivateScript")?; + Ok(true) + } else if property.hash[0] == 0x7669_7463_6165_4473_7365_6363_7553_6e6f + && property.hash[1] == 0x7470_6972_6353_6574_61 + { + self.on_success_deactivate_script = parser + .next_token::<bool>()? + .unwrap_bool_or_null("onSuccessDeactivateScript")?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/crates/jmap-proto/src/parser/base32.rs b/crates/jmap-proto/src/parser/base32.rs new file mode 100644 index 00000000..7d78f519 --- /dev/null +++ b/crates/jmap-proto/src/parser/base32.rs @@ -0,0 +1,59 @@ +use utils::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator}; + +use super::{json::Parser, Error}; + +#[derive(Debug)] +pub struct JsonBase32Reader<'x, 'y> { + bytes: &'y mut Parser<'x>, + last_byte: u8, + pos: usize, +} + +impl<'x, 'y> JsonBase32Reader<'x, 'y> { + pub fn new(bytes: &'y mut Parser<'x>) -> Self { + JsonBase32Reader { + bytes, + pos: 0, + last_byte: 0, + } + } + + #[inline(always)] + fn map_byte(&mut self) -> Option<u8> { + match self.bytes.next_unescaped() { + Ok(Some(byte)) => match BASE32_INVERSE[byte as usize] { + decoded_byte if decoded_byte != u8::MAX => { + self.last_byte = decoded_byte; + Some(decoded_byte) + } + _ => None, + }, + _ => None, + } + } + + pub fn error(&mut self) -> Error { + self.bytes.error_value() + } +} + +impl<'x, 'y> Iterator for JsonBase32Reader<'x, 'y> { + type Item = u8; + fn next(&mut self) -> Option<Self::Item> { + let pos = self.pos % 5; + let last_byte = self.last_byte; + let byte = self.map_byte()?; + self.pos += 1; + + match pos { + 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(), + 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(), + 2 => ((last_byte << 4) | (byte >> 1)).into(), + 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(), + 4 => ((last_byte << 5) | byte).into(), + _ => None, + } + } +} + +impl<'x, 'y> Leb128Iterator<u8> for JsonBase32Reader<'x, 'y> {} diff --git a/crates/jmap-proto/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs new file mode 100644 index 00000000..86558669 --- /dev/null +++ b/crates/jmap-proto/src/parser/impls.rs @@ -0,0 +1,275 @@ +use std::fmt::Display; + +use utils::map::vec_map::VecMap; + +use super::{json::Parser, Ignore, JsonObjectParser, Token}; + +impl JsonObjectParser for u64 { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if shift < 64 { + hash |= (ch as u64) << shift; + shift += 8; + } else { + hash = 0; + break; + } + } + + Ok(hash) + } +} + +impl JsonObjectParser for u128 { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + hash = 0; + break; + } + } + + Ok(hash) + } +} + +impl JsonObjectParser for String { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + let start_pos = parser.pos; + + while let Some(ch) = parser.next_char() { + match ch { + b'\\' => { + let mut is_escaped = true; + let mut buf = Vec::with_capacity((parser.pos - start_pos) + 16); + buf.extend_from_slice(&parser.bytes[start_pos..parser.pos - 1]); + + while let Some(ch) = parser.next_char() { + match ch { + b'\\' if !is_escaped => { + is_escaped = true; + } + b'"' if !is_escaped => { + parser.is_eof = true; + return String::from_utf8(buf) + .map(Into::into) + .map_err(|_| parser.error_utf8()); + } + _ => { + if !is_escaped { + buf.push(ch); + } else { + match ch { + b'"' => { + buf.push(b'"'); + } + b'\\' => { + buf.push(b'\\'); + } + b'n' => { + buf.push(b'\n'); + } + b't' => { + buf.push(b'\t'); + } + b'r' => { + buf.push(b'\r'); + } + b'b' => { + buf.push(0x08); + } + b'f' => { + buf.push(0x0c); + } + b'/' => { + buf.push(b'/'); + } + b'u' => { + let mut code = [ + *parser.iter.next().ok_or_else(|| { + parser.error("Incomplete unicode sequence") + })?, + *parser.iter.next().ok_or_else(|| { + parser.error("Incomplete unicode sequence") + })?, + *parser.iter.next().ok_or_else(|| { + parser.error("Incomplete unicode sequence") + })?, + *parser.iter.next().ok_or_else(|| { + parser.error("Incomplete unicode sequence") + })?, + ]; + parser.pos += 4; + let code_str = std::str::from_utf8(&code) + .map_err(|_| parser.error_utf8())?; + let code_str = char::from_u32( + u32::from_str_radix(code_str, 16).map_err( + |_| { + parser.error(&format!( + "Invalid unicode sequence {code_str}" + )) + }, + )?, + ) + .ok_or_else(|| { + parser.error(&format!( + "Invalid unicode sequence {code_str}" + )) + })? + .encode_utf8(&mut code); + buf.extend_from_slice(code_str.as_bytes()); + } + _ => { + buf.push(ch); + } + } + is_escaped = false; + } + } + } + } + break; + } + b'"' => { + parser.is_eof = true; + return std::str::from_utf8( + parser + .bytes + .get(start_pos..parser.pos - 1) + .unwrap_or_default(), + ) + .map(Into::into) + .map_err(|_| parser.error_utf8()); + } + _ => (), + } + } + + Err(parser.error_unterminated()) + } +} + +impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + let mut vec = Vec::new(); + + parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?; + while { + vec.push(parser.next_token::<T>()?.unwrap_string("")?); + + !parser.is_array_end()? + } {} + + Ok(vec) + } +} + +impl<T: JsonObjectParser + Eq> JsonObjectParser for Option<Vec<T>> { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + match parser.next_token::<Ignore>()? { + Token::ArrayStart => { + let mut vec = Vec::new(); + while { + vec.push(parser.next_token::<T>()?.unwrap_string("")?); + + !parser.is_array_end()? + } {} + + Ok(Some(vec)) + } + Token::Null => Ok(None), + token => Err(token.error("", &token.to_string())), + } + } +} + +impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser for VecMap<K, V> { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + let mut map = VecMap::new(); + + parser.next_token::<Ignore>()?.assert(Token::DictStart)?; + while { + map.append(parser.next_dict_key()?, V::parse(parser)?); + !parser.is_dict_end()? + } {} + + Ok(map) + } +} + +impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser + for Option<VecMap<K, V>> +{ + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + match parser.next_token::<Ignore>()? { + Token::DictStart => { + let mut map = VecMap::new(); + + while { + map.append(parser.next_dict_key()?, V::parse(parser)?); + !parser.is_dict_end()? + } {} + + Ok(Some(map)) + } + Token::Null => Ok(None), + token => Err(token.error("", &token.to_string())), + } + } +} + +impl JsonObjectParser for bool { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + match parser.next_token::<Ignore>()? { + Token::Boolean(value) => Ok(value), + Token::Null => Ok(false), + token => Err(token.error("", &token.to_string())), + } + } +} + +impl JsonObjectParser for Ignore { + fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + where + Self: Sized, + { + if parser.skip_string() { + Ok(Ignore {}) + } else { + Err(parser.error_unterminated()) + } + } +} diff --git a/crates/jmap-proto/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs new file mode 100644 index 00000000..ead5800a --- /dev/null +++ b/crates/jmap-proto/src/parser/json.rs @@ -0,0 +1,384 @@ +use std::{fmt::Display, slice::Iter}; + +use crate::{error::method::MethodError, request::method::MethodObject}; + +use super::{Error, Ignore, JsonObjectParser, Token}; + +const MAX_NESTED_LEVELS: u32 = 16; + +#[derive(Debug)] +pub struct Parser<'x> { + pub bytes: &'x [u8], + pub iter: Iter<'x, u8>, + pub next_ch: Option<u8>, + pub pos: usize, + pub pos_marker: usize, + pub depth_array: u32, + pub depth_dict: u32, + pub is_eof: bool, + pub ctx: MethodObject, +} + +impl<'x> Parser<'x> { + pub fn new(bytes: &'x [u8]) -> Self { + Self { + bytes, + iter: bytes.iter(), + next_ch: None, + pos: 0, + pos_marker: 0, + is_eof: false, + depth_array: 0, + depth_dict: 0, + ctx: MethodObject::Core, + } + } + + pub fn error(&self, message: &str) -> Error { + println!("{}", std::str::from_utf8(&self.bytes[self.pos..]).unwrap()); + format!("{message} at position {}.", self.pos).into() + } + + pub fn error_unterminated(&self) -> Error { + format!("Unterminated string at position {pos}.", pos = self.pos).into() + } + + pub fn error_utf8(&self) -> Error { + format!("Invalid UTF-8 sequence at position {pos}.", pos = self.pos).into() + } + + pub fn error_value(&mut self) -> Error { + if self.is_eof || self.skip_string() { + Error::Method(MethodError::InvalidArguments(format!( + "Invalid value {:?} at position {}.", + String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()), + self.pos + ))) + } else { + self.error_unterminated() + } + } + + #[inline(always)] + pub fn next_char(&mut self) -> Option<u8> { + self.pos += 1; + self.iter.next().copied() + } + + #[inline(always)] + pub fn next_unescaped(&mut self) -> super::Result<Option<u8>> { + match self.next_char() { + Some(b'"') => { + self.is_eof = true; + Ok(None) + } + Some(b'\\') => self + .next_char() + .ok_or_else(|| self.error_unterminated()) + .map(Some), + Some(ch) => Ok(Some(ch)), + None => { + if self.is_eof { + Ok(None) + } else { + Err(self.error_unterminated()) + } + } + } + } + + pub fn skip_string(&mut self) -> bool { + let mut last_ch = 0; + + while let Some(ch) = self.next_char() { + if ch == b'"' && last_ch != b'\\' { + self.is_eof = true; + return true; + } else { + last_ch = ch; + } + } + + false + } + + pub fn next_token<T: JsonObjectParser>(&mut self) -> super::Result<Token<T>> { + let mut next_ch = self.next_ch.take().or_else(|| self.next_char()); + + while let Some(mut ch) = next_ch { + match ch { + b'"' => { + self.pos_marker = self.pos; + self.is_eof = false; + let value = T::parse(self)?; + return if self.is_eof || self.skip_string() { + Ok(Token::String(value)) + } else { + Err(self.error_unterminated()) + }; + } + b',' => { + return Ok(Token::Comma); + } + b':' => { + return Ok(Token::Colon); + } + b'[' => { + if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { + self.depth_array += 1; + return Ok(Token::ArrayStart); + } else { + return Err(self.error("Too many nested objects")); + } + } + b']' => { + return if self.depth_array != 0 { + self.depth_array -= 1; + Ok(Token::ArrayEnd) + } else { + Err(self.error("Unexpected array end")) + }; + } + b'{' => { + if self.depth_array + self.depth_dict < MAX_NESTED_LEVELS { + self.depth_dict += 1; + return Ok(Token::DictStart); + } else { + return Err(self.error("Too many nested objects")); + } + } + b'}' => { + return if self.depth_dict != 0 { + self.depth_dict -= 1; + Ok(Token::DictEnd) + } else { + Err(self.error("Unexpected dictionary end")) + }; + } + b'0'..=b'9' | b'-' | b'+' => { + let mut num: i64 = 0; + let mut is_float = false; + let mut is_negative = false; + let num_start = self.pos - 1; + + loop { + match ch { + b'-' => { + is_negative = true; + } + b'0'..=b'9' => { + if !is_float { + num = num.saturating_mul(10).saturating_add((ch - b'0') as i64); + } + } + b',' | b']' | b'}' => { + self.next_ch = ch.into(); + break; + } + b'+' => (), + b'.' | b'e' | b'E' => { + is_float = true; + } + b' ' | b'\r' | b'\t' | b'\n' => { + break; + } + _ => { + return Err(self + .error(&format!("Unexpected character {:?}", char::from(ch)))); + } + } + + ch = self.next_char().ok_or_else(|| self.error_unterminated())?; + } + + return if !is_float { + Ok(Token::Integer(if !is_negative { num } else { -num })) + } else { + fast_float::parse( + self.bytes.get(num_start..self.pos - 1).unwrap_or_default(), + ) + .map(Token::Float) + .map_err(|_| { + self.error(&format!( + "Failed to parse number {:?}", + String::from_utf8_lossy( + self.bytes.get(num_start..self.pos - 1).unwrap_or_default() + ) + )) + }) + }; + } + b't' => { + return if let (Some(b'r'), Some(b'u'), Some(b'e')) = + (self.iter.next(), self.iter.next(), self.iter.next()) + { + self.pos += 3; + Ok(Token::Boolean(true)) + } else { + Err(self.error("Invalid JSON token")) + }; + } + b'f' => { + return if let (Some(b'a'), Some(b'l'), Some(b's'), Some(b'e')) = ( + self.iter.next(), + self.iter.next(), + self.iter.next(), + self.iter.next(), + ) { + self.pos += 4; + Ok(Token::Boolean(false)) + } else { + Err(self.error("Invalid JSON token")) + }; + } + b'n' => { + return if let (Some(b'u'), Some(b'l'), Some(b'l')) = + (self.iter.next(), self.iter.next(), self.iter.next()) + { + self.pos += 3; + Ok(Token::Null) + } else { + Err(self.error("Invalid JSON token")) + }; + } + b' ' | b'\t' | b'\r' | b'\n' => (), + _ => { + return Err(self.error(&format!("Unexpected character {:?}", char::from(ch)))); + } + } + + next_ch = self.next_char(); + } + + Err(self.error("Unexpected EOF")) + } + + pub fn next_dict_key<T: JsonObjectParser + Display + Eq>(&mut self) -> super::Result<T> { + self.next_token::<T>().and_then(|k| { + let k = k.unwrap_string("")?; + self.next_token::<T>()?.assert(Token::Colon)?; + Ok(k) + }) + } + + pub fn is_dict_end(&mut self) -> super::Result<bool> { + match self.next_token::<String>()? { + Token::Comma => Ok(false), + Token::DictEnd => Ok(true), + token => Err(self.error(&format!("Expected ',' or '}}', found {}", token))), + } + } + + pub fn is_array_end(&mut self) -> super::Result<bool> { + match self.next_token::<String>()? { + Token::Comma => Ok(false), + Token::ArrayEnd => Ok(true), + token => Err(self.error(&format!("Expected ',' or ']', found {}", token))), + } + } + + pub fn skip_token( + &mut self, + start_depth_array: u32, + start_depth_dict: u32, + ) -> super::Result<()> { + while { + self.next_token::<Ignore>()?; + start_depth_array != self.depth_array || start_depth_dict != self.depth_dict + } {} + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::parser::Token; + + use super::Parser; + + #[test] + fn parse_json() { + for (input, expected_result) in [ + ( + &b"[true, false, 123, 456 , -123, 0.123, -0.456, 3.7e-5, 6.02e+23, null]"[..], + vec![ + Token::ArrayStart, + Token::Boolean(true), + Token::Comma, + Token::Boolean(false), + Token::Comma, + Token::Integer(123), + Token::Comma, + Token::Integer(456), + Token::Comma, + Token::Integer(-123), + Token::Comma, + Token::Float(0.123), + Token::Comma, + Token::Float(-0.456), + Token::Comma, + Token::Float(3.7e-5), + Token::Comma, + Token::Float(6.02e23), + Token::Comma, + Token::Null, + Token::ArrayEnd, + ], + ), + ( + &b"{\"\": true, \"\": false , \"\": {\"\": 123}, \"\": [ ]}"[..], + vec![ + Token::DictStart, + Token::String("".to_string()), + Token::Colon, + Token::Boolean(true), + Token::Comma, + Token::String("".to_string()), + Token::Colon, + Token::Boolean(false), + Token::Comma, + Token::String("".to_string()), + Token::Colon, + Token::DictStart, + Token::String("".to_string()), + Token::Colon, + Token::Integer(123), + Token::DictEnd, + Token::Comma, + Token::String("".to_string()), + Token::Colon, + Token::ArrayStart, + Token::ArrayEnd, + Token::DictEnd, + ], + ), + ] { + let mut p = Parser::new(input); + let mut result = Vec::new(); + while let Ok(token) = p.next_token() { + result.push(token); + } + + assert_eq!(result, expected_result); + } + + for (input, expected_result) in [ + ("hello\t\nworld", "hello\t\nworld"), + ("hello\t\n\\\"world\\\"\\n", "hello\t\n\"world\"\n"), + ("\\\"hello\\\tworld\\\"", "\"hello\tworld\""), + ("\\u0009\\u0020\\u263A", "\t ☺"), + ("", ""), + ] { + assert_eq!( + Parser::new(format!("\"{input}\"").as_bytes()) + .next_token::<String>() + .unwrap() + .unwrap_string("") + .unwrap(), + expected_result + ); + } + } +} diff --git a/crates/jmap-proto/src/parser/mod.rs b/crates/jmap-proto/src/parser/mod.rs new file mode 100644 index 00000000..77fa2818 --- /dev/null +++ b/crates/jmap-proto/src/parser/mod.rs @@ -0,0 +1,173 @@ +use std::fmt::Display; + +use crate::error::{method::MethodError, request::RequestError}; + +use self::json::Parser; + +pub mod base32; +pub mod impls; +pub mod json; + +#[derive(Debug, PartialEq, Clone)] +pub enum Token<T> { + Colon, + Comma, + DictStart, + DictEnd, + ArrayStart, + ArrayEnd, + Integer(i64), + Float(f64), + Boolean(bool), + String(T), + Null, +} + +impl<T: PartialEq> Eq for Token<T> {} + +pub trait JsonObjectParser { + fn parse(parser: &mut Parser<'_>) -> Result<Self> + where + Self: Sized; +} + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Debug)] +pub enum Error { + Request(RequestError), + Method(MethodError), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Ignore {} + +impl<T: Eq> Token<T> { + pub fn unwrap_string(self, property: &str) -> Result<T> { + match self { + Token::String(s) => Ok(s), + token => Err(token.error(property, "string")), + } + } + + pub fn unwrap_string_or_null(self, property: &str) -> Result<Option<T>> { + match self { + Token::String(s) => Ok(Some(s)), + Token::Null => Ok(None), + token => Err(token.error(property, "string")), + } + } + + pub fn unwrap_bool(self, property: &str) -> Result<bool> { + match self { + Token::Boolean(v) => Ok(v), + token => Err(token.error(property, "boolean")), + } + } + + pub fn unwrap_bool_or_null(self, property: &str) -> Result<Option<bool>> { + match self { + Token::Boolean(v) => Ok(Some(v)), + Token::Null => Ok(None), + token => Err(token.error(property, "boolean")), + } + } + + pub fn unwrap_usize_or_null(self, property: &str) -> Result<Option<usize>> { + match self { + Token::Integer(v) if v >= 0 => Ok(Some(v as usize)), + Token::Float(v) if v >= 0.0 => Ok(Some(v as usize)), + Token::Null => Ok(None), + token => Err(token.error(property, "unsigned integer")), + } + } + + pub fn unwrap_uint_or_null(self, property: &str) -> Result<Option<u64>> { + match self { + Token::Integer(v) if v >= 0 => Ok(Some(v as u64)), + Token::Float(v) if v >= 0.0 => Ok(Some(v as u64)), + Token::Null => Ok(None), + token => Err(token.error(property, "unsigned integer")), + } + } + + pub fn unwrap_int_or_null(self, property: &str) -> Result<Option<i64>> { + match self { + Token::Integer(v) => Ok(Some(v)), + Token::Float(v) => Ok(Some(v as i64)), + Token::Null => Ok(None), + token => Err(token.error(property, "unsigned integer")), + } + } + + pub fn unwrap_ints_or_null(self, property: &str) -> Result<Option<i32>> { + match self { + Token::Integer(v) => Ok(Some(v as i32)), + Token::Float(v) => Ok(Some(v as i32)), + Token::Null => Ok(None), + token => Err(token.error(property, "unsigned integer")), + } + } + + pub fn assert(self, token: Token<T>) -> Result<()> { + if self == token { + Ok(()) + } else { + Err(self.error("", &token.to_string())) + } + } + + pub fn assert_jmap(self, token: Token<T>) -> Result<()> { + if self == token { + Ok(()) + } else { + Err(Error::Request(RequestError::not_request(format!( + "Invalid JMAP request: expected '{token}', got '{self}'." + )))) + } + } + + pub fn error(&self, property: &str, expected: &str) -> Error { + Error::Method(MethodError::InvalidArguments(if !property.is_empty() { + format!("Invalid argument for '{property:?}': expected '{expected}', got '{self}'.",) + } else { + format!("Invalid argument: expected '{expected}', got '{self}'.") + })) + } +} + +impl Display for Ignore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "string") + } +} + +impl From<String> for Error { + fn from(s: String) -> Self { + Error::Request(RequestError::not_json(&s)) + } +} + +impl From<&str> for Error { + fn from(s: &str) -> Self { + Error::Request(RequestError::not_json(s)) + } +} + +impl<T> Display for Token<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::Colon => write!(f, ":"), + Token::Comma => write!(f, ","), + Token::DictStart => write!(f, "{{"), + Token::DictEnd => write!(f, "}}"), + Token::ArrayStart => write!(f, "["), + Token::ArrayEnd => write!(f, "]"), + Token::Integer(i) => write!(f, "{}", i), + Token::Float(v) => write!(f, "{}", v), + Token::Boolean(b) => write!(f, "{}", b), + Token::Null => write!(f, "null"), + Token::String(_) => write!(f, "string"), + } + } +} diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs new file mode 100644 index 00000000..054366c6 --- /dev/null +++ b/crates/jmap-proto/src/request/capability.rs @@ -0,0 +1,69 @@ +use crate::{ + error::request::RequestError, + parser::{json::Parser, Error, JsonObjectParser}, +}; + +#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq)] +pub enum Capability { + #[serde(rename(serialize = "urn:ietf:params:jmap:core"))] + Core = 1 << 0, + #[serde(rename(serialize = "urn:ietf:params:jmap:mail"))] + Mail = 1 << 1, + #[serde(rename(serialize = "urn:ietf:params:jmap:submission"))] + Submission = 1 << 2, + #[serde(rename(serialize = "urn:ietf:params:jmap:vacationresponse"))] + VacationResponse = 1 << 3, + #[serde(rename(serialize = "urn:ietf:params:jmap:contacts"))] + Contacts = 1 << 4, + #[serde(rename(serialize = "urn:ietf:params:jmap:calendars"))] + Calendars = 1 << 5, + #[serde(rename(serialize = "urn:ietf:params:jmap:websocket"))] + WebSocket = 1 << 6, + #[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))] + Sieve = 1 << 7, +} + +impl JsonObjectParser for Capability { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + for ch in b"urn:ietf:params:jmap:" { + if parser + .next_unescaped()? + .ok_or_else(|| parser.error_capability())? + != *ch + { + return Err(parser.error_capability()); + } + } + + match u128::parse(parser) { + Ok(key) => match key { + 0x6572_6f63 => Ok(Capability::Core), + 0x6c69_616d => Ok(Capability::Mail), + 0x6e6f_6973_7369_6d62_7573 => Ok(Capability::Submission), + 0x6573_6e6f_7073_6572_6e6f_6974_6163_6176 => Ok(Capability::VacationResponse), + 0x7374_6361_746e_6f63 => Ok(Capability::Contacts), + 0x7372_6164_6e65_6c61_63 => Ok(Capability::Calendars), + 0x7465_6b63_6f73_6265_77 => Ok(Capability::WebSocket), + 0x6576_6569_73 => Ok(Capability::Sieve), + _ => Err(parser.error_capability()), + }, + Err(Error::Method(_)) => Err(parser.error_capability()), + Err(err @ Error::Request(_)) => Err(err), + } + } +} + +impl<'x> Parser<'x> { + fn error_capability(&mut self) -> Error { + if self.is_eof || self.skip_string() { + Error::Request(RequestError::unknown_capability(&String::from_utf8_lossy( + self.bytes[self.pos_marker..self.pos - 1].as_ref(), + ))) + } else { + self.error_unterminated() + } + } +} diff --git a/crates/jmap-proto/src/request/echo.rs b/crates/jmap-proto/src/request/echo.rs new file mode 100644 index 00000000..e18da058 --- /dev/null +++ b/crates/jmap-proto/src/request/echo.rs @@ -0,0 +1,32 @@ +use serde_json::value::RawValue; +use std::fmt::Write; + +use crate::parser::{json::Parser, JsonObjectParser, Token}; + +#[derive(Debug, serde::Serialize)] +pub struct Echo { + pub payload: Box<RawValue>, +} + +impl JsonObjectParser for Echo { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let start_depth_array = parser.depth_array; + let start_depth_dict = parser.depth_dict; + let mut value = String::new(); + + while { + let _ = match parser.next_token::<String>()? { + Token::String(string) => write!(value, "{string:?}"), + token => write!(value, "{token}"), + }; + start_depth_array != parser.depth_array || start_depth_dict != parser.depth_dict + } {} + + Ok(Echo { + payload: RawValue::from_string(value).unwrap(), + }) + } +} diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs new file mode 100644 index 00000000..5b9f8523 --- /dev/null +++ b/crates/jmap-proto/src/request/method.rs @@ -0,0 +1,196 @@ +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MethodName { + pub obj: MethodObject, + pub fnc: MethodFunction, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodObject { + Email, + Mailbox, + Core, + Blob, + PushSubscription, + Thread, + SearchSnippet, + Identity, + EmailSubmission, + VacationResponse, + SieveScript, + Principal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodFunction { + Get, + Set, + Changes, + Query, + QueryChanges, + Copy, + Import, + Parse, + Validate, + Echo, +} + +impl JsonObjectParser for MethodName { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut shift = 0; + let mut obj_hash: u128 = 0; + let mut fnc_hash: u128 = 0; + + loop { + let ch = parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())?; + if ch != b'/' { + if shift < 128 { + obj_hash |= (ch as u128) << shift; + shift += 8; + } else { + return Err(parser.error_value()); + } + } else { + break; + } + } + + shift = 0; + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + fnc_hash |= (ch as u128) << shift; + shift += 8; + } else { + return Err(parser.error_value()); + } + } + + Ok(MethodName { + obj: match obj_hash { + 0x6c69_616d_45 => MethodObject::Email, + 0x786f_626c_6961_4d => MethodObject::Mailbox, + 0x6461_6572_6854 => MethodObject::Thread, + 0x626f_6c42 => MethodObject::Blob, + 0x6e6f_6973_7369_6d62_7553_6c69_616d_45 => MethodObject::EmailSubmission, + 0x7465_7070_696e_5368_6372_6165_53 => MethodObject::SearchSnippet, + 0x7974_6974_6e65_6449 => MethodObject::Identity, + 0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => MethodObject::VacationResponse, + 0x6e6f_6974_7069_7263_7362_7553_6873_7550 => MethodObject::PushSubscription, + 0x7470_6972_6353_6576_6569_53 => MethodObject::SieveScript, + 0x6c61_7069_636e_6972_50 => MethodObject::Principal, + 0x6572_6f43 => MethodObject::Core, + _ => return Err(parser.error_value()), + }, + fnc: match fnc_hash { + 0x7465_67 => MethodFunction::Get, + 0x7972_6575_71 => MethodFunction::Query, + 0x7465_73 => MethodFunction::Set, + 0x7365_676e_6168_63 => MethodFunction::Changes, + 0x7365_676e_6168_4379_7265_7571 => MethodFunction::QueryChanges, + 0x7970_6f63 => MethodFunction::Copy, + 0x7472_6f70_6d69 => MethodFunction::Import, + 0x6573_7261_70 => MethodFunction::Parse, + 0x6574_6164_696c_6176 => MethodFunction::Validate, + 0x6f68_6365 => MethodFunction::Echo, + _ => return Err(parser.error_value()), + }, + }) + } +} + +impl Display for MethodName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl MethodName { + pub fn unknown_method() -> Self { + Self { + obj: MethodObject::Thread, + fnc: MethodFunction::Echo, + } + } + + pub fn as_str(&self) -> &'static str { + match (self.fnc, self.obj) { + (MethodFunction::Echo, MethodObject::Core) => "Core/echo", + (MethodFunction::Copy, MethodObject::Blob) => "Blob/copy", + (MethodFunction::Get, MethodObject::PushSubscription) => "PushSubscription/get", + (MethodFunction::Set, MethodObject::PushSubscription) => "PushSubscription/set", + (MethodFunction::Get, MethodObject::Mailbox) => "Mailbox/get", + (MethodFunction::Changes, MethodObject::Mailbox) => "Mailbox/changes", + (MethodFunction::Query, MethodObject::Mailbox) => "Mailbox/query", + (MethodFunction::QueryChanges, MethodObject::Mailbox) => "Mailbox/queryChanges", + (MethodFunction::Set, MethodObject::Mailbox) => "Mailbox/set", + (MethodFunction::Get, MethodObject::Thread) => "Thread/get", + (MethodFunction::Changes, MethodObject::Thread) => "Thread/changes", + (MethodFunction::Get, MethodObject::Email) => "Email/get", + (MethodFunction::Changes, MethodObject::Email) => "Email/changes", + (MethodFunction::Query, MethodObject::Email) => "Email/query", + (MethodFunction::QueryChanges, MethodObject::Email) => "Email/queryChanges", + (MethodFunction::Set, MethodObject::Email) => "Email/set", + (MethodFunction::Copy, MethodObject::Email) => "Email/copy", + (MethodFunction::Import, MethodObject::Email) => "Email/import", + (MethodFunction::Parse, MethodObject::Email) => "Email/parse", + (MethodFunction::Get, MethodObject::SearchSnippet) => "SearchSnippet/get", + (MethodFunction::Get, MethodObject::Identity) => "Identity/get", + (MethodFunction::Changes, MethodObject::Identity) => "Identity/changes", + (MethodFunction::Set, MethodObject::Identity) => "Identity/set", + (MethodFunction::Get, MethodObject::EmailSubmission) => "EmailSubmission/get", + (MethodFunction::Changes, MethodObject::EmailSubmission) => "EmailSubmission/changes", + (MethodFunction::Query, MethodObject::EmailSubmission) => "EmailSubmission/query", + (MethodFunction::QueryChanges, MethodObject::EmailSubmission) => { + "EmailSubmission/queryChanges" + } + (MethodFunction::Set, MethodObject::EmailSubmission) => "EmailSubmission/set", + (MethodFunction::Get, MethodObject::VacationResponse) => "VacationResponse/get", + (MethodFunction::Set, MethodObject::VacationResponse) => "VacationResponse/set", + (MethodFunction::Get, MethodObject::SieveScript) => "SieveScript/get", + (MethodFunction::Set, MethodObject::SieveScript) => "SieveScript/set", + (MethodFunction::Query, MethodObject::SieveScript) => "SieveScript/query", + (MethodFunction::Validate, MethodObject::SieveScript) => "SieveScript/validate", + (MethodFunction::Get, MethodObject::Principal) => "Principal/get", + (MethodFunction::Set, MethodObject::Principal) => "Principal/set", + (MethodFunction::Query, MethodObject::Principal) => "Principal/query", + _ => "error", + } + } +} + +impl Display for MethodObject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + MethodObject::Blob => "Blob", + MethodObject::EmailSubmission => "EmailSubmission", + MethodObject::SearchSnippet => "SearchSnippet", + MethodObject::Identity => "Identity", + MethodObject::VacationResponse => "VacationResponse", + MethodObject::PushSubscription => "PushSubscription", + MethodObject::SieveScript => "SieveScript", + MethodObject::Principal => "Principal", + MethodObject::Core => "Core", + MethodObject::Mailbox => "Mailbox", + MethodObject::Thread => "Thread", + MethodObject::Email => "Email", + }) + } +} + +// Method serialization +impl serde::Serialize for MethodName { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs new file mode 100644 index 00000000..3fab6d7e --- /dev/null +++ b/crates/jmap-proto/src/request/mod.rs @@ -0,0 +1,110 @@ +pub mod capability; +pub mod echo; +pub mod method; +pub mod parser; +pub mod reference; + +use std::{ + collections::HashMap, + fmt::{Debug, Display}, +}; + +use crate::{ + error::method::MethodError, + method::{ + changes::ChangesRequest, + copy::{CopyBlobRequest, CopyRequest}, + get::{self, GetRequest}, + import::ImportEmailRequest, + parse::ParseEmailRequest, + query::{self, QueryRequest}, + query_changes::QueryChangesRequest, + search_snippet::GetSearchSnippetRequest, + set::SetRequest, + validate::ValidateSieveScriptRequest, + }, + parser::{json::Parser, JsonObjectParser}, + types::id::Id, +}; + +use self::echo::Echo; + +#[derive(Debug)] +pub struct Request { + pub using: u32, + pub method_calls: Vec<Call<RequestMethod>>, + pub created_ids: Option<HashMap<String, Id>>, +} + +#[derive(Debug, serde::Serialize)] +pub struct Call<T> { + pub id: String, + pub method: T, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct RequestProperty { + pub hash: [u128; 2], + pub is_ref: bool, +} + +#[derive(Debug)] +pub enum RequestMethod { + Get(GetRequest<get::RequestArguments>), + Set(SetRequest), + Changes(ChangesRequest), + Copy(CopyRequest), + CopyBlob(CopyBlobRequest), + ImportEmail(ImportEmailRequest), + ParseEmail(ParseEmailRequest), + QueryChanges(QueryChangesRequest), + Query(QueryRequest<query::RequestArguments>), + SearchSnippet(GetSearchSnippetRequest), + ValidateScript(ValidateSieveScriptRequest), + Echo(Echo), + Error(MethodError), +} + +impl JsonObjectParser for RequestProperty { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut hash = [0; 2]; + let mut shift = 0; + let mut is_ref = false; + + 'outer: for hash in hash.iter_mut() { + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + if ch != b'#' || parser.pos > parser.pos_marker + 1 { + *hash |= (ch as u128) << shift; + shift += 8; + } else { + is_ref = true; + } + } else { + shift = 0; + continue 'outer; + } + } + break; + } + + Ok(RequestProperty { hash, is_ref }) + } +} + +impl Display for RequestProperty { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +pub trait RequestPropertyParser { + fn parse( + &mut self, + parser: &mut Parser, + property: RequestProperty, + ) -> crate::parser::Result<bool>; +} diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs new file mode 100644 index 00000000..4e3cfac7 --- /dev/null +++ b/crates/jmap-proto/src/request/parser.rs @@ -0,0 +1,220 @@ +use std::collections::HashMap; + +use crate::{ + error::{ + method::MethodError, + request::{RequestError, RequestLimitError}, + }, + method::{ + changes::ChangesRequest, + copy::{CopyBlobRequest, CopyRequest}, + get::GetRequest, + import::ImportEmailRequest, + parse::ParseEmailRequest, + query::QueryRequest, + query_changes::QueryChangesRequest, + set::SetRequest, + validate::ValidateSieveScriptRequest, + }, + parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + types::id::Id, +}; + +use super::{ + capability::Capability, + echo::Echo, + method::{MethodFunction, MethodName, MethodObject}, + Call, Request, RequestMethod, +}; + +impl Request { + pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> Result<Self, RequestError> { + if json.len() <= max_size { + let mut request = Request { + using: 0, + method_calls: Vec::new(), + created_ids: None, + }; + let mut found_valid_keys = false; + let mut parser = Parser::new(json); + parser.next_token::<String>()?.assert(Token::DictStart)?; + while { + match parser.next_dict_key::<u128>()? { + 0x676e_6973_75 => { + found_valid_keys = true; + parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?; + while { + request.using |= + parser.next_token::<Capability>()?.unwrap_string("using")? as u32; + !parser.is_array_end()? + } {} + } + 0x736c_6c61_4364_6f68_7465_6d => { + found_valid_keys = true; + + parser + .next_token::<Ignore>()? + .assert_jmap(Token::ArrayStart)?; + while { + if request.method_calls.len() < max_calls { + parser + .next_token::<Ignore>()? + .assert_jmap(Token::ArrayStart)?; + let method = match parser.next_token::<MethodName>() { + Ok(Token::String(method)) => method, + Ok(_) => { + return Err(RequestError::not_request( + "Invalid JMAP request", + )); + } + Err(Error::Method(MethodError::InvalidArguments(_))) => { + MethodName::unknown_method() + } + Err(err) => { + return Err(err.into()); + } + }; + parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?; + parser.ctx = method.obj; + let start_depth_array = parser.depth_array; + let start_depth_dict = parser.depth_dict; + + let method = match (&method.fnc, &method.obj) { + (MethodFunction::Get, _) => { + GetRequest::parse(&mut parser).map(RequestMethod::Get) + } + (MethodFunction::Query, _) => { + QueryRequest::parse(&mut parser).map(RequestMethod::Query) + } + (MethodFunction::Set, _) => { + SetRequest::parse(&mut parser).map(RequestMethod::Set) + } + (MethodFunction::Changes, _) => { + ChangesRequest::parse(&mut parser) + .map(RequestMethod::Changes) + } + (MethodFunction::QueryChanges, _) => { + QueryChangesRequest::parse(&mut parser) + .map(RequestMethod::QueryChanges) + } + (MethodFunction::Copy, MethodObject::Email) => { + CopyRequest::parse(&mut parser).map(RequestMethod::Copy) + } + (MethodFunction::Copy, MethodObject::Blob) => { + CopyBlobRequest::parse(&mut parser) + .map(RequestMethod::CopyBlob) + } + (MethodFunction::Import, MethodObject::Email) => { + ImportEmailRequest::parse(&mut parser) + .map(RequestMethod::ImportEmail) + } + (MethodFunction::Parse, MethodObject::Email) => { + ParseEmailRequest::parse(&mut parser) + .map(RequestMethod::ParseEmail) + } + (MethodFunction::Validate, MethodObject::SieveScript) => { + ValidateSieveScriptRequest::parse(&mut parser) + .map(RequestMethod::ValidateScript) + } + (MethodFunction::Echo, MethodObject::Core) => { + Echo::parse(&mut parser).map(RequestMethod::Echo) + } + _ => Err(Error::Method(MethodError::UnknownMethod( + method.to_string(), + ))), + }; + + let method = match method { + Ok(method) => method, + Err(Error::Method(err)) => { + parser.skip_token(start_depth_array, start_depth_dict)?; + RequestMethod::Error(err) + } + Err(err) => { + return Err(err.into()); + } + }; + + parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?; + let id = parser.next_token::<String>()?.unwrap_string("")?; + parser + .next_token::<Ignore>()? + .assert_jmap(Token::ArrayEnd)?; + request.method_calls.push(Call { id, method }); + } else { + return Err(RequestError::limit(RequestLimitError::CallsIn)); + } + !parser.is_array_end()? + } {} + } + 0x7364_4964_6574_6165_7263 => { + found_valid_keys = true; + let mut created_ids = HashMap::new(); + parser.next_token::<Ignore>()?.assert(Token::DictStart)?; + while { + created_ids.insert( + parser.next_dict_key::<String>()?, + parser.next_token::<Id>()?.unwrap_string("createdIds")?, + ); + !parser.is_dict_end()? + } {} + request.created_ids = Some(created_ids); + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + if found_valid_keys { + Ok(request) + } else { + Err(RequestError::not_request("Invalid JMAP request")) + } + } else { + Err(RequestError::limit(RequestLimitError::Size)) + } + } +} + +impl From<Error> for RequestError { + fn from(value: Error) -> Self { + match value { + Error::Request(err) => err, + Error::Method(err) => RequestError::not_request(err.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::request::Request; + + const TEST: &str = r#" + { + "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], + "methodCalls": [ + [ "method1", { + "arg1": "arg1data", + "arg2": "arg2data" + }, "c1" ], + [ "Core/echo", { + "hello": true, + "high": 5 + }, "c2" ], + [ "method3", {"hello": [{"a": {"b": true}}]}, "c3" ] + ], + "createdIds": { + "c1": "m1", + "c2": "m2" + } + } + "#; + + #[test] + fn parse_request() { + println!("{:?}", Request::parse(TEST.as_bytes(), 10, 1024)); + } +} diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs new file mode 100644 index 00000000..95e16c40 --- /dev/null +++ b/crates/jmap-proto/src/request/reference.rs @@ -0,0 +1,110 @@ +use std::fmt::Display; + +use crate::{ + error::method::MethodError, + parser::{json::Parser, Error, JsonObjectParser, Token}, + types::{id::Id, pointer::JSONPointer}, +}; + +use super::method::MethodName; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +pub struct ResultReference { + #[serde(rename = "resultOf")] + pub result_of: String, + pub name: MethodName, + pub path: JSONPointer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaybeReference<V, R> { + Value(V), + Reference(R), +} + +impl<V, R> MaybeReference<V, R> { + pub fn unwrap(self) -> V { + match self { + MaybeReference::Value(v) => v, + MaybeReference::Reference(_) => panic!("unwrap() called on MaybeReference::Reference"), + } + } +} + +impl JsonObjectParser for ResultReference { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut result_of = None; + let mut name = None; + let mut path = None; + + parser + .next_token::<String>()? + .assert_jmap(Token::DictStart)?; + + while { + match parser.next_dict_key::<u64>()? { + 0x664f_746c_7573_6572 => { + result_of = Some(parser.next_token::<String>()?.unwrap_string("resultOf")?); + } + 0x656d_616e => { + name = Some(parser.next_token::<MethodName>()?.unwrap_string("name")?); + } + 0x6874_6170 => { + path = Some(parser.next_token::<JSONPointer>()?.unwrap_string("path")?); + } + _ => { + parser.skip_token(parser.depth_array, parser.depth_dict)?; + } + } + + !parser.is_dict_end()? + } {} + + if let (Some(result_of), Some(name), Some(path)) = (result_of, name, path) { + Ok(Self { + result_of, + name, + path, + }) + } else { + Err(Error::Method(MethodError::InvalidResultReference( + "Missing required fields".into(), + ))) + } + } +} + +impl Display for ResultReference { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ resultOf: {}, name: {}, path: {} }}", + self.result_of, self.name, self.path + ) + } +} + +impl<V: Display, R: Display> Display for MaybeReference<V, R> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaybeReference::Value(id) => write!(f, "{}", id), + MaybeReference::Reference(str) => write!(f, "#{}", str), + } + } +} + +// MaybeReference de/serialization +impl serde::Serialize for MaybeReference<Id, String> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + match self { + MaybeReference::Value(id) => id.serialize(serializer), + MaybeReference::Reference(str) => serializer.serialize_str(&format!("#{}", str)), + } + } +} diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs new file mode 100644 index 00000000..3a7ffc93 --- /dev/null +++ b/crates/jmap-proto/src/response/mod.rs @@ -0,0 +1,168 @@ +pub mod references; + +use std::collections::HashMap; + +use serde::Serialize; + +use crate::{ + error::method::MethodError, + method::{ + changes::ChangesResponse, + copy::{CopyBlobResponse, CopyResponse}, + get::GetResponse, + import::ImportEmailResponse, + parse::ParseEmailResponse, + query::QueryResponse, + query_changes::QueryChangesResponse, + search_snippet::GetSearchSnippetResponse, + set::SetResponse, + validate::ValidateSieveScriptResponse, + }, + request::{echo::Echo, Call}, + types::id::Id, +}; + +#[derive(Debug, serde::Serialize)] +pub enum ResponseMethod { + Get(GetResponse), + Set(SetResponse), + Changes(ChangesResponse), + Copy(CopyResponse), + CopyBlob(CopyBlobResponse), + ImportEmail(ImportEmailResponse), + ParseEmail(ParseEmailResponse), + QueryChanges(QueryChangesResponse), + Query(QueryResponse), + SearchSnippet(GetSearchSnippetResponse), + ValidateScript(ValidateSieveScriptResponse), + Echo(Echo), + Error(MethodError), +} + +#[derive(Debug, serde::Serialize)] +pub struct Response { + #[serde(rename = "methodResponses")] + pub method_responses: Vec<Call<ResponseMethod>>, + + #[serde(rename = "sessionState")] + #[serde(serialize_with = "serialize_hex")] + pub session_state: u32, + + #[serde(rename(deserialize = "createdIds"))] + pub created_ids: HashMap<String, Id>, +} + +impl Response { + pub fn new(session_state: u32, created_ids: HashMap<String, Id>, capacity: usize) -> Self { + Response { + session_state, + created_ids, + method_responses: Vec::with_capacity(capacity), + } + } + + pub fn push_response(&mut self, id: String, method: impl Into<ResponseMethod>) { + self.method_responses.push(Call { + id, + method: method.into(), + }); + } + + pub fn push_created_id(&mut self, create_id: String, id: Id) { + self.created_ids.insert(create_id, id); + } +} + +pub fn serialize_hex<S>(value: &u32, serializer: S) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + format!("{:x}", value).serialize(serializer) +} + +impl From<MethodError> for ResponseMethod { + fn from(error: MethodError) -> Self { + ResponseMethod::Error(error) + } +} + +impl From<Echo> for ResponseMethod { + fn from(echo: Echo) -> Self { + ResponseMethod::Echo(echo) + } +} + +impl From<GetResponse> for ResponseMethod { + fn from(get: GetResponse) -> Self { + ResponseMethod::Get(get) + } +} + +impl From<SetResponse> for ResponseMethod { + fn from(set: SetResponse) -> Self { + ResponseMethod::Set(set) + } +} + +impl From<ChangesResponse> for ResponseMethod { + fn from(changes: ChangesResponse) -> Self { + ResponseMethod::Changes(changes) + } +} + +impl From<CopyResponse> for ResponseMethod { + fn from(copy: CopyResponse) -> Self { + ResponseMethod::Copy(copy) + } +} + +impl From<CopyBlobResponse> for ResponseMethod { + fn from(copy_blob: CopyBlobResponse) -> Self { + ResponseMethod::CopyBlob(copy_blob) + } +} + +impl From<ImportEmailResponse> for ResponseMethod { + fn from(import_email: ImportEmailResponse) -> Self { + ResponseMethod::ImportEmail(import_email) + } +} + +impl From<ParseEmailResponse> for ResponseMethod { + fn from(parse_email: ParseEmailResponse) -> Self { + ResponseMethod::ParseEmail(parse_email) + } +} + +impl From<QueryChangesResponse> for ResponseMethod { + fn from(query_changes: QueryChangesResponse) -> Self { + ResponseMethod::QueryChanges(query_changes) + } +} + +impl From<QueryResponse> for ResponseMethod { + fn from(query: QueryResponse) -> Self { + ResponseMethod::Query(query) + } +} + +impl From<GetSearchSnippetResponse> for ResponseMethod { + fn from(search_snippet: GetSearchSnippetResponse) -> Self { + ResponseMethod::SearchSnippet(search_snippet) + } +} + +impl From<ValidateSieveScriptResponse> for ResponseMethod { + fn from(validate_script: ValidateSieveScriptResponse) -> Self { + ResponseMethod::ValidateScript(validate_script) + } +} + +impl<T: Into<ResponseMethod>> From<Result<T, MethodError>> for ResponseMethod { + fn from(result: Result<T, MethodError>) -> Self { + match result { + Ok(value) => value.into(), + Err(error) => error.into(), + } + } +} diff --git a/crates/jmap-proto/src/response/references.rs b/crates/jmap-proto/src/response/references.rs new file mode 100644 index 00000000..777bffff --- /dev/null +++ b/crates/jmap-proto/src/response/references.rs @@ -0,0 +1,695 @@ +use std::collections::HashMap; + +use utils::map::vec_map::VecMap; + +use crate::{ + error::method::MethodError, + object::Object, + request::{ + method::MethodFunction, + reference::{MaybeReference, ResultReference}, + RequestMethod, + }, + types::{ + id::Id, + property::Property, + value::{SetValue, Value}, + }, +}; + +use super::{Response, ResponseMethod}; + +enum EvalResult { + Properties(Vec<Property>), + Values(Vec<Value>), + Failed, +} + +impl Response { + pub fn resolve_references(&self, request: &mut RequestMethod) -> Result<(), MethodError> { + match request { + RequestMethod::Get(request) => { + // Resolve id references + if let Some(MaybeReference::Reference(reference)) = &request.ids { + request.ids = Some(MaybeReference::Value( + self.eval_result_references(reference) + .unwrap_ids(reference)?, + )); + } + + // Resolve properties references + if let Some(MaybeReference::Reference(reference)) = &request.properties { + request.properties = Some(MaybeReference::Value( + self.eval_result_references(reference) + .unwrap_properties(reference)?, + )); + } + } + RequestMethod::Set(request) => { + // Resolve create references + if let Some(create) = &mut request.create { + let mut graph = HashMap::with_capacity(create.len()); + for (id, obj) in create.iter_mut() { + self.eval_object_references(obj, Some((&*id, &mut graph)))?; + } + + // Perform topological sort + if !graph.is_empty() { + let mut sorted_create = VecMap::with_capacity(create.len()); + let mut it_stack = Vec::new(); + let keys = graph.keys().cloned().collect::<Vec<_>>(); + let mut it = keys.iter(); + + 'main: loop { + while let Some(from_id) = it.next() { + if let Some(to_ids) = graph.get(from_id) { + it_stack.push((it, from_id)); + if it_stack.len() > 1000 { + return Err(MethodError::InvalidArguments( + "Cyclical references are not allowed.".to_string(), + )); + } + it = to_ids.iter(); + continue; + } else if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } + + if let Some((prev_it, from_id)) = it_stack.pop() { + it = prev_it; + if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } else { + break; + } + } + + // Add remaining items + if !create.is_empty() { + for (id, value) in std::mem::take(create) { + sorted_create.append(id, value); + } + } + request.create = sorted_create.into(); + } + } + + // Resolve update references + if let Some(update) = &mut request.update { + for obj in update.values_mut() { + self.eval_object_references(obj, None)?; + } + } + + // Resolve destroy references + if let Some(MaybeReference::Reference(reference)) = &request.destroy { + request.destroy = Some(MaybeReference::Value( + self.eval_result_references(reference) + .unwrap_ids(reference)?, + )); + } + } + RequestMethod::Copy(request) => { + // Resolve create references + for (id, obj) in request.create.iter_mut() { + self.eval_object_references(obj, None)?; + if let MaybeReference::Reference(ir) = id { + *id = MaybeReference::Value(self.eval_id_reference(ir)?); + } + } + } + RequestMethod::ImportEmail(request) => { + // Resolve email mailbox references + for email in request.emails.values_mut() { + match &mut email.mailbox_ids { + MaybeReference::Reference(rr) => { + email.mailbox_ids = MaybeReference::Value( + self.eval_result_references(rr) + .unwrap_ids(rr)? + .into_iter() + .map(MaybeReference::Value) + .collect(), + ); + } + MaybeReference::Value(values) => { + for value in values { + if let MaybeReference::Reference(ir) = value { + *value = MaybeReference::Value(self.eval_id_reference(ir)?); + } + } + } + } + } + } + RequestMethod::SearchSnippet(request) => { + // Resolve emailIds references + if let MaybeReference::Reference(reference) = &request.email_ids { + request.email_ids = MaybeReference::Value( + self.eval_result_references(reference) + .unwrap_ids(reference)?, + ); + } + } + _ => {} + } + + Ok(()) + } + + fn eval_result_references(&self, rr: &ResultReference) -> EvalResult { + for response in &self.method_responses { + if response.id == rr.result_of { + match (&rr.name.fnc, &response.method) { + (MethodFunction::Get, ResponseMethod::Get(response)) => { + return match rr.path.item_subquery() { + Some((root, property)) if root == "list" => { + let property = Property::parse(property); + + EvalResult::Values( + response + .list + .iter() + .filter_map(|obj| obj.properties.get(&property).cloned()) + .collect(), + ) + } + _ => EvalResult::Failed, + }; + } + (MethodFunction::Changes, ResponseMethod::Changes(response)) => { + return match rr.path.item_query() { + Some("created") => EvalResult::Values( + response + .created + .clone() + .into_iter() + .map(Into::into) + .collect(), + ), + Some("updated") => EvalResult::Values( + response + .updated + .clone() + .into_iter() + .map(Into::into) + .collect(), + ), + Some("updatedProperties") => EvalResult::Properties( + response.updated_properties.clone().unwrap_or_default(), + ), + _ => EvalResult::Failed, + }; + } + (MethodFunction::Query, ResponseMethod::Query(response)) => { + return if rr.path.item_query() == Some("ids") { + EvalResult::Values( + response.ids.iter().copied().map(Into::into).collect(), + ) + } else { + EvalResult::Failed + }; + } + (MethodFunction::QueryChanges, ResponseMethod::QueryChanges(response)) => { + return if rr.path.item_subquery() == Some(("added", "id")) { + EvalResult::Values( + response.added.iter().map(|item| item.id.into()).collect(), + ) + } else { + EvalResult::Failed + }; + } + _ => (), + } + } + } + + EvalResult::Failed + } + + fn eval_id_reference(&self, ir: &str) -> Result<Id, MethodError> { + if let Some(id) = self.created_ids.get(ir) { + Ok(*id) + } else { + Err(MethodError::InvalidResultReference(format!( + "Id reference {ir:?} not found." + ))) + } + } + + fn eval_object_references( + &self, + obj: &mut Object<SetValue>, + mut graph: Option<(&str, &mut HashMap<String, Vec<String>>)>, + ) -> Result<(), MethodError> { + for set_value in obj.properties.values_mut() { + match set_value { + SetValue::IdReference(MaybeReference::Reference(parent_id)) => { + if let Some(id) = self.created_ids.get(parent_id) { + *set_value = SetValue::Value(Value::Id(*id)); + } else if let Some((child_id, graph)) = &mut graph { + graph + .entry(child_id.to_string()) + .or_insert_with(Vec::new) + .push(parent_id.to_string()); + } else { + return Err(MethodError::InvalidResultReference(format!( + "Id reference {parent_id:?} not found." + ))); + } + } + SetValue::IdReferences(id_refs) => { + for id_ref in id_refs { + if let MaybeReference::Reference(parent_id) = id_ref { + if let Some(id) = self.created_ids.get(parent_id) { + *id_ref = MaybeReference::Value(*id); + } else if let Some((child_id, graph)) = &mut graph { + graph + .entry(child_id.to_string()) + .or_insert_with(Vec::new) + .push(parent_id.to_string()); + } else { + return Err(MethodError::InvalidResultReference(format!( + "Id reference {parent_id:?} not found." + ))); + } + } + } + } + SetValue::ResultReference(rr) => { + *set_value = + SetValue::Value(self.eval_result_references(rr).unwrap_ids(rr)?.into()); + } + _ => (), + } + } + + Ok(()) + } +} + +impl EvalResult { + pub fn unwrap_ids(self, rr: &ResultReference) -> Result<Vec<Id>, MethodError> { + if let EvalResult::Values(values) = self { + let mut ids = Vec::with_capacity(values.len()); + for value in values { + match value { + Value::Id(id) => ids.push(id), + Value::List(list) => { + for value in list { + if let Value::Id(id) = value { + ids.push(id); + } else { + return Err(MethodError::InvalidResultReference(format!( + "Failed to evaluate {rr} result reference." + ))); + } + } + } + _ => { + return Err(MethodError::InvalidResultReference(format!( + "Failed to evaluate {rr} result reference." + ))) + } + } + } + Ok(ids) + } else { + Err(MethodError::InvalidResultReference(format!( + "Failed to evaluate {rr} result reference." + ))) + } + } + + pub fn unwrap_properties(self, rr: &ResultReference) -> Result<Vec<Property>, MethodError> { + if let EvalResult::Properties(properties) = self { + Ok(properties) + } else { + Err(MethodError::InvalidResultReference(format!( + "Failed to evaluate {rr} result reference." + ))) + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{ + error::method::MethodError, + request::{Request, RequestMethod}, + response::Response, + types::{ + id::Id, + property::Property, + value::{SetValue, Value}, + }, + }; + + #[test] + fn eval_references() { + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#e" + }, + "e": { + "name": "Folder e", + "parentId": "#f" + }, + "f": { + "name": "Folder f", + "parentId": "#g" + }, + "g": { + "name": "Folder g", + "parentId": null + } + } + }, + "fulltree" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "Folder a1", + "parentId": null + }, + "b2": { + "name": "Folder b2", + "parentId": "#a1" + }, + "c3": { + "name": "Folder c3", + "parentId": "#a1" + }, + "d4": { + "name": "Folder d4", + "parentId": "#b2" + }, + "e5": { + "name": "Folder e5", + "parentId": "#b2" + }, + "f6": { + "name": "Folder f6", + "parentId": "#d4" + }, + "g7": { + "name": "Folder g7", + "parentId": "#e5" + } + } + }, + "fulltree2" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "z": { + "name": "Folder Z", + "parentId": "#x" + }, + "y": { + "name": null + }, + "x": { + "name": "Folder X" + } + } + }, + "xyz" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#a" + } + } + }, + "circular" + ] + ] + }"##, + 100, + 1024 * 1024, + ) + .unwrap(); + + let response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + for (test_num, mut call) in request.method_calls.into_iter().enumerate() { + match response.resolve_references(&mut call.method) { + Ok(_) => assert!( + (0..3).contains(&test_num), + "Unexpected invocation {}", + test_num + ), + Err(err) => { + assert_eq!(test_num, 3); + assert!(matches!(err, MethodError::InvalidArguments(_))); + continue; + } + } + + if let RequestMethod::Set(request) = call.method { + if test_num == 0 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::<Vec<_>>(), + ["g", "f", "e", "d", "c", "b", "a"] + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + ); + } else if test_num == 1 { + let mut pending_ids = vec!["a1", "b2", "d4", "e5", "f6", "c3", "g7"]; + + for (id, _) in request.create.as_ref().unwrap() { + match id.as_str() { + "a1" => (), + "b2" | "c3" => assert!(!pending_ids.contains(&"a1")), + "d4" | "e5" => assert!(!pending_ids.contains(&"b2")), + "f6" => assert!(!pending_ids.contains(&"d4")), + "g7" => assert!(!pending_ids.contains(&"e5")), + _ => panic!("Unexpected ID"), + } + pending_ids.retain(|i| i != id); + } + + if !pending_ids.is_empty() { + panic!( + "Unexpected order: {:?}", + request + .create + .as_ref() + .unwrap() + .iter() + .map(|b| b.0.to_string()) + .collect::<Vec<_>>() + ); + } + } else if test_num == 2 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::<Vec<_>>(), + ["x", "z", "y"] + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + ); + } + } else { + panic!("Expected Set Mailbox Request"); + } + } + + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "a", + "parentId": "#x" + }, + "b": { + "name": "b", + "parentId": "#y" + }, + "c": { + "name": "c", + "parentId": "#z" + } + } + }, + "ref1" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "a1", + "parentId": "#a" + }, + "b2": { + "name": "b2", + "parentId": "#b" + }, + "c3": { + "name": "c3", + "parentId": "#c" + } + } + }, + "red2" + ] + ], + "createdIds": { + "x": "b", + "y": "c", + "z": "d" + } + }"##, + 1024, + 1024 * 1024, + ) + .unwrap(); + + let mut response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + let mut invocations = request.method_calls.into_iter(); + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(request) = call.method { + let create = request + .create + .unwrap() + .into_iter() + .map(|(p, mut v)| (p, v.properties.remove(&Property::ParentId).unwrap())) + .collect::<HashMap<_, _>>(); + assert_eq!( + create.get("a").unwrap(), + &SetValue::Value(Value::Id(Id::new(1))) + ); + assert_eq!( + create.get("b").unwrap(), + &SetValue::Value(Value::Id(Id::new(2))) + ); + assert_eq!( + create.get("c").unwrap(), + &SetValue::Value(Value::Id(Id::new(3))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + + response.created_ids.insert("a".to_string(), Id::new(5)); + response.created_ids.insert("b".to_string(), Id::new(6)); + response.created_ids.insert("c".to_string(), Id::new(7)); + + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(request) = call.method { + let create = request + .create + .unwrap() + .into_iter() + .map(|(p, mut v)| (p, v.properties.remove(&Property::ParentId).unwrap())) + .collect::<HashMap<_, _>>(); + assert_eq!( + create.get("a1").unwrap(), + &SetValue::Value(Value::Id(Id::new(5))) + ); + assert_eq!( + create.get("b2").unwrap(), + &SetValue::Value(Value::Id(Id::new(6))) + ); + assert_eq!( + create.get("c3").unwrap(), + &SetValue::Value(Value::Id(Id::new(7))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + } +} diff --git a/crates/jmap-proto/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs new file mode 100644 index 00000000..7ff2d8df --- /dev/null +++ b/crates/jmap-proto/src/types/acl.rs @@ -0,0 +1,110 @@ +use std::fmt::{self, Display}; + +use crate::{ + object::{DeserializeValue, SerializeValue}, + parser::{json::Parser, JsonObjectParser}, +}; + +#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Clone, Copy)] +#[repr(u8)] +pub enum Acl { + Read = 0, + Modify = 1, + Delete = 2, + ReadItems = 3, + AddItems = 4, + ModifyItems = 5, + RemoveItems = 6, + CreateChild = 7, + Administer = 8, + Submit = 9, +} + +impl JsonObjectParser for Acl { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return Err(parser.error_value()); + } + } + + match hash { + 0x6461_6572 => Ok(Acl::Read), + 0x7966_6964_6f6d => Ok(Acl::Modify), + 0x6574_656c_6564 => Ok(Acl::Delete), + 0x736d_6574_4964_6165_72 => Ok(Acl::ReadItems), + 0x736d_6574_4964_6461 => Ok(Acl::AddItems), + 0x736d_6574_4979_6669_646f_6d => Ok(Acl::ModifyItems), + 0x736d_6574_4965_766f_6d65_72 => Ok(Acl::RemoveItems), + 0x646c_6968_4365_7461_6572_63 => Ok(Acl::CreateChild), + 0x7265_7473_696e_696d_6461 => Ok(Acl::Administer), + 0x7469_6d62_7573 => Ok(Acl::Submit), + _ => Err(parser.error_value()), + } + } +} + +impl Acl { + fn as_str(&self) -> &'static str { + match self { + Acl::Read => "read", + Acl::Modify => "modify", + Acl::Delete => "delete", + Acl::ReadItems => "readItems", + Acl::AddItems => "addItems", + Acl::ModifyItems => "modifyItems", + Acl::RemoveItems => "removeItems", + Acl::CreateChild => "createChild", + Acl::Administer => "administer", + Acl::Submit => "submit", + } + } +} + +impl Display for Acl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl serde::Serialize for Acl { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl SerializeValue for Acl { + fn serialize_value(self, buf: &mut Vec<u8>) { + buf.push(self as u8); + } +} + +impl DeserializeValue for Acl { + fn deserialize_value(bytes: &mut std::slice::Iter<'_, u8>) -> Option<Self> { + match *bytes.next()? { + 0 => Some(Acl::Read), + 1 => Some(Acl::Modify), + 2 => Some(Acl::Delete), + 3 => Some(Acl::ReadItems), + 4 => Some(Acl::AddItems), + 5 => Some(Acl::ModifyItems), + 6 => Some(Acl::RemoveItems), + 7 => Some(Acl::CreateChild), + 8 => Some(Acl::Administer), + 9 => Some(Acl::Submit), + _ => None, + } + } +} diff --git a/crates/jmap-proto/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs new file mode 100644 index 00000000..a2aa5db8 --- /dev/null +++ b/crates/jmap-proto/src/types/blob.rs @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::io::Write; + +use store::{BlobHash, BLOB_HASH_LEN}; +use utils::codec::{ + base32_custom::Base32Writer, + leb128::{Leb128Iterator, Leb128Writer}, +}; + +use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BlobId { + pub hash: BlobHash, + pub section: Option<BlobSection>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BlobSection { + pub offset_start: usize, + pub size: usize, + pub encoding: u8, +} + +impl JsonObjectParser for BlobId { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let encoding = match parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())? + { + b'a' => None, + b @ b'b'..=b'g' => Some(b - b'b'), + _ => { + return Err(parser.error_value()); + } + }; + + let mut it = JsonBase32Reader::new(parser); + let mut hash = [0; BLOB_HASH_LEN]; + + for byte in hash.iter_mut().take(BLOB_HASH_LEN) { + *byte = it.next().ok_or_else(|| it.error())?; + } + + Ok(BlobId { + hash: BlobHash { hash }, + section: if let Some(encoding) = encoding { + BlobSection { + offset_start: it.next_leb128().ok_or_else(|| it.error())?, + size: it.next_leb128().ok_or_else(|| it.error())?, + encoding, + } + .into() + } else { + None + }, + }) + } +} + +impl BlobId { + pub fn new(hash: BlobHash) -> Self { + BlobId { + hash, + section: None, + } + } + + pub fn new_section( + hash: BlobHash, + offset_start: usize, + offset_end: usize, + encoding: impl Into<u8>, + ) -> Self { + BlobId { + hash, + section: BlobSection { + offset_start, + size: offset_end - offset_start, + encoding: encoding.into(), + } + .into(), + } + } + + pub fn start_offset(&self) -> usize { + if let Some(section) = &self.section { + section.offset_start + } else { + 0 + } + } +} + +impl From<&BlobHash> for BlobId { + fn from(id: &BlobHash) -> Self { + BlobId::new(*id) + } +} + +impl From<BlobHash> for BlobId { + fn from(id: BlobHash) -> Self { + BlobId::new(id) + } +} + +impl Default for BlobId { + fn default() -> Self { + Self { + hash: BlobHash { + hash: [0; BLOB_HASH_LEN], + }, + section: None, + } + } +} + +impl serde::Serialize for BlobId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl std::fmt::Display for BlobId { + #[allow(clippy::unused_io_amount)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut writer; + if let Some(section) = &self.section { + writer = + Base32Writer::with_capacity(BLOB_HASH_LEN + (std::mem::size_of::<u32>() * 2) + 1); + writer.push_char(char::from(b'b' + section.encoding)); + writer.write(&self.hash.hash).unwrap(); + writer.write_leb128(section.offset_start).unwrap(); + writer.write_leb128(section.size).unwrap(); + } else { + writer = Base32Writer::with_capacity(BLOB_HASH_LEN + 1); + writer.push_char('a'); + writer.write(&self.hash.hash).unwrap(); + } + + f.write_str(&writer.finalize()) + } +} diff --git a/crates/jmap-proto/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs new file mode 100644 index 00000000..e220d388 --- /dev/null +++ b/crates/jmap-proto/src/types/collection.rs @@ -0,0 +1,34 @@ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[repr(u8)] +pub enum Collection { + Principal = 0, + PushSubscription = 1, + Email = 2, + Mailbox = 3, + Thread = 4, + Identity = 5, + EmailSubmission = 6, + SieveScript = 7, +} + +impl From<u8> for Collection { + fn from(v: u8) -> Self { + match v { + 0 => Collection::Principal, + 1 => Collection::PushSubscription, + 2 => Collection::Email, + 3 => Collection::Mailbox, + 4 => Collection::Thread, + 5 => Collection::Identity, + 6 => Collection::EmailSubmission, + 7 => Collection::SieveScript, + _ => panic!("Invalid collection"), + } + } +} + +impl From<Collection> for u8 { + fn from(v: Collection) -> Self { + v as u8 + } +} diff --git a/crates/jmap-proto/src/types/date.rs b/crates/jmap-proto/src/types/date.rs new file mode 100644 index 00000000..7e4b9028 --- /dev/null +++ b/crates/jmap-proto/src/types/date.rs @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use store::Serialize; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct UTCDate { + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub minute: u8, + pub second: u8, + pub tz_before_gmt: bool, + pub tz_hour: u8, + pub tz_minute: u8, +} + +impl JsonObjectParser for UTCDate { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + // 2004 - 06 - 28 T 23 : 43 : 45 . 000 Z + // 1969 - 02 - 13 T 23 : 32 : 00 - 03 : 30 + // 0 1 2 3 4 5 6 7 + + let mut pos = 0; + let mut parts = [0u32; 8]; + let mut parts_sizes = [ + 4u32, // Year (0) + 2u32, // Month (1) + 2u32, // Day (2) + 2u32, // Hour (3) + 2u32, // Minute (4) + 2u32, // Second (5) + 2u32, // TZ Hour (6) + 2u32, // TZ Minute (7) + ]; + let mut skip_digits = false; + let mut is_plus = true; + + while let Some(ch) = parser.next_unescaped()? { + match ch { + b'0'..=b'9' => { + if !skip_digits { + if parts_sizes[pos] > 0 { + parts_sizes[pos] -= 1; + parts[pos] += (ch - b'0') as u32 * u32::pow(10, parts_sizes[pos]); + } else { + break; + } + } + } + b'-' => { + if pos <= 1 { + pos += 1; + } else if pos == 5 { + pos += 1; + is_plus = false; + skip_digits = false; + } else { + break; + } + } + b'T' => { + if pos == 2 { + pos += 1; + } else { + break; + } + } + b':' => { + if [3, 4, 6].contains(&pos) { + pos += 1; + } else { + break; + } + } + b'+' => { + if pos == 5 { + pos += 1; + skip_digits = false; + } else { + break; + } + } + b'.' => { + if pos == 5 { + skip_digits = true; + } else { + break; + } + } + b'Z' | b'z' => (), + _ => { + break; + } + } + } + + if pos >= 5 { + Ok(UTCDate { + year: parts[0] as u16, + month: parts[1] as u8, + day: parts[2] as u8, + hour: parts[3] as u8, + minute: parts[4] as u8, + second: parts[5] as u8, + tz_hour: parts[6] as u8, + tz_minute: parts[7] as u8, + tz_before_gmt: !is_plus, + }) + } else { + Err(parser.error_value()) + } + } +} + +impl UTCDate { + pub fn from_timestamp(timestamp: i64) -> Self { + // Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days + let (z, seconds) = ((timestamp / 86400) + 719468, timestamp % 86400); + let era: i64 = (if z >= 0 { z } else { z - 146096 }) / 146097; + let doe: u64 = (z - era * 146097) as u64; // [0, 146096] + let yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399] + let y: i64 = (yoe as i64) + era * 400; + let doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] + let mp = (5 * doy + 2) / 153; // [0, 11] + let d: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31] + let m: u64 = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12] + let (h, mn, s) = (seconds / 3600, (seconds / 60) % 60, seconds % 60); + + UTCDate { + year: (y + i64::from(m <= 2)) as u16, + month: m as u8, + day: d as u8, + hour: h as u8, + minute: mn as u8, + second: s as u8, + tz_before_gmt: false, + tz_hour: 0, + tz_minute: 0, + } + } + + pub fn is_valid(&self) -> bool { + (0..=23).contains(&self.tz_hour) + && (1970..=3000).contains(&self.year) + && (0..=59).contains(&self.tz_minute) + && (1..=12).contains(&self.month) + && (1..=31).contains(&self.day) + && (0..=23).contains(&self.hour) + && (0..=59).contains(&self.minute) + && (0..=59).contains(&self.second) + } + + pub fn timestamp(&self) -> i64 { + // Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992 + let month = self.month as u32; + let year_base = 4800; /* Before min year, multiple of 400. */ + let m_adj = month.wrapping_sub(3); /* March-based month. */ + let carry = i64::from(m_adj > month); + let adjust = if carry > 0 { 12 } else { 0 }; + let y_adj = self.year as i64 + year_base - carry; + let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048; + let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400; + (y_adj * 365 + leap_days + month_days as i64 + (self.day as i64 - 1) - 2472632) * 86400 + + self.hour as i64 * 3600 + + self.minute as i64 * 60 + + self.second as i64 + + ((self.tz_hour as i64 * 3600 + self.tz_minute as i64 * 60) + * if self.tz_before_gmt { 1 } else { -1 }) + } +} + +impl Display for UTCDate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.tz_hour != 0 || self.tz_minute != 0 { + write!( + f, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}", + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + if self.tz_before_gmt && (self.tz_hour > 0 || self.tz_minute > 0) { + "-" + } else { + "+" + }, + self.tz_hour, + self.tz_minute, + ) + } else { + write!( + f, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + self.year, self.month, self.day, self.hour, self.minute, self.second, + ) + } + } +} + +impl serde::Serialize for UTCDate { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl Serialize for UTCDate { + fn serialize(self) -> Vec<u8> { + (self.timestamp() as u64).serialize() + } +} + +impl From<UTCDate> for u64 { + fn from(value: UTCDate) -> Self { + value.timestamp() as u64 + } +} + +#[cfg(test)] +mod tests { + use crate::{parser::json::Parser, types::date::UTCDate}; + + #[test] + fn parse_jmap_date() { + for (input, expected_result) in [ + ("1997-11-21T09:55:06-06:00", "1997-11-21T09:55:06-06:00"), + ("1997-11-21T09:55:06+00:00", "1997-11-21T09:55:06Z"), + ("2021-01-01T09:55:06+02:00", "2021-01-01T09:55:06+02:00"), + ("2004-06-28T23:43:45.000Z", "2004-06-28T23:43:45Z"), + ("1997-11-21T09:55:06.123+00:00", "1997-11-21T09:55:06Z"), + ( + "2021-01-01T09:55:06.4567+02:00", + "2021-01-01T09:55:06+02:00", + ), + ] { + let date = Parser::new(format!("\"{input}\"").as_bytes()) + .next_token::<UTCDate>() + .unwrap() + .unwrap_string("") + .unwrap(); + assert_eq!(date.to_string(), expected_result); + + let timestamp = date.timestamp(); + assert_eq!(UTCDate::from_timestamp(timestamp).timestamp(), timestamp); + } + } +} diff --git a/crates/jmap-proto/src/types/id.rs b/crates/jmap-proto/src/types/id.rs new file mode 100644 index 00000000..231b784c --- /dev/null +++ b/crates/jmap-proto/src/types/id.rs @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::ops::Deref; + +use store::{write::IntoBitmap, Serialize, BM_TAG, TAG_ID}; +use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE}; + +use crate::{ + parser::{json::Parser, JsonObjectParser}, + request::reference::MaybeReference, +}; + +use super::DocumentId; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] +pub struct Id { + id: u64, +} + +impl Default for Id { + fn default() -> Self { + Id { id: u64::MAX } + } +} + +impl JsonObjectParser for Id { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut id = 0; + + while let Some(ch) = parser.next_unescaped()? { + let i = BASE32_INVERSE[ch as usize]; + if i != u8::MAX { + id = (id << 5) | i as u64; + } else { + return Err(parser.error_value()); + } + } + + Ok(Id { id }) + } +} + +impl JsonObjectParser for MaybeReference<Id, String> { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let ch = parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())?; + + if ch != b'#' { + let mut id = BASE32_INVERSE[ch as usize] as u64; + + if id != u8::MAX as u64 { + while let Some(ch) = parser.next_unescaped()? { + let i = BASE32_INVERSE[ch as usize]; + if i != u8::MAX { + id = (id << 5) | i as u64; + } else { + return Err(parser.error_value()); + } + } + + Ok(MaybeReference::Value(Id { id })) + } else { + Err(parser.error_value()) + } + } else { + String::parse(parser).map(MaybeReference::Reference) + } + } +} + +impl Id { + pub fn new(id: u64) -> Self { + Self { id } + } + + pub fn singleton() -> Self { + Self::new(20080258862541) + } + + // From https://github.com/archer884/crockford by J/A <archer884@gmail.com> + // License: MIT/Apache 2.0 + pub fn as_string(&self) -> String { + match self.id { + 0 => "a".to_string(), + mut n => { + // Used for the initial shift. + const QUAD_SHIFT: usize = 60; + const QUAD_RESET: usize = 4; + + // Used for all subsequent shifts. + const FIVE_SHIFT: usize = 59; + const FIVE_RESET: usize = 5; + + // After we clear the four most significant bits, the four least significant bits will be + // replaced with 0001. We can then know to stop once the four most significant bits are, + // likewise, 0001. + const STOP_BIT: u64 = 1 << QUAD_SHIFT; + + let mut buf = String::with_capacity(7); + + // Start by getting the most significant four bits. We get four here because these would be + // leftovers when starting from the least significant bits. In either case, tag the four least + // significant bits with our stop bit. + match (n >> QUAD_SHIFT) as usize { + // Eat leading zero-bits. This should not be done if the first four bits were non-zero. + // Additionally, we *must* do this in increments of five bits. + 0 => { + n <<= QUAD_RESET; + n |= 1; + n <<= n.leading_zeros() / 5 * 5; + } + + // Write value of first four bytes. + i => { + n <<= QUAD_RESET; + n |= 1; + buf.push(char::from(BASE32_ALPHABET[i])); + } + } + + // From now until we reach the stop bit, take the five most significant bits and then shift + // left by five bits. + while n != STOP_BIT { + buf.push(char::from(BASE32_ALPHABET[(n >> FIVE_SHIFT) as usize])); + n <<= FIVE_RESET; + } + + buf + } + } + } + + pub fn from_parts(prefix_id: DocumentId, doc_id: DocumentId) -> Id { + Id { + id: (prefix_id as u64) << 32 | doc_id as u64, + } + } + + pub fn id(&self) -> u64 { + self.id + } + + pub fn document_id(&self) -> DocumentId { + (self.id & 0xFFFFFFFF) as DocumentId + } + + pub fn prefix_id(&self) -> DocumentId { + (self.id >> 32) as DocumentId + } + + pub fn is_singleton(&self) -> bool { + self.id == 20080258862541 + } +} + +impl From<u64> for Id { + fn from(id: u64) -> Self { + Id { id } + } +} + +impl From<u32> for Id { + fn from(id: u32) -> Self { + Id { id: id as u64 } + } +} + +impl From<Id> for u64 { + fn from(id: Id) -> Self { + id.id + } +} + +impl From<&Id> for u64 { + fn from(id: &Id) -> Self { + id.id + } +} + +impl From<(u32, u32)> for Id { + fn from(id: (u32, u32)) -> Self { + Id::from_parts(id.0, id.1) + } +} + +impl Deref for Id { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.id + } +} + +impl AsRef<u64> for Id { + fn as_ref(&self) -> &u64 { + &self.id + } +} + +impl From<Id> for u32 { + fn from(id: Id) -> Self { + id.document_id() + } +} + +impl From<Id> for String { + fn from(id: Id) -> Self { + id.as_string() + } +} + +impl IntoBitmap for Id { + fn into_bitmap(self) -> (Vec<u8>, u8) { + (self.serialize(), BM_TAG | TAG_ID) + } +} + +impl serde::Serialize for Id { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_string().as_str()) + } +} + +impl std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_string()) + } +} + +#[cfg(test)] +mod tests { + use crate::{parser::json::Parser, types::id::Id}; + + #[test] + fn parse_jmap_id() { + for number in [ + 0, + 1, + 10, + 1000, + Id::singleton().id, + u64::MAX / 2, + u64::MAX - 1, + u64::MAX, + ] { + let id = Id::from(number); + assert_eq!( + Parser::new(format!("\"{id}\"").as_bytes()) + .next_token::<Id>() + .unwrap() + .unwrap_string("") + .unwrap(), + id + ); + } + + Parser::new(b"\"p333333333333p333333333333\"") + .next_token::<Id>() + .unwrap() + .unwrap_string("") + .unwrap(); + } +} diff --git a/crates/jmap-proto/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs new file mode 100644 index 00000000..0126483d --- /dev/null +++ b/crates/jmap-proto/src/types/keyword.rs @@ -0,0 +1,215 @@ +use std::fmt::Display; + +use store::{write::IntoBitmap, BM_TAG, TAG_STATIC, TAG_TEXT}; +use utils::codec::leb128::{Leb128Iterator, Leb128Vec}; + +use crate::{ + object::{DeserializeValue, SerializeValue}, + parser::{json::Parser, JsonObjectParser}, +}; + +pub const SEEN: u8 = 0; +pub const DRAFT: u8 = 1; +pub const FLAGGED: u8 = 2; +pub const ANSWERED: u8 = 3; +pub const RECENT: u8 = 4; +pub const IMPORTANT: u8 = 5; +pub const PHISHING: u8 = 6; +pub const JUNK: u8 = 7; +pub const NOTJUNK: u8 = 8; +pub const DELETED: u8 = 9; +pub const FORWARDED: u8 = 10; +pub const MDN_SENT: u8 = 11; +pub const OTHER: u8 = 12; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +#[serde(untagged)] +pub enum Keyword { + #[serde(rename(serialize = "$seen"))] + Seen, + #[serde(rename(serialize = "$draft"))] + Draft, + #[serde(rename(serialize = "$flagged"))] + Flagged, + #[serde(rename(serialize = "$answered"))] + Answered, + #[serde(rename(serialize = "$recent"))] + Recent, + #[serde(rename(serialize = "$important"))] + Important, + #[serde(rename(serialize = "$phishing"))] + Phishing, + #[serde(rename(serialize = "$junk"))] + Junk, + #[serde(rename(serialize = "$notjunk"))] + NotJunk, + #[serde(rename(serialize = "$deleted"))] + Deleted, + #[serde(rename(serialize = "$forwarded"))] + Forwarded, + #[serde(rename(serialize = "$mdnsent"))] + MdnSent, + Other(String), +} + +impl JsonObjectParser for Keyword { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + if parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())? + == b'$' + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + break; + } + } + + match hash { + 0x6e65_6573 => return Ok(Keyword::Seen), + 0x0074_6661_7264 => return Ok(Keyword::Draft), + 0x6465_6767_616c_66 => return Ok(Keyword::Flagged), + 0x6465_7265_7773_6e61 => return Ok(Keyword::Answered), + 0x746e_6563_6572 => return Ok(Keyword::Recent), + 0x746e_6174_726f_706d_69 => return Ok(Keyword::Important), + 0x676e_6968_7369_6870 => return Ok(Keyword::Phishing), + 0x6b6e_756a => return Ok(Keyword::Junk), + 0x6b6e_756a_746f_6e => return Ok(Keyword::NotJunk), + 0x0064_6574_656c_6564 => return Ok(Keyword::Deleted), + 0x6465_6472_6177_726f_66 => return Ok(Keyword::Forwarded), + 0x746e_6573_6e64_6d => return Ok(Keyword::MdnSent), + _ => (), + } + } + + if parser.is_eof || parser.skip_string() { + Ok(Keyword::Other( + String::from_utf8_lossy(parser.bytes[parser.pos_marker..parser.pos - 1].as_ref()) + .into_owned(), + )) + } else { + Err(parser.error_unterminated()) + } + } +} + +impl Display for Keyword { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Keyword::Seen => write!(f, "$seen"), + Keyword::Draft => write!(f, "$draft"), + Keyword::Flagged => write!(f, "$flagged"), + Keyword::Answered => write!(f, "$answered"), + Keyword::Recent => write!(f, "$recent"), + Keyword::Important => write!(f, "$important"), + Keyword::Phishing => write!(f, "$phishing"), + Keyword::Junk => write!(f, "$junk"), + Keyword::NotJunk => write!(f, "$notjunk"), + Keyword::Deleted => write!(f, "$deleted"), + Keyword::Forwarded => write!(f, "$forwarded"), + Keyword::MdnSent => write!(f, "$mdnsent"), + Keyword::Other(s) => write!(f, "{}", s), + } + } +} + +impl IntoBitmap for &Keyword { + fn into_bitmap(self) -> (Vec<u8>, u8) { + match self { + Keyword::Seen => (vec![SEEN], BM_TAG | TAG_STATIC), + Keyword::Draft => (vec![DRAFT], BM_TAG | TAG_STATIC), + Keyword::Flagged => (vec![FLAGGED], BM_TAG | TAG_STATIC), + Keyword::Answered => (vec![ANSWERED], BM_TAG | TAG_STATIC), + Keyword::Recent => (vec![RECENT], BM_TAG | TAG_STATIC), + Keyword::Important => (vec![IMPORTANT], BM_TAG | TAG_STATIC), + Keyword::Phishing => (vec![PHISHING], BM_TAG | TAG_STATIC), + Keyword::Junk => (vec![JUNK], BM_TAG | TAG_STATIC), + Keyword::NotJunk => (vec![NOTJUNK], BM_TAG | TAG_STATIC), + Keyword::Deleted => (vec![DELETED], BM_TAG | TAG_STATIC), + Keyword::Forwarded => (vec![FORWARDED], BM_TAG | TAG_STATIC), + Keyword::MdnSent => (vec![MDN_SENT], BM_TAG | TAG_STATIC), + Keyword::Other(string) => (string.as_bytes().to_vec(), BM_TAG | TAG_TEXT), + } + } +} + +impl IntoBitmap for Keyword { + fn into_bitmap(self) -> (Vec<u8>, u8) { + match self { + Keyword::Seen => (vec![SEEN], BM_TAG | TAG_STATIC), + Keyword::Draft => (vec![DRAFT], BM_TAG | TAG_STATIC), + Keyword::Flagged => (vec![FLAGGED], BM_TAG | TAG_STATIC), + Keyword::Answered => (vec![ANSWERED], BM_TAG | TAG_STATIC), + Keyword::Recent => (vec![RECENT], BM_TAG | TAG_STATIC), + Keyword::Important => (vec![IMPORTANT], BM_TAG | TAG_STATIC), + Keyword::Phishing => (vec![PHISHING], BM_TAG | TAG_STATIC), + Keyword::Junk => (vec![JUNK], BM_TAG | TAG_STATIC), + Keyword::NotJunk => (vec![NOTJUNK], BM_TAG | TAG_STATIC), + Keyword::Deleted => (vec![DELETED], BM_TAG | TAG_STATIC), + Keyword::Forwarded => (vec![FORWARDED], BM_TAG | TAG_STATIC), + Keyword::MdnSent => (vec![MDN_SENT], BM_TAG | TAG_STATIC), + Keyword::Other(string) => (string.into_bytes(), BM_TAG | TAG_TEXT), + } + } +} + +impl SerializeValue for Keyword { + fn serialize_value(self, buf: &mut Vec<u8>) { + match self { + Keyword::Seen => buf.push(SEEN), + Keyword::Draft => buf.push(DRAFT), + Keyword::Flagged => buf.push(FLAGGED), + Keyword::Answered => buf.push(ANSWERED), + Keyword::Recent => buf.push(RECENT), + Keyword::Important => buf.push(IMPORTANT), + Keyword::Phishing => buf.push(PHISHING), + Keyword::Junk => buf.push(JUNK), + Keyword::NotJunk => buf.push(NOTJUNK), + Keyword::Deleted => buf.push(DELETED), + Keyword::Forwarded => buf.push(FORWARDED), + Keyword::MdnSent => buf.push(MDN_SENT), + Keyword::Other(string) => { + buf.push_leb128(OTHER as usize + string.len()); + if !string.is_empty() { + buf.extend_from_slice(string.as_bytes()) + } + } + } + } +} + +impl DeserializeValue for Keyword { + fn deserialize_value(bytes: &mut std::slice::Iter<'_, u8>) -> Option<Self> { + match *bytes.next()? { + SEEN => Some(Keyword::Seen), + DRAFT => Some(Keyword::Draft), + FLAGGED => Some(Keyword::Flagged), + ANSWERED => Some(Keyword::Answered), + RECENT => Some(Keyword::Recent), + IMPORTANT => Some(Keyword::Important), + PHISHING => Some(Keyword::Phishing), + JUNK => Some(Keyword::Junk), + NOTJUNK => Some(Keyword::NotJunk), + DELETED => Some(Keyword::Deleted), + FORWARDED => Some(Keyword::Forwarded), + MDN_SENT => Some(Keyword::MdnSent), + _ => { + let len = bytes.next_leb128::<usize>()? - OTHER as usize; + let mut keyword = Vec::with_capacity(len); + for _ in 0..len { + keyword.push(*bytes.next()?); + } + Some(Keyword::Other(String::from_utf8(keyword).ok()?)) + } + } + } +} diff --git a/crates/jmap-proto/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs new file mode 100644 index 00000000..067eb2bc --- /dev/null +++ b/crates/jmap-proto/src/types/mod.rs @@ -0,0 +1,14 @@ +pub mod acl; +pub mod blob; +pub mod collection; +pub mod date; +pub mod id; +pub mod keyword; +pub mod pointer; +pub mod property; +pub mod state; +pub mod type_state; +pub mod value; + +pub type DocumentId = u32; +pub type ChangeId = u64; diff --git a/crates/jmap-proto/src/types/pointer.rs b/crates/jmap-proto/src/types/pointer.rs new file mode 100644 index 00000000..1860fd9d --- /dev/null +++ b/crates/jmap-proto/src/types/pointer.rs @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use crate::parser::{json::Parser, JsonObjectParser}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] +pub enum JSONPointer { + Root, + Wildcard, + String(String), + Number(u64), + Path(Vec<JSONPointer>), +} + +pub trait JSONPointerEval { + fn eval_json_pointer(&self, ptr: &JSONPointer) -> Option<Vec<u64>>; +} + +enum TokenType { + Unknown, + Number, + String, + Wildcard, + Escaped, +} + +impl JsonObjectParser for JSONPointer { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut path = Vec::new(); + let mut num = 0u64; + let mut buf = Vec::new(); + let mut token = TokenType::Unknown; + let mut start_pos = parser.pos; + + while let Some(ch) = parser.next_char() { + match (ch, &token) { + (b'0'..=b'9', TokenType::Unknown | TokenType::Number) => { + num = num.saturating_mul(10).saturating_add((ch - b'0') as u64); + token = TokenType::Number; + } + (b'*', TokenType::Unknown) => { + token = TokenType::Wildcard; + } + (b'0', TokenType::Escaped) => { + buf.push(b'~'); + token = TokenType::String; + } + (b'1', TokenType::Escaped) => { + buf.push(b'/'); + token = TokenType::String; + } + (b'/' | b'"', _) => { + match token { + TokenType::String => { + path.push(JSONPointer::String( + String::from_utf8(buf).map_err(|_| parser.error_utf8())?, + )); + buf = Vec::new(); + } + TokenType::Number => { + path.push(JSONPointer::Number(num)); + num = 0; + } + TokenType::Wildcard => { + path.push(JSONPointer::Wildcard); + } + TokenType::Unknown if parser.pos_marker != start_pos => { + path.push(JSONPointer::String(String::new())); + } + _ => (), + } + + if ch == b'/' { + token = TokenType::Unknown; + start_pos = parser.pos; + } else { + parser.is_eof = true; + return Ok(match path.len() { + 1 => path.pop().unwrap(), + 0 => JSONPointer::Root, + _ => JSONPointer::Path(path), + }); + } + } + (_, _) => { + if matches!(&token, TokenType::Number | TokenType::Wildcard) + && parser.pos - 1 > start_pos + { + buf.extend_from_slice( + parser + .bytes + .get(start_pos..parser.pos - 1) + .unwrap_or_default(), + ); + } + + token = match ch { + b'~' if !matches!(&token, TokenType::Escaped) => TokenType::Escaped, + b'\\' => { + buf.push(parser.next_char().unwrap_or(b'\\')); + TokenType::String + } + _ => { + buf.push(ch); + TokenType::String + } + }; + } + } + } + + Err(parser.error_unterminated()) + } +} + +impl JSONPointer { + pub fn to_string(&self) -> Option<&str> { + match self { + JSONPointer::String(s) => s.as_str().into(), + _ => None, + } + } + + pub fn unwrap_string(self) -> Option<String> { + match self { + JSONPointer::String(s) => s.into(), + _ => None, + } + } + + pub fn item_query(&self) -> Option<&str> { + match self { + JSONPointer::String(property) => property.as_str().into(), + JSONPointer::Path(path) if path.len() == 2 => { + if let (Some(JSONPointer::String(property)), Some(JSONPointer::Wildcard)) = + (path.get(0), path.get(1)) + { + property.as_str().into() + } else { + None + } + } + _ => None, + } + } + + pub fn item_subquery(&self) -> Option<(&str, &str)> { + match self { + JSONPointer::Path(path) if path.len() == 3 => { + match (path.get(0), path.get(1), path.get(2)) { + ( + Some(JSONPointer::String(root)), + Some(JSONPointer::Wildcard), + Some(JSONPointer::String(property)), + ) => Some((root.as_str(), property.as_str())), + _ => None, + } + } + _ => None, + } + } +} + +impl Display for JSONPointer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JSONPointer::Root => write!(f, "/"), + JSONPointer::Wildcard => write!(f, "*"), + JSONPointer::String(s) => write!(f, "{}", s), + JSONPointer::Number(n) => write!(f, "{}", n), + JSONPointer::Path(path) => { + for (i, ptr) in path.iter().enumerate() { + if i > 0 { + write!(f, "/")?; + } + write!(f, "{}", ptr)?; + } + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::parser::json::Parser; + + use super::JSONPointer; + + #[test] + fn json_pointer_parse() { + for (input, output) in vec![ + ("hello", JSONPointer::String("hello".to_string())), + ("9a", JSONPointer::String("9a".to_string())), + ("a9", JSONPointer::String("a9".to_string())), + ("*a", JSONPointer::String("*a".to_string())), + ( + "/hello/world", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::String("world".to_string()), + ]), + ), + ("*", JSONPointer::Wildcard), + ( + "/hello/*", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::Wildcard, + ]), + ), + ("1234", JSONPointer::Number(1234)), + ( + "/hello/1234", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::Number(1234), + ]), + ), + ("~0~1", JSONPointer::String("~/".to_string())), + ( + "/hello/~0~1", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::String("~/".to_string()), + ]), + ), + ( + "/hello/1~0~1/*~1~0", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::String("1~/".to_string()), + JSONPointer::String("*/~".to_string()), + ]), + ), + ( + "/hello/world/*/99", + JSONPointer::Path(vec![ + JSONPointer::String("hello".to_string()), + JSONPointer::String("world".to_string()), + JSONPointer::Wildcard, + JSONPointer::Number(99), + ]), + ), + ("/", JSONPointer::String("".to_string())), + ( + "///", + JSONPointer::Path(vec![ + JSONPointer::String("".to_string()), + JSONPointer::String("".to_string()), + JSONPointer::String("".to_string()), + ]), + ), + ("", JSONPointer::Root), + ] { + assert_eq!( + Parser::new(format!("\"{input}\"").as_bytes()) + .next_token::<JSONPointer>() + .unwrap() + .unwrap_string("") + .unwrap(), + output, + "{input}" + ); + } + } +} diff --git a/crates/jmap-proto/src/types/property.rs b/crates/jmap-proto/src/types/property.rs new file mode 100644 index 00000000..f416e869 --- /dev/null +++ b/crates/jmap-proto/src/types/property.rs @@ -0,0 +1,1086 @@ +use std::fmt::{Display, Formatter}; + +use mail_parser::RfcHeader; +use serde::Serialize; + +use crate::{ + object::{DeserializeValue, SerializeValue}, + parser::{json::Parser, Error, JsonObjectParser}, +}; + +use super::{acl::Acl, id::Id, keyword::Keyword, value::Value}; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] +pub enum Property { + Acl, + Aliases, + Attachments, + Bcc, + BlobId, + BodyStructure, + BodyValues, + Capabilities, + Cc, + Charset, + Cid, + DeliveryStatus, + Description, + DeviceClientId, + Disposition, + DsnBlobIds, + Email, + EmailId, + EmailIds, + Envelope, + Expires, + From, + FromDate, + HasAttachment, + Header(HeaderProperty), + Headers, + HtmlBody, + HtmlSignature, + Id, + IdentityId, + InReplyTo, + IsActive, + IsEnabled, + IsSubscribed, + Keys, + Keywords, + Language, + Location, + MailboxIds, + MayDelete, + MdnBlobIds, + Members, + MessageId, + MyRights, + Name, + ParentId, + PartId, + Picture, + Preview, + Quota, + ReceivedAt, + References, + ReplyTo, + Role, + Secret, + SendAt, + Sender, + SentAt, + Size, + SortOrder, + Subject, + SubParts, + TextBody, + TextSignature, + ThreadId, + Timezone, + To, + ToDate, + TotalEmails, + TotalThreads, + Type, + Types, + UndoStatus, + UnreadEmails, + UnreadThreads, + Url, + VerificationCode, + Addresses, + P256dh, + Auth, + Value, + SmtpReply, + Delivered, + Displayed, + MailFrom, + RcptTo, + Parameters, + IsEncodingProblem, + IsTruncated, + MayReadItems, + MayAddItems, + MayRemoveItems, + MaySetSeen, + MaySetKeywords, + MayCreateChild, + MayRename, + MaySubmit, + _T(String), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SetProperty { + pub property: Property, + pub patch: Vec<Value>, + pub is_ref: bool, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ObjectProperty(Property); + +pub trait IntoProperty: Eq + Display { + fn into_property(self) -> Property; +} + +impl JsonObjectParser for Property { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + let mut first_char = 0; + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if ch.is_ascii_alphabetic() { + if first_char != 0 { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return parser.invalid_property(); + } + } else { + first_char = ch; + } + } else if ch == b':' && first_char == b'h' && hash == 0x7265_6461_65 { + return parse_header_property(parser); + } else { + return parser.invalid_property(); + } + } + + if let Some(property) = parse_property(first_char, hash) { + Ok(property) + } else { + parser.invalid_property() + } + } +} + +impl JsonObjectParser for SetProperty { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + let mut first_char = 0; + let mut hash = 0; + let mut shift = 0; + let mut is_ref = false; + let mut is_patch = false; + + while let Some(ch) = parser.next_unescaped()? { + if ch.is_ascii_alphabetic() { + if first_char != 0 { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return parser.invalid_property().map(|property| SetProperty { + property, + patch: vec![], + is_ref: false, + }); + } + } else { + first_char = ch; + } + } else { + match ch { + b'#' if first_char == 0 && !is_ref => is_ref = true, + b'/' if !is_ref => { + is_patch = true; + break; + } + b':' if first_char == b'h' && hash == 0x7265_6461_65 && !is_ref => { + return parse_header_property(parser).map(|property| SetProperty { + property, + patch: vec![], + is_ref: false, + }); + } + _ => { + return parser.invalid_property().map(|property| SetProperty { + property, + patch: vec![], + is_ref: false, + }); + } + } + } + } + + let mut property = if let Some(property) = parse_property(first_char, hash) { + property + } else { + parser.invalid_property()? + }; + let mut patch = Vec::new(); + + if is_patch { + match &property { + Property::MailboxIds | Property::Members => match Id::parse(parser) { + Ok(id) => { + patch.push(Value::Id(id)); + } + Err(Error::Method(_)) => { + property = parser.invalid_property()?; + } + Err(err) => { + return Err(err); + } + }, + Property::Keywords => match Keyword::parse(parser) { + Ok(keyword) => { + patch.push(Value::Keyword(keyword)); + } + Err(Error::Method(_)) => { + property = parser.invalid_property()?; + } + Err(err) => { + return Err(err); + } + }, + Property::Acl => { + let mut has_acl = false; + let mut account = Vec::with_capacity(16); + + while let Some(ch) = parser.next_unescaped()? { + if ch != b'/' { + account.push(ch); + } else { + has_acl = true; + break; + } + } + + match String::from_utf8(account) { + Ok(account) if !account.is_empty() => { + patch.push(Value::Text(account)); + if has_acl { + match Acl::parse(parser) { + Ok(acl) => { + patch.push(Value::Acl(acl)); + } + Err(Error::Method(_)) => { + property = parser.invalid_property()?; + } + Err(err) => { + return Err(err); + } + } + } + } + _ => { + property = parser.invalid_property()?; + } + } + } + Property::Aliases => match String::parse(parser) { + Ok(text) if !text.is_empty() => { + patch.push(Value::Text(text)); + } + Err(err) => { + return Err(err); + } + _ => { + property = parser.invalid_property()?; + } + }, + _ => { + property = parser.invalid_property()?; + } + } + } + + Ok(SetProperty { + property, + patch, + is_ref, + }) + } +} + +fn parse_property(first_char: u8, hash: u128) -> Option<Property> { + Some(match first_char { + b'a' => match hash { + 0x6c63 => Property::Acl, + 0x7365_7361_696c => Property::Aliases, + 0x7374_6e65_6d68_6361_7474 => Property::Attachments, + _ => return None, + }, + b'b' => match hash { + 0x6363 => Property::Bcc, + 0x6449_626f_6c => Property::BlobId, + 0x6572_7574_6375_7274_5379_646f => Property::BodyStructure, + 0x7365_756c_6156_7964_6f => Property::BodyValues, + _ => return None, + }, + b'c' => match hash { + 0x7365_6974_696c_6962_6170_61 => Property::Capabilities, + 0x63 => Property::Cc, + 0x7465_7372_6168 => Property::Charset, + 0x6469 => Property::Cid, + _ => return None, + }, + b'd' => match hash { + 0x7375_7461_7453_7972_6576_696c_65 => Property::DeliveryStatus, + 0x6e6f_6974_7069_7263_7365 => Property::Description, + 0x6449_746e_6569_6c43_6563_6976_65 => Property::DeviceClientId, + 0x6e6f_6974_6973_6f70_7369 => Property::Disposition, + 0x7364_4962_6f6c_426e_73 => Property::DsnBlobIds, + _ => return None, + }, + b'e' => match hash { + 0x6c69_616d => Property::Email, + 0x6449_6c69_616d => Property::EmailId, + 0x7364_496c_6961_6d => Property::EmailIds, + 0x6570_6f6c_6576_6e => Property::Envelope, + 0x7365_7269_7078 => Property::Expires, + _ => return None, + }, + b'f' => match hash { + 0x6d6f_72 => Property::From, + 0x6574_6144_6d6f_72 => Property::FromDate, + _ => return None, + }, + b'h' => match hash { + 0x746e_656d_6863_6174_7441_7361 => Property::HasAttachment, + 0x7372_6564_6165 => Property::Headers, + 0x7964_6f42_6c6d_74 => Property::HtmlBody, + 0x6572_7574_616e_6769_536c_6d74 => Property::HtmlSignature, + _ => return None, + }, + b'i' => match hash { + 0x64 => Property::Id, + 0x0064_4979_7469_746e_6564 => Property::IdentityId, + 0x6f54_796c_7065_526e => Property::InReplyTo, + 0x6576_6974_6341_73 => Property::IsActive, + 0x6465_6c62_616e_4573 => Property::IsEnabled, + 0x6465_6269_7263_7362_7553_73 => Property::IsSubscribed, + _ => return None, + }, + b'k' => match hash { + 0x7379_65 => Property::Keys, + 0x7364_726f_7779_65 => Property::Keywords, + _ => return None, + }, + b'l' => match hash { + 0x6567_6175_676e_61 => Property::Language, + 0x6e6f_6974_6163_6f => Property::Location, + _ => return None, + }, + b'm' => match hash { + 0x7364_4978_6f62_6c69_61 => Property::MailboxIds, + 0x6574_656c_6544_7961 => Property::MayDelete, + 0x0073_6449_626f_6c42_6e64 => Property::MdnBlobIds, + 0x7372_6562_6d65 => Property::Members, + 0x6449_6567_6173_7365 => Property::MessageId, + 0x7374_6867_6952_79 => Property::MyRights, + _ => return None, + }, + b'n' => match hash { + 0x656d_61 => Property::Name, + _ => return None, + }, + b'p' => match hash { + 0x6449_746e_6572_61 => Property::ParentId, + 0x6449_7472_61 => Property::PartId, + 0x6572_7574_6369 => Property::Picture, + 0x7765_6976_6572 => Property::Preview, + _ => return None, + }, + b'q' => match hash { + 0x6174_6f75 => Property::Quota, + _ => return None, + }, + b'r' => match hash { + 0x7441_6465_7669_6563_65 => Property::ReceivedAt, + 0x7365_636e_6572_6566_65 => Property::References, + 0x6f54_796c_7065 => Property::ReplyTo, + 0x656c_6f => Property::Role, + _ => return None, + }, + b's' => match hash { + 0x7465_7263_65 => Property::Secret, + 0x7441_646e_65 => Property::SendAt, + 0x7265_646e_65 => Property::Sender, + 0x7441_746e_65 => Property::SentAt, + 0x657a_69 => Property::Size, + 0x7265_6472_4f74_726f => Property::SortOrder, + 0x7463_656a_6275 => Property::Subject, + 0x7374_7261_5062_7573 => Property::SubParts, + _ => return None, + }, + b't' => match hash { + 0x7964_6f42_7478_65 => Property::TextBody, + 0x6572_7574_616e_6769_5374_7865 => Property::TextSignature, + 0x6449_6461_6572_68 => Property::ThreadId, + 0x656e_6f7a_656d_69 => Property::Timezone, + 0x6f => Property::To, + 0x6574_6144_6f => Property::ToDate, + 0x736c_6961_6d45_6c61_746f => Property::TotalEmails, + 0x7364_6165_7268_546c_6174_6f => Property::TotalThreads, + 0x6570_79 => Property::Type, + 0x7365_7079 => Property::Types, + _ => return None, + }, + b'u' => match hash { + 0x7375_7461_7453_6f64_6e => Property::UndoStatus, + 0x736c_6961_6d45_6461_6572_6e => Property::UnreadEmails, + 0x7364_6165_7268_5464_6165_726e => Property::UnreadThreads, + 0x6c72 => Property::Url, + _ => return None, + }, + b'v' => match hash { + 0x6564_6f43_6e6f_6974_6163_6966_6972_65 => Property::VerificationCode, + _ => return None, + }, + _ => return None, + }) +} + +fn parse_header_property(parser: &mut Parser) -> crate::parser::Result<Property> { + let hdr_start_pos = parser.pos; + let mut has_next = false; + + while let Some(ch) = parser.next_unescaped()? { + if ch == b':' { + has_next = true; + break; + } + } + + let mut all = false; + let mut form = HeaderForm::Raw; + let header = if parser.pos > hdr_start_pos + 1 { + String::from_utf8_lossy(&parser.bytes[hdr_start_pos..parser.pos - 1]).into_owned() + } else { + return parser.invalid_property(); + }; + + if has_next { + match (parser.next_unescaped()?, parser.next_unescaped()?) { + (Some(b'a'), Some(b's')) => { + let mut hash = 0; + let mut shift = 0; + has_next = false; + + while let Some(ch) = parser.next_unescaped()? { + if ch != b':' { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return parser.invalid_property(); + } + } else { + has_next = true; + break; + } + } + + form = match hash { + 0x7478_6554 => HeaderForm::Text, + 0x7365_7373_6572_6464_41 => HeaderForm::Addresses, + 0x7365_7373_6572_6464_4164_6570_756f_7247 => HeaderForm::GroupedAddresses, + 0x7364_4965_6761_7373_654d => HeaderForm::MessageIds, + 0x6574_6144 => HeaderForm::Date, + 0x734c_5255 => HeaderForm::URLs, + 0x7761_52 => HeaderForm::Raw, + _ => return parser.invalid_property(), + }; + + if has_next { + for ch in b"all" { + if Some(*ch) != parser.next_unescaped()? { + return parser.invalid_property(); + } + } + if parser.next_unescaped()?.is_none() { + all = true; + } else { + return parser.invalid_property(); + } + } + } + (Some(b'a'), Some(b'l')) => { + if let (Some(b'l'), None) = (parser.next_unescaped()?, parser.next_unescaped()?) { + all = true; + } else { + return parser.invalid_property(); + } + } + _ => { + return parser.invalid_property(); + } + } + } + + Ok(Property::Header(HeaderProperty { form, header, all })) +} + +impl JsonObjectParser for ObjectProperty { + fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + let mut first_char = 0; + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if ch.is_ascii_alphabetic() { + if first_char != 0 { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + break; + } + } else { + first_char = ch; + } + } else if ch == b':' && first_char == b'h' && hash == 0x7265_6461_65 { + return parse_header_property(parser).map(ObjectProperty); + } else { + return parser.invalid_property().map(ObjectProperty); + } + } + + Ok(ObjectProperty(match first_char { + b'a' => match hash { + 0x7365_7373_6572_6464 => Property::Addresses, + 0x6874_75 => Property::Auth, + _ => parser.invalid_property()?, + }, + b'b' => match hash { + 0x6449_626f_6c => Property::BlobId, + _ => parser.invalid_property()?, + }, + b'c' => match hash { + 0x7465_7372_6168 => Property::Charset, + 0x6469 => Property::Cid, + _ => parser.invalid_property()?, + }, + b'd' => match hash { + 0x6e6f_6974_6973_6f70_7369 => Property::Disposition, + 0x6465_7265_7669_6c65 => Property::Delivered, + 0x6465_7961_6c70_7369 => Property::Displayed, + _ => parser.invalid_property()?, + }, + b'e' => match hash { + 0x6c69_616d => Property::Email, + _ => parser.invalid_property()?, + }, + b'h' => match hash { + 0x7372_6564_6165 => Property::Headers, + _ => parser.invalid_property()?, + }, + b'i' => match hash { + 0x656c_626f_7250_676e_6964_6f63_6e45_73 => Property::IsEncodingProblem, + 0x6465_7461_636e_7572_5473 => Property::IsTruncated, + _ => parser.invalid_property()?, + }, + b'l' => match hash { + 0x6567_6175_676e_61 => Property::Language, + 0x6e6f_6974_6163_6f => Property::Location, + _ => parser.invalid_property()?, + }, + b'm' => match hash { + 0x6d6f_7246_6c69_61 => Property::MailFrom, + 0x736d_6574_4964_6165_5279_61 => Property::MayReadItems, + 0x736d_6574_4964_6441_7961 => Property::MayAddItems, + 0x736d_6574_4965_766f_6d65_5279_61 => Property::MayRemoveItems, + 0x6e65_6553_7465_5379_61 => Property::MaySetSeen, + 0x7364_726f_7779_654b_7465_5379_61 => Property::MaySetKeywords, + 0x646c_6968_4365_7461_6572_4379_61 => Property::MayCreateChild, + 0x656d_616e_6552_7961 => Property::MayRename, + 0x6574_656c_6544_7961 => Property::MayDelete, + 0x7469_6d62_7553_7961 => Property::MaySubmit, + _ => parser.invalid_property()?, + }, + b'n' => match hash { + 0x656d_61 => Property::Name, + _ => parser.invalid_property()?, + }, + b'p' => match hash { + 0x6449_7472_61 => Property::PartId, + 0x0068_6436_3532 => Property::P256dh, + 0x7372_6574_656d_6172_61 => Property::Parameters, + _ => parser.invalid_property()?, + }, + b'r' => match hash { + 0x6f54_7470_63 => Property::RcptTo, + _ => parser.invalid_property()?, + }, + b's' => match hash { + 0x657a_69 => Property::Size, + 0x7374_7261_5062_75 => Property::SubParts, + 0x796c_7065_5270_746d => Property::SmtpReply, + _ => parser.invalid_property()?, + }, + b't' => match hash { + 0x6570_79 => Property::Type, + _ => parser.invalid_property()?, + }, + b'v' => match hash { + 0x6575_6c61 => Property::Value, + _ => parser.invalid_property()?, + }, + _ => parser.invalid_property()?, + })) + } +} + +impl<'x> Parser<'x> { + fn invalid_property(&mut self) -> crate::parser::Result<Property> { + if self.is_eof || self.skip_string() { + Ok(Property::_T( + String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()) + .into_owned(), + )) + } else { + Err(self.error_unterminated()) + } + } +} + +impl Property { + pub fn parse(value: &str) -> Property { + let mut first_char = 0; + let mut hash = 0; + let mut shift = 0; + + for &ch in value.as_bytes() { + if ch.is_ascii_alphabetic() { + if first_char != 0 { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return Property::_T(value.to_string()); + } + } else { + first_char = ch; + } + } else { + return Property::_T(value.to_string()); + } + } + + if let Some(property) = parse_property(first_char, hash) { + property + } else { + Property::_T(value.to_string()) + } + } +} + +impl Display for Property { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Property::Acl => write!(f, "acl"), + Property::Aliases => write!(f, "aliases"), + Property::Attachments => write!(f, "attachments"), + Property::Bcc => write!(f, "bcc"), + Property::BlobId => write!(f, "blobId"), + Property::BodyStructure => write!(f, "bodyStructure"), + Property::BodyValues => write!(f, "bodyValues"), + Property::Capabilities => write!(f, "capabilities"), + Property::Cc => write!(f, "cc"), + Property::Charset => write!(f, "charset"), + Property::Cid => write!(f, "cid"), + Property::DeliveryStatus => write!(f, "deliveryStatus"), + Property::Description => write!(f, "description"), + Property::DeviceClientId => write!(f, "deviceClientId"), + Property::Disposition => write!(f, "disposition"), + Property::DsnBlobIds => write!(f, "dsnBlobIds"), + Property::Email => write!(f, "email"), + Property::EmailId => write!(f, "emailId"), + Property::EmailIds => write!(f, "emailIds"), + Property::Envelope => write!(f, "envelope"), + Property::Expires => write!(f, "expires"), + Property::From => write!(f, "from"), + Property::FromDate => write!(f, "fromDate"), + Property::HasAttachment => write!(f, "hasAttachment"), + Property::Header(p) => write!(f, "{p}"), + Property::Headers => write!(f, "headers"), + Property::HtmlBody => write!(f, "htmlBody"), + Property::HtmlSignature => write!(f, "htmlSignature"), + Property::Id => write!(f, "id"), + Property::IdentityId => write!(f, "identityId"), + Property::InReplyTo => write!(f, "inReplyTo"), + Property::IsActive => write!(f, "isActive"), + Property::IsEnabled => write!(f, "isEnabled"), + Property::IsSubscribed => write!(f, "isSubscribed"), + Property::Keys => write!(f, "keys"), + Property::Keywords => write!(f, "keywords"), + Property::Language => write!(f, "language"), + Property::Location => write!(f, "location"), + Property::MailboxIds => write!(f, "mailboxIds"), + Property::MayDelete => write!(f, "mayDelete"), + Property::MdnBlobIds => write!(f, "mdnBlobIds"), + Property::Members => write!(f, "members"), + Property::MessageId => write!(f, "messageId"), + Property::MyRights => write!(f, "myRights"), + Property::Name => write!(f, "name"), + Property::ParentId => write!(f, "parentId"), + Property::PartId => write!(f, "partId"), + Property::Picture => write!(f, "picture"), + Property::Preview => write!(f, "preview"), + Property::Quota => write!(f, "quota"), + Property::ReceivedAt => write!(f, "receivedAt"), + Property::References => write!(f, "references"), + Property::ReplyTo => write!(f, "replyTo"), + Property::Role => write!(f, "role"), + Property::Secret => write!(f, "secret"), + Property::SendAt => write!(f, "sendAt"), + Property::Sender => write!(f, "sender"), + Property::SentAt => write!(f, "sentAt"), + Property::Size => write!(f, "size"), + Property::SortOrder => write!(f, "sortOrder"), + Property::Subject => write!(f, "subject"), + Property::SubParts => write!(f, "subParts"), + Property::TextBody => write!(f, "textBody"), + Property::TextSignature => write!(f, "textSignature"), + Property::ThreadId => write!(f, "threadId"), + Property::Timezone => write!(f, "timezone"), + Property::To => write!(f, "to"), + Property::ToDate => write!(f, "toDate"), + Property::TotalEmails => write!(f, "totalEmails"), + Property::TotalThreads => write!(f, "totalThreads"), + Property::Type => write!(f, "type"), + Property::Types => write!(f, "types"), + Property::UndoStatus => write!(f, "undoStatus"), + Property::UnreadEmails => write!(f, "unreadEmails"), + Property::UnreadThreads => write!(f, "unreadThreads"), + Property::Url => write!(f, "url"), + Property::VerificationCode => write!(f, "verificationCode"), + Property::Parameters => write!(f, "parameters"), + Property::Addresses => write!(f, "addresses"), + Property::P256dh => write!(f, "p256dh"), + Property::Auth => write!(f, "auth"), + Property::Value => write!(f, "value"), + Property::SmtpReply => write!(f, "smtpReply"), + Property::Delivered => write!(f, "delivered"), + Property::Displayed => write!(f, "displayed"), + Property::MailFrom => write!(f, "mailFrom"), + Property::RcptTo => write!(f, "rcptTo"), + Property::IsEncodingProblem => write!(f, "isEncodingProblem"), + Property::IsTruncated => write!(f, "isTruncated"), + Property::MayReadItems => write!(f, "mayReadItems"), + Property::MayAddItems => write!(f, "mayAddItems"), + Property::MayRemoveItems => write!(f, "mayRemoveItems"), + Property::MaySetSeen => write!(f, "maySetSeen"), + Property::MaySetKeywords => write!(f, "maySetKeywords"), + Property::MayCreateChild => write!(f, "mayCreateChild"), + Property::MayRename => write!(f, "mayRename"), + Property::MaySubmit => write!(f, "maySubmit"), + Property::_T(s) => write!(f, "{s}"), + } + } +} + +impl Display for SetProperty { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.property.fmt(f) + } +} + +impl Display for ObjectProperty { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl IntoProperty for ObjectProperty { + fn into_property(self) -> Property { + self.0 + } +} + +impl IntoProperty for String { + fn into_property(self) -> Property { + Property::_T(self) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize)] +pub struct HeaderProperty { + pub form: HeaderForm, + pub header: String, + pub all: bool, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +pub enum HeaderForm { + Raw, + Text, + Addresses, + GroupedAddresses, + MessageIds, + Date, + URLs, +} + +impl Display for HeaderProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "header:{}", self.header)?; + self.form.fmt(f)?; + if self.all { + write!(f, ":all") + } else { + Ok(()) + } + } +} + +impl Display for HeaderForm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HeaderForm::Raw => Ok(()), + HeaderForm::Text => write!(f, ":asText"), + HeaderForm::Addresses => write!(f, ":asAddresses"), + HeaderForm::GroupedAddresses => write!(f, ":asGroupedAddresses"), + HeaderForm::MessageIds => write!(f, ":asMessageIds"), + HeaderForm::Date => write!(f, ":asDate"), + HeaderForm::URLs => write!(f, ":asURLs"), + } + } +} + +impl From<Property> for u8 { + fn from(value: Property) -> Self { + match value { + Property::IsActive => 0, + Property::IsEnabled => 1, + Property::IsSubscribed => 2, + Property::Keys => 3, + Property::Keywords => 4, + Property::Language => 5, + Property::Location => 6, + Property::MailboxIds => 7, + Property::MayDelete => 8, + Property::MdnBlobIds => 9, + Property::Members => 10, + Property::MessageId => 11, + Property::MyRights => 12, + Property::Name => 13, + Property::ParentId => 14, + Property::PartId => 15, + Property::Picture => 16, + Property::Preview => 17, + Property::Quota => 18, + Property::ReceivedAt => 19, + Property::References => 20, + Property::ReplyTo => 21, + Property::Role => 22, + Property::Secret => 23, + Property::SendAt => 24, + Property::Sender => 25, + Property::SentAt => 26, + Property::Size => 27, + Property::SortOrder => 28, + Property::Subject => 29, + Property::SubParts => 30, + Property::TextBody => 31, + Property::TextSignature => 32, + Property::ThreadId => 33, + Property::Timezone => 34, + Property::To => 35, + Property::ToDate => 36, + Property::TotalEmails => 37, + Property::TotalThreads => 38, + Property::Type => 39, + Property::Types => 40, + Property::UndoStatus => 41, + Property::UnreadEmails => 42, + Property::UnreadThreads => 43, + Property::Url => 44, + Property::VerificationCode => 45, + Property::Parameters => 46, + Property::Addresses => 47, + Property::P256dh => 48, + Property::Auth => 49, + Property::Value => 50, + Property::SmtpReply => 51, + Property::Delivered => 52, + Property::Displayed => 53, + Property::MailFrom => 54, + Property::RcptTo => 55, + Property::IsEncodingProblem => 56, + Property::IsTruncated => 57, + Property::MayReadItems => 58, + Property::MayAddItems => 59, + Property::MayRemoveItems => 60, + Property::MaySetSeen => 61, + Property::MaySetKeywords => 62, + Property::MayCreateChild => 63, + Property::MayRename => 64, + Property::MaySubmit => 65, + Property::Acl => 66, + Property::Aliases => 67, + Property::Attachments => 68, + Property::Bcc => 69, + Property::BlobId => 70, + Property::BodyStructure => 71, + Property::BodyValues => 72, + Property::Capabilities => 73, + Property::Cc => 74, + Property::Charset => 75, + Property::Cid => 76, + Property::DeliveryStatus => 77, + Property::Description => 78, + Property::DeviceClientId => 79, + Property::Disposition => 80, + Property::DsnBlobIds => 81, + Property::Email => 82, + Property::EmailId => 83, + Property::EmailIds => 84, + Property::Envelope => 85, + Property::Expires => 86, + Property::From => 87, + Property::FromDate => 88, + Property::HasAttachment => 89, + Property::Header(_) => 90, + Property::Headers => 91, + Property::HtmlBody => 92, + Property::HtmlSignature => 93, + Property::Id => 94, + Property::IdentityId => 95, + Property::InReplyTo => 96, + Property::_T(_) => 97, + } + } +} + +impl From<RfcHeader> for Property { + fn from(value: RfcHeader) -> Self { + match value { + RfcHeader::Subject => Property::Subject, + RfcHeader::From => Property::From, + RfcHeader::To => Property::To, + RfcHeader::Cc => Property::Cc, + RfcHeader::Date => Property::SentAt, + RfcHeader::Bcc => Property::Bcc, + RfcHeader::ReplyTo => Property::ReplyTo, + RfcHeader::Sender => Property::Sender, + RfcHeader::InReplyTo => Property::InReplyTo, + RfcHeader::MessageId => Property::MessageId, + RfcHeader::References => Property::References, + _ => unreachable!(), + } + } +} + +impl SerializeValue for Property { + fn serialize_value(self, buf: &mut Vec<u8>) { + buf.push(self.into()); + } +} + +impl DeserializeValue for Property { + fn deserialize_value(bytes: &mut std::slice::Iter<'_, u8>) -> Option<Self> { + match *bytes.next()? { + 0 => Some(Property::IsActive), + 1 => Some(Property::IsEnabled), + 2 => Some(Property::IsSubscribed), + 3 => Some(Property::Keys), + 4 => Some(Property::Keywords), + 5 => Some(Property::Language), + 6 => Some(Property::Location), + 7 => Some(Property::MailboxIds), + 8 => Some(Property::MayDelete), + 9 => Some(Property::MdnBlobIds), + 10 => Some(Property::Members), + 11 => Some(Property::MessageId), + 12 => Some(Property::MyRights), + 13 => Some(Property::Name), + 14 => Some(Property::ParentId), + 15 => Some(Property::PartId), + 16 => Some(Property::Picture), + 17 => Some(Property::Preview), + 18 => Some(Property::Quota), + 19 => Some(Property::ReceivedAt), + 20 => Some(Property::References), + 21 => Some(Property::ReplyTo), + 22 => Some(Property::Role), + 23 => Some(Property::Secret), + 24 => Some(Property::SendAt), + 25 => Some(Property::Sender), + 26 => Some(Property::SentAt), + 27 => Some(Property::Size), + 28 => Some(Property::SortOrder), + 29 => Some(Property::Subject), + 30 => Some(Property::SubParts), + 31 => Some(Property::TextBody), + 32 => Some(Property::TextSignature), + 33 => Some(Property::ThreadId), + 34 => Some(Property::Timezone), + 35 => Some(Property::To), + 36 => Some(Property::ToDate), + 37 => Some(Property::TotalEmails), + 38 => Some(Property::TotalThreads), + 39 => Some(Property::Type), + 40 => Some(Property::Types), + 41 => Some(Property::UndoStatus), + 42 => Some(Property::UnreadEmails), + 43 => Some(Property::UnreadThreads), + 44 => Some(Property::Url), + 45 => Some(Property::VerificationCode), + 46 => Some(Property::Parameters), + 47 => Some(Property::Addresses), + 48 => Some(Property::P256dh), + 49 => Some(Property::Auth), + 50 => Some(Property::Value), + 51 => Some(Property::SmtpReply), + 52 => Some(Property::Delivered), + 53 => Some(Property::Displayed), + 54 => Some(Property::MailFrom), + 55 => Some(Property::RcptTo), + 56 => Some(Property::IsEncodingProblem), + 57 => Some(Property::IsTruncated), + 58 => Some(Property::MayReadItems), + 59 => Some(Property::MayAddItems), + 60 => Some(Property::MayRemoveItems), + 61 => Some(Property::MaySetSeen), + 62 => Some(Property::MaySetKeywords), + 63 => Some(Property::MayCreateChild), + 64 => Some(Property::MayRename), + 65 => Some(Property::MaySubmit), + 66 => Some(Property::Acl), + 67 => Some(Property::Aliases), + 68 => Some(Property::Attachments), + 69 => Some(Property::Bcc), + 70 => Some(Property::BlobId), + 71 => Some(Property::BodyStructure), + 72 => Some(Property::BodyValues), + 73 => Some(Property::Capabilities), + 74 => Some(Property::Cc), + 75 => Some(Property::Charset), + 76 => Some(Property::Cid), + 77 => Some(Property::DeliveryStatus), + 78 => Some(Property::Description), + 79 => Some(Property::DeviceClientId), + 80 => Some(Property::Disposition), + 81 => Some(Property::DsnBlobIds), + 82 => Some(Property::Email), + 83 => Some(Property::EmailId), + 84 => Some(Property::EmailIds), + 85 => Some(Property::Envelope), + 86 => Some(Property::Expires), + 87 => Some(Property::From), + 88 => Some(Property::FromDate), + 89 => Some(Property::HasAttachment), + 90 => Some(Property::Header(HeaderProperty { + form: HeaderForm::Raw, + header: String::new(), + all: false, + })), // Never serialized + 91 => Some(Property::Headers), + 92 => Some(Property::HtmlBody), + 93 => Some(Property::HtmlSignature), + 94 => Some(Property::Id), + 95 => Some(Property::IdentityId), + 96 => Some(Property::InReplyTo), + 97 => Some(Property::_T(String::new())), // Never serialized + _ => None, + } + } +} diff --git a/crates/jmap-proto/src/types/state.rs b/crates/jmap-proto/src/types/state.rs new file mode 100644 index 00000000..5efd6807 --- /dev/null +++ b/crates/jmap-proto/src/types/state.rs @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart JMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use utils::codec::{ + base32_custom::Base32Writer, + leb128::{Leb128Iterator, Leb128Writer}, +}; + +use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser}; + +use super::ChangeId; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JMAPIntermediateState { + pub from_id: ChangeId, + pub to_id: ChangeId, + pub items_sent: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum State { + #[default] + Initial, + Exact(ChangeId), + Intermediate(JMAPIntermediateState), +} + +impl From<ChangeId> for State { + fn from(change_id: ChangeId) -> Self { + State::Exact(change_id) + } +} + +impl From<Option<ChangeId>> for State { + fn from(change_id: Option<ChangeId>) -> Self { + match change_id { + Some(change_id) => State::Exact(change_id), + None => State::Initial, + } + } +} + +impl JsonObjectParser for State { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + match parser + .next_unescaped()? + .ok_or_else(|| parser.error_value())? + { + b'n' => Ok(State::Initial), + b's' => { + let mut reader = JsonBase32Reader::new(parser); + reader + .next_leb128::<ChangeId>() + .map(State::Exact) + .ok_or_else(|| parser.error_value()) + } + b'r' => { + let mut it = JsonBase32Reader::new(parser); + + if let (Some(from_id), Some(to_id), Some(items_sent)) = ( + it.next_leb128::<ChangeId>(), + it.next_leb128::<ChangeId>(), + it.next_leb128::<usize>(), + ) { + if items_sent > 0 { + Ok(State::Intermediate(JMAPIntermediateState { + from_id, + to_id: from_id.saturating_add(to_id), + items_sent, + })) + } else { + Err(parser.error_value()) + } + } else { + Err(parser.error_value()) + } + } + _ => Err(parser.error_value()), + } + } +} + +impl State { + pub fn new_initial() -> Self { + State::Initial + } + + pub fn new_exact(id: ChangeId) -> Self { + State::Exact(id) + } + + pub fn new_intermediate(from_id: ChangeId, to_id: ChangeId, items_sent: usize) -> Self { + State::Intermediate(JMAPIntermediateState { + from_id, + to_id, + items_sent, + }) + } + + pub fn get_change_id(&self) -> ChangeId { + match self { + State::Exact(id) => *id, + State::Intermediate(intermediate) => intermediate.to_id, + State::Initial => ChangeId::MAX, + } + } +} + +impl serde::Serialize for State { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut writer = Base32Writer::with_capacity(10); + + match self { + State::Initial => { + writer.push_char('n'); + } + State::Exact(id) => { + writer.push_char('s'); + writer.write_leb128(*id).unwrap(); + } + State::Intermediate(intermediate) => { + writer.push_char('r'); + writer.write_leb128(intermediate.from_id).unwrap(); + writer + .write_leb128(intermediate.to_id - intermediate.from_id) + .unwrap(); + writer.write_leb128(intermediate.items_sent).unwrap(); + } + } + + f.write_str(&writer.finalize()) + } +} + +#[cfg(test)] +mod tests { + + use crate::{parser::json::Parser, types::ChangeId}; + + use super::State; + + #[test] + fn test_state_id() { + for id in [ + State::new_initial(), + State::new_exact(0), + State::new_exact(12345678), + State::new_exact(ChangeId::MAX), + State::new_intermediate(0, 0, 1), + State::new_intermediate(1024, 2048, 100), + State::new_intermediate(12345678, 87654321, 1), + State::new_intermediate(0, 0, 12345678), + State::new_intermediate(0, 87654321, 12345678), + State::new_intermediate(12345678, 87654321, 1), + State::new_intermediate(12345678, 87654321, 12345678), + State::new_intermediate(ChangeId::MAX, ChangeId::MAX, ChangeId::MAX as usize), + ] { + assert_eq!( + Parser::new(format!("\"{id}\"").as_bytes()) + .next_token::<State>() + .unwrap() + .unwrap_string("") + .unwrap(), + id + ); + } + } +} diff --git a/crates/jmap-proto/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs new file mode 100644 index 00000000..bab02ec3 --- /dev/null +++ b/crates/jmap-proto/src/types/type_state.rs @@ -0,0 +1,93 @@ +use std::fmt::Display; + +use serde::Serialize; + +use crate::{ + object::{DeserializeValue, SerializeValue}, + parser::{json::Parser, JsonObjectParser}, +}; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize)] +#[repr(u8)] +pub enum TypeState { + #[serde(rename = "Email")] + Email = 0, + #[serde(rename = "EmailDelivery")] + EmailDelivery = 1, + #[serde(rename = "EmailSubmission")] + EmailSubmission = 2, + #[serde(rename = "Mailbox")] + Mailbox = 3, + #[serde(rename = "Thread")] + Thread = 4, + #[serde(rename = "Identity")] + Identity = 5, +} + +impl JsonObjectParser for TypeState { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut hash = 0; + let mut shift = 0; + + while let Some(ch) = parser.next_unescaped()? { + if shift < 128 { + hash |= (ch as u128) << shift; + shift += 8; + } else { + return Err(parser.error_value()); + } + } + + match hash { + 0x6c69_616d_45 => Ok(TypeState::Email), + 0x7972_6576_696c_6544_6c69_616d_45 => Ok(TypeState::EmailDelivery), + 0x6e6f_6973_7369_6d62_7553_6c69_616d_45 => Ok(TypeState::EmailSubmission), + 0x786f_626c_6961_4d => Ok(TypeState::Mailbox), + 0x6461_6572_6854 => Ok(TypeState::Thread), + 0x7974_6974_6e65_6449 => Ok(TypeState::Identity), + _ => Err(parser.error_value()), + } + } +} + +impl TypeState { + pub fn as_str(&self) -> &'static str { + match self { + TypeState::Email => "Email", + TypeState::EmailDelivery => "EmailDelivery", + TypeState::EmailSubmission => "EmailSubmission", + TypeState::Mailbox => "Mailbox", + TypeState::Thread => "Thread", + TypeState::Identity => "Identity", + } + } +} + +impl Display for TypeState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl SerializeValue for TypeState { + fn serialize_value(self, buf: &mut Vec<u8>) { + buf.push(self as u8); + } +} + +impl DeserializeValue for TypeState { + fn deserialize_value(bytes: &mut std::slice::Iter<'_, u8>) -> Option<Self> { + match *bytes.next()? { + 0 => Some(TypeState::Email), + 1 => Some(TypeState::EmailDelivery), + 2 => Some(TypeState::EmailSubmission), + 3 => Some(TypeState::Mailbox), + 4 => Some(TypeState::Thread), + 5 => Some(TypeState::Identity), + _ => None, + } + } +} diff --git a/crates/jmap-proto/src/types/value.rs b/crates/jmap-proto/src/types/value.rs new file mode 100644 index 00000000..4c185f5d --- /dev/null +++ b/crates/jmap-proto/src/types/value.rs @@ -0,0 +1,351 @@ +use std::{borrow::Cow, fmt::Display}; + +use mail_parser::{Addr, DateTime, Group}; +use serde::Serialize; +use store::BlobHash; + +use crate::{ + error::method::MethodError, + object::Object, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, + request::reference::{MaybeReference, ResultReference}, +}; + +use super::{ + acl::Acl, + blob::BlobId, + date::UTCDate, + id::Id, + keyword::Keyword, + property::{HeaderForm, IntoProperty, ObjectProperty, Property}, + type_state::TypeState, +}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub enum Value { + Text(String), + UnsignedInt(u64), + Bool(bool), + Id(Id), + Date(UTCDate), + BlobId(BlobId), + Keyword(Keyword), + TypeState(TypeState), + Acl(Acl), + List(Vec<Value>), + Object(Object<Value>), + #[default] + Null, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SetValue { + Value(Value), + Patch(Vec<Value>), + IdReference(MaybeReference<Id, String>), + IdReferences(Vec<MaybeReference<Id, String>>), + ResultReference(ResultReference), +} + +#[derive(Debug, Clone)] +pub struct SetValueMap<T> { + pub values: Vec<T>, +} + +pub trait IntoValue: Eq { + fn into_value(self) -> Value; +} + +impl Value { + pub fn parse<K: JsonObjectParser + IntoProperty, V: JsonObjectParser + IntoValue>( + parser: &mut Parser<'_>, + ) -> crate::parser::Result<Self> { + Ok(match parser.next_token::<V>()? { + Token::String(v) => v.into_value(), + Token::DictStart => { + let mut properties = Object::with_capacity(4); + while { + let property = parser.next_dict_key::<K>()?.into_property(); + let value = Value::from_property(parser, &property)?; + properties.append(property, value); + !parser.is_dict_end()? + } {} + Value::Object(properties) + } + Token::ArrayStart => { + let mut values = Vec::with_capacity(4); + while { + values.push(Value::parse::<K, V>(parser)?); + !parser.is_array_end()? + } {} + Value::List(values) + } + Token::Integer(v) => Value::UnsignedInt(std::cmp::max(v, 0) as u64), + Token::Float(v) => Value::UnsignedInt(if v > 0.0 { v as u64 } else { 0 }), + Token::Boolean(v) => Value::Bool(v), + Token::Null => Value::Null, + token => return Err(token.error("", "value")), + }) + } + + pub fn from_property( + parser: &mut Parser<'_>, + property: &Property, + ) -> crate::parser::Result<Self> { + match &property { + Property::BlobId => Ok(parser + .next_token::<BlobId>()? + .unwrap_string_or_null("")? + .map(Value::BlobId) + .unwrap_or(Value::Null)), + Property::Size => Ok(parser + .next_token::<String>()? + .unwrap_uint_or_null("")? + .map(Value::UnsignedInt) + .unwrap_or(Value::Null)), + Property::PartId + | Property::Name + | Property::Email + | Property::Type + | Property::Charset + | Property::Cid + | Property::Disposition + | Property::Location + | Property::Value + | Property::SmtpReply + | Property::P256dh + | Property::Delivered + | Property::Displayed + | Property::Auth => Ok(parser + .next_token::<String>()? + .unwrap_string_or_null("")? + .map(Value::Text) + .unwrap_or(Value::Null)), + + Property::Header(h) => { + if matches!(h.form, HeaderForm::Date) { + Value::parse::<ObjectProperty, UTCDate>(parser) + } else { + Value::parse::<ObjectProperty, String>(parser) + } + } + + Property::Headers + | Property::Addresses + | Property::MailFrom + | Property::RcptTo + | Property::SubParts => Value::parse::<ObjectProperty, String>(parser), + Property::Language | Property::Parameters => Value::parse::<String, String>(parser), + + Property::IsEncodingProblem + | Property::IsTruncated + | Property::MayReadItems + | Property::MayAddItems + | Property::MayRemoveItems + | Property::MaySetSeen + | Property::MaySetKeywords + | Property::MayCreateChild + | Property::MayRename + | Property::MayDelete + | Property::MaySubmit => Ok(parser + .next_token::<String>()? + .unwrap_bool_or_null("")? + .map(Value::Bool) + .unwrap_or(Value::Null)), + _ => Value::parse::<String, String>(parser), + } + } + + pub fn as_blob(&self) -> Result<&BlobId, MethodError> { + match self { + Value::BlobId(blob) => Ok(blob), + _ => { + let log = "log"; + Err(MethodError::ServerPartialFail) + } + } + } +} + +impl<T: JsonObjectParser + Display + Eq> JsonObjectParser for SetValueMap<T> { + fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + where + Self: Sized, + { + let mut values = Vec::new(); + match parser.next_token::<Ignore>()? { + Token::DictStart => { + parser.next_token::<Ignore>()?.assert(Token::DictStart)?; + while { + let value = parser.next_dict_key::<T>()?; + if bool::parse(parser)? { + values.push(value); + } + !parser.is_dict_end()? + } {} + } + Token::Null => (), + token => return Err(token.error("", &token.to_string())), + } + Ok(SetValueMap { values }) + } +} + +impl IntoValue for String { + fn into_value(self) -> Value { + Value::Text(self) + } +} + +impl IntoValue for Id { + fn into_value(self) -> Value { + Value::Id(self) + } +} + +impl IntoValue for UTCDate { + fn into_value(self) -> Value { + Value::Date(self) + } +} + +impl IntoValue for Acl { + fn into_value(self) -> Value { + Value::Acl(self) + } +} + +impl IntoValue for TypeState { + fn into_value(self) -> Value { + Value::TypeState(self) + } +} + +impl From<usize> for Value { + fn from(value: usize) -> Self { + Value::UnsignedInt(value as u64) + } +} + +impl From<u64> for Value { + fn from(value: u64) -> Self { + Value::UnsignedInt(value) + } +} + +impl From<u32> for Value { + fn from(value: u32) -> Self { + Value::UnsignedInt(value as u64) + } +} + +impl From<String> for Value { + fn from(value: String) -> Self { + Value::Text(value) + } +} + +impl From<&str> for Value { + fn from(value: &str) -> Self { + Value::Text(value.to_string()) + } +} + +impl From<bool> for Value { + fn from(value: bool) -> Self { + Value::Bool(value) + } +} + +impl From<Keyword> for Value { + fn from(value: Keyword) -> Self { + Value::Keyword(value) + } +} + +impl From<Object<Value>> for Value { + fn from(value: Object<Value>) -> Self { + Value::Object(value) + } +} + +impl From<BlobId> for Value { + fn from(value: BlobId) -> Self { + Value::BlobId(value) + } +} + +impl From<BlobHash> for Value { + fn from(value: BlobHash) -> Self { + Value::BlobId(BlobId::new(value)) + } +} + +impl From<Id> for Value { + fn from(value: Id) -> Self { + Value::Id(value) + } +} + +impl From<Acl> for Value { + fn from(value: Acl) -> Self { + Value::Acl(value) + } +} + +impl From<DateTime> for Value { + fn from(date: DateTime) -> Self { + Value::Date(UTCDate { + year: date.year, + month: date.month, + day: date.day, + hour: date.hour, + minute: date.minute, + second: date.second, + tz_before_gmt: date.tz_before_gmt, + tz_hour: date.tz_hour, + tz_minute: date.tz_minute, + }) + } +} + +impl From<Cow<'_, str>> for Value { + fn from(value: Cow<'_, str>) -> Self { + Value::Text(value.into_owned()) + } +} + +impl<T: Into<Value>> From<Vec<T>> for Value { + fn from(value: Vec<T>) -> Self { + Value::List(value.into_iter().map(|v| v.into()).collect()) + } +} + +impl<T: Into<Value>> From<Option<T>> for Value { + fn from(value: Option<T>) -> Self { + match value { + Some(value) => value.into(), + None => Value::Null, + } + } +} + +impl From<Addr<'_>> for Value { + fn from(value: Addr<'_>) -> Self { + Value::Object( + Object::with_capacity(2) + .with_property(Property::Name, value.name) + .with_property(Property::Email, value.address.unwrap_or_default()), + ) + } +} + +impl From<Group<'_>> for Value { + fn from(group: Group<'_>) -> Self { + Value::Object( + Object::with_capacity(2) + .with_property(Property::Name, group.name) + .with_property(Property::Addresses, group.addresses), + ) + } +} |