summaryrefslogtreecommitdiff
path: root/crates/jmap-proto
diff options
context:
space:
mode:
authorMauro D <mauro@stalw.art>2023-04-14 07:04:36 +0000
committerMauro D <mauro@stalw.art>2023-04-14 07:04:36 +0000
commit3751133c7d90436440855a479b7fed6b32ae6945 (patch)
tree77865424cb83522ec375a76a07485c1541b5de3c /crates/jmap-proto
parent98e8febbefab44cbbfd660b4df54f16aa9973680 (diff)
Crates renamed.
Diffstat (limited to 'crates/jmap-proto')
-rw-r--r--crates/jmap-proto/Cargo.lock159
-rw-r--r--crates/jmap-proto/Cargo.toml13
-rw-r--r--crates/jmap-proto/src/error/method.rs173
-rw-r--r--crates/jmap-proto/src/error/mod.rs26
-rw-r--r--crates/jmap-proto/src/error/request.rs187
-rw-r--r--crates/jmap-proto/src/error/set.rs173
-rw-r--r--crates/jmap-proto/src/lib.rs112
-rw-r--r--crates/jmap-proto/src/method/changes.rs105
-rw-r--r--crates/jmap-proto/src/method/copy.rs195
-rw-r--r--crates/jmap-proto/src/method/get.rs141
-rw-r--r--crates/jmap-proto/src/method/import.rs147
-rw-r--r--crates/jmap-proto/src/method/mod.rs17
-rw-r--r--crates/jmap-proto/src/method/parse.rs105
-rw-r--r--crates/jmap-proto/src/method/query.rs688
-rw-r--r--crates/jmap-proto/src/method/query_changes.rs136
-rw-r--r--crates/jmap-proto/src/method/search_snippet.rs91
-rw-r--r--crates/jmap-proto/src/method/set.rs340
-rw-r--r--crates/jmap-proto/src/method/validate.rs56
-rw-r--r--crates/jmap-proto/src/object/email.rs73
-rw-r--r--crates/jmap-proto/src/object/email_submission.rs39
-rw-r--r--crates/jmap-proto/src/object/mailbox.rs58
-rw-r--r--crates/jmap-proto/src/object/mod.rs319
-rw-r--r--crates/jmap-proto/src/object/sieve.rs37
-rw-r--r--crates/jmap-proto/src/parser/base32.rs59
-rw-r--r--crates/jmap-proto/src/parser/impls.rs275
-rw-r--r--crates/jmap-proto/src/parser/json.rs384
-rw-r--r--crates/jmap-proto/src/parser/mod.rs173
-rw-r--r--crates/jmap-proto/src/request/capability.rs69
-rw-r--r--crates/jmap-proto/src/request/echo.rs32
-rw-r--r--crates/jmap-proto/src/request/method.rs196
-rw-r--r--crates/jmap-proto/src/request/mod.rs110
-rw-r--r--crates/jmap-proto/src/request/parser.rs220
-rw-r--r--crates/jmap-proto/src/request/reference.rs110
-rw-r--r--crates/jmap-proto/src/response/mod.rs168
-rw-r--r--crates/jmap-proto/src/response/references.rs695
-rw-r--r--crates/jmap-proto/src/types/acl.rs110
-rw-r--r--crates/jmap-proto/src/types/blob.rs171
-rw-r--r--crates/jmap-proto/src/types/collection.rs34
-rw-r--r--crates/jmap-proto/src/types/date.rs280
-rw-r--r--crates/jmap-proto/src/types/id.rs294
-rw-r--r--crates/jmap-proto/src/types/keyword.rs215
-rw-r--r--crates/jmap-proto/src/types/mod.rs14
-rw-r--r--crates/jmap-proto/src/types/pointer.rs292
-rw-r--r--crates/jmap-proto/src/types/property.rs1086
-rw-r--r--crates/jmap-proto/src/types/state.rs200
-rw-r--r--crates/jmap-proto/src/types/type_state.rs93
-rw-r--r--crates/jmap-proto/src/types/value.rs351
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),
+ )
+ }
+}