Reordering

This commit is contained in:
Mauro D 2023-04-04 13:12:46 +00:00
parent cfb2637b55
commit 9f63c86db9
105 changed files with 8813 additions and 105 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
/Cargo.lock
.vscode

View file

@ -1,46 +1,23 @@
[package]
name = "store"
version = "0.1.0"
name = "stalwart-jmap"
description = "Stalwart JMAP Server"
authors = [ "Stalwart Labs Ltd. <hello@stalw.art>"]
repository = "https://github.com/stalwartlabs/jmap-server"
homepage = "https://stalw.art/jmap"
keywords = ["jmap", "email", "mail", "server"]
categories = ["email"]
license = "AGPL-3.0-only"
version = "0.3.0"
edition = "2021"
resolver = "2"
[lib]
path = "crates/core/src/lib.rs"
[dependencies]
utils = { path = "../utils" }
rocksdb = { version = "0.20.1", optional = true }
foundationdb = { version = "0.7.0", optional = true }
rusqlite = { version = "0.29.0", features = ["bundled"], optional = true }
tokio = { version = "1.23", features = ["sync"], optional = true }
r2d2 = { version = "0.8.10", optional = true }
futures = { version = "0.3", optional = true }
rand = "0.8.5"
roaring = "0.10.1"
rayon = { version = "1.5.1", optional = true }
serde = { version = "1.0", features = ["derive"]}
ahash = { version = "0.8.0", features = ["serde"] }
bitpacking = "0.8.4"
lazy_static = "1.4"
whatlang = "0.16" # Language detection
rust-stemmers = "1.2" # Stemmers
tinysegmenter = "0.1" # Japanese tokenizer
jieba-rs = "0.6" # Chinese stemmer
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
farmhash = "1.1.5"
siphasher = "0.3"
maybe-async = "0.2"
parking_lot = { version = "0.12.1", optional = true }
lru-cache = { version = "0.1.2", optional = true }
blake3 = "1.3.3"
[features]
default = ["sqlite"]
rocks = ["rocksdb", "rayon", "is_sync"]
sqlite = ["rusqlite", "rayon", "r2d2", "tokio", "is_sync"]
foundation = ["foundationdb", "futures", "is_async"]
is_sync = ["maybe-async/is_sync", "parking_lot", "lru-cache"]
is_async = []
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }
csv = "1.1"
rayon = { version = "1.5.1" }
flate2 = { version = "1.0.17", features = ["zlib"], default-features = false }
[workspace]
members = [
"crates/protocol",
"crates/store",
"crates/core",
"tests",
]

11
crates/core/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
mail-send = { git = "https://github.com/stalwartlabs/mail-send" }
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"

0
crates/core/src/lib.rs Normal file
View file

159
crates/protocol/Cargo.lock generated Normal file
View file

@ -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"

View file

@ -0,0 +1,11 @@
[package]
name = "procotol"
version = "0.1.0"
edition = "2021"
[dependencies]
utils = { path = "/home/vagrant/code/utils" }
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"] }

View file

@ -0,0 +1,166 @@
/*
* 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()
}
}

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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>;

109
crates/protocol/src/lib.rs Normal file
View file

@ -0,0 +1,109 @@
pub mod error;
pub mod method;
pub mod object;
pub mod parser;
pub mod request;
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!("}}");
}*/
}
}

View file

@ -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")]
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)
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,126 @@
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 {
pub account_id: Id,
pub ids: Option<MaybeReference<Vec<Id>, ResultReference>>,
pub properties: Option<MaybeReference<Vec<Property>, ResultReference>>,
pub arguments: RequestArguments,
}
#[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 {
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)
}
}
}

View file

@ -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, EmailImport>,
}
#[derive(Debug, Clone)]
pub struct EmailImport {
pub blob_id: BlobId,
pub mailbox_ids: Option<MaybeReference<Vec<MaybeReference<Id, String>>, ResultReference>>,
pub keywords: Vec<Keyword>,
pub received_at: Option<UTCDate>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct EmailImportResponse {
#[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, EmailImport>>::parse(parser)?;
}
_ => {
parser.skip_token(parser.depth_array, parser.depth_dict)?;
}
}
!parser.is_dict_end()?
} {}
Ok(request)
}
}
impl JsonObjectParser for EmailImport {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
where
Self: Sized,
{
let mut request = EmailImport {
blob_id: BlobId::default(),
mailbox_ids: None,
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 {
Some(MaybeReference::Value(
<SetValueMap<MaybeReference<Id, String>>>::parse(parser)?.values,
))
} else {
Some(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)
}
}

View file

@ -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()
}

View file

@ -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 EmailParseResponse {
#[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)
}
}

View file

@ -0,0 +1,596 @@
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 {
pub account_id: Id,
pub filter: Vec<Filter>,
pub sort: Option<Vec<Comparator>>,
pub position: Option<i64>,
pub anchor: Option<Id>,
pub anchor_offset: Option<i64>,
pub limit: Option<usize>,
pub calculate_total: Option<bool>,
pub arguments: RequestArguments,
}
#[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(u64),
QuotaGt(u64),
IdentityIds(Vec<Id>),
EmailIds(Vec<Id>),
ThreadIds(Vec<Id>),
UndoStatus(String),
Before(UTCDate),
After(UTCDate),
InMailbox(Id),
InMailboxOtherThan(Vec<Id>),
MinSize(u64),
MaxSize(u64),
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 {
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_int_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_int_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(),
),
(0x6e61_6854_7265_7461_6572_4761_746f_7571, _) => Filter::QuotaGt(
parser
.next_token::<String>()?
.unwrap_uint_or_null("quotaGreaterThan")?
.unwrap_or_default(),
),
(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(),
),
(0x657a_6953_7861_6d, _) => Filter::MaxSize(
parser
.next_token::<String>()?
.unwrap_uint_or_null("maxSize")?
.unwrap_or_default(),
),
(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 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),
}
}
}

View file

@ -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 {
id: Id,
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)
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,352 @@
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
| Property::UndoStatus => parser
.next_token::<MaybeReference<Id, String>>()?
.unwrap_string_or_null("")?
.map(|id| match id {
MaybeReference::Value(id) => SetValue::Value(Value::Id(id)),
MaybeReference::Reference(r) => SetValue::Value(Value::Text(r)),
})
.unwrap_or(SetValue::Value(Value::Null)),
Property::MailboxIds => {
if property.patch.is_empty() {
SetValue::Value(Value::List(
<SetValueMap<MaybeReference<Id, String>>>::parse(parser)?
.values
.into_iter()
.map(|id| match id {
MaybeReference::Value(id) => Value::Id(id),
MaybeReference::Reference(r) => Value::Text(r),
})
.collect(),
))
} 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 => {
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),
}
}
}

View file

@ -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)
}
}

View file

@ -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 {
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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,13 @@
pub mod email;
pub mod email_submission;
pub mod mailbox;
pub mod sieve;
use utils::map::vec_map::VecMap;
use crate::types::property::Property;
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct Object<T> {
pub properties: VecMap<Property, T>,
}

View file

@ -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)
}
}
}

View file

@ -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> {}

View file

@ -0,0 +1,276 @@
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();
parser.next_token::<Ignore>()?.assert(Token::DictStart)?;
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())
}
}
}

View file

@ -0,0 +1,383 @@
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 {
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
);
}
}
}

View file

@ -0,0 +1,164 @@
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 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"),
}
}
}

View file

@ -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()
}
}
}

View file

@ -0,0 +1,32 @@
use serde_json::value::RawValue;
use std::fmt::Write;
use crate::parser::{json::Parser, JsonObjectParser, Token};
#[derive(Debug)]
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(),
})
}
}

View file

@ -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())
}
}

View file

@ -0,0 +1,109 @@
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::GetRequest,
import::ImportEmailRequest,
parse::ParseEmailRequest,
query::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)]
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),
Set(SetRequest),
Changes(ChangesRequest),
Copy(CopyRequest),
CopyBlob(CopyBlobRequest),
ImportEmail(ImportEmailRequest),
ParseEmail(ParseEmailRequest),
QueryChanges(QueryChangesRequest),
Query(QueryRequest),
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;
}
}
}
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>;
}

View file

@ -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));
}
}

View file

@ -0,0 +1,101 @@
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 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)),
}
}
}

View file

@ -0,0 +1,82 @@
use std::fmt::{self, Display};
use crate::parser::{json::Parser, JsonObjectParser};
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum Acl {
Read,
Modify,
Delete,
ReadItems,
AddItems,
ModifyItems,
RemoveItems,
CreateChild,
Administer,
Submit,
}
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())
}
}

View file

@ -0,0 +1,219 @@
/*
* 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 utils::codec::{
base32_custom::Base32Writer,
leb128::{Leb128Iterator, Leb128Writer},
};
use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser};
pub const BLOB_HASH_LEN: usize = 32;
pub const BLOB_LOCAL: u8 = 0;
pub const BLOB_EXTERNAL: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum BlobHash {
Local { hash: [u8; BLOB_HASH_LEN] },
External { hash: [u8; BLOB_HASH_LEN] },
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BlobId {
pub id: 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 (is_local, encoding) = match parser
.next_unescaped()?
.ok_or_else(|| parser.error_value())?
{
b'b' => (false, None),
b'a' => (true, None),
b @ b'c'..=b'g' => (true, Some(b - b'c')),
b @ b'h'..=b'l' => (false, Some(b - b'h')),
_ => {
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 {
id: if is_local {
BlobHash::Local { hash }
} else {
BlobHash::External { 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(id: BlobHash) -> Self {
BlobId { id, section: None }
}
pub fn new_section(id: BlobHash, offset_start: usize, offset_end: usize, encoding: u8) -> Self {
BlobId {
id,
section: BlobSection {
offset_start,
size: offset_end - offset_start,
encoding,
}
.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.clone())
}
}
impl From<BlobHash> for BlobId {
fn from(id: BlobHash) -> Self {
BlobId::new(id)
}
}
impl Default for BlobId {
fn default() -> Self {
Self {
id: BlobHash::Local {
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(if self.id.is_local() {
b'c' + section.encoding
} else {
b'h' + section.encoding
}));
writer.write(self.id.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(if self.id.is_local() { 'a' } else { 'b' });
writer.write(self.id.hash()).unwrap();
}
f.write_str(&writer.finalize())
}
}
impl BlobHash {
/*pub fn new_local(bytes: &[u8]) -> Self {
// Create blob key
let mut hasher = Sha256::new();
hasher.update(bytes);
BlobId::Local {
hash: hasher.finalize().into(),
}
}
pub fn new_external(bytes: &[u8]) -> Self {
// Create blob key
let mut hasher = Sha256::new();
hasher.update(bytes);
BlobId::External {
hash: hasher.finalize().into(),
}
}*/
pub fn is_local(&self) -> bool {
matches!(self, BlobHash::Local { .. })
}
pub fn is_external(&self) -> bool {
matches!(self, BlobHash::External { .. })
}
pub fn hash(&self) -> &[u8] {
match self {
BlobHash::Local { hash } => hash,
BlobHash::External { hash } => hash,
}
}
}

View file

@ -0,0 +1,12 @@
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum Collection {
Principal = 0,
PushSubscription = 1,
Mail = 2,
Mailbox = 3,
Thread = 4,
Identity = 5,
EmailSubmission = 6,
SieveScript = 7,
}

View file

@ -0,0 +1,266 @@
/*
* 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, 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())
}
}
#[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);
}
}
}

View file

@ -0,0 +1,283 @@
/*
* 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 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 get_document_id(&self) -> DocumentId {
(self.id & 0xFFFFFFFF) as DocumentId
}
pub fn get_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.get_document_id()
}
}
impl From<Id> for String {
fn from(id: Id) -> Self {
id.as_string()
}
}
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();
}
}

View file

@ -0,0 +1,116 @@
use std::fmt::Display;
use crate::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;
#[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),
}
}
}

View file

@ -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;

View file

@ -0,0 +1,276 @@
/*
* 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 is_item_query(&self, name: &str) -> bool {
match self {
JSONPointer::String(property) => property == name,
JSONPointer::Path(path) if path.len() == 2 => {
if let (Some(JSONPointer::String(property)), Some(JSONPointer::Wildcard)) =
(path.get(0), path.get(1))
{
property == name
} else {
false
}
}
_ => false,
}
}
}
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}"
);
}
}
}

View file

@ -0,0 +1,807 @@
use std::fmt::{Display, Formatter};
use serde::Serialize;
use crate::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();
}
}
parse_property(parser, first_char, hash)
}
}
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 = parse_property(parser, first_char, hash)?;
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(
parser: &mut Parser,
first_char: u8,
hash: u128,
) -> crate::parser::Result<Property> {
Ok(match first_char {
b'a' => match hash {
0x6c63 => Property::Acl,
0x7365_7361_696c => Property::Aliases,
0x7374_6e65_6d68_6361_7474 => Property::Attachments,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
b'c' => match hash {
0x7365_6974_696c_6962_6170_61 => Property::Capabilities,
0x63 => Property::Cc,
0x7465_7372_6168 => Property::Charset,
0x6469 => Property::Cid,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
b'f' => match hash {
0x6d6f_72 => Property::From,
0x6574_6144_6d6f_72 => Property::FromDate,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
b'k' => match hash {
0x7379_65 => Property::Keys,
0x7364_726f_7779_65 => Property::Keywords,
_ => 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 {
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,
_ => parser.invalid_property()?,
},
b'n' => match hash {
0x656d_61 => Property::Name,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
b'q' => match hash {
0x6174_6f75 => Property::Quota,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
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,
_ => parser.invalid_property()?,
},
b'v' => match hash {
0x6564_6f43_6e6f_6974_6163_6966_6972_65 => Property::VerificationCode,
_ => parser.invalid_property()?,
},
_ => parser.invalid_property()?,
})
}
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 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"),
}
}
}

View file

@ -0,0 +1,191 @@
/*
* 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 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
);
}
}
}

View file

@ -0,0 +1,69 @@
use std::fmt::Display;
use serde::Serialize;
use crate::parser::{json::Parser, JsonObjectParser};
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize)]
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())
}
}

View file

@ -0,0 +1,206 @@
use std::fmt::Display;
use serde::Serialize;
use utils::map::vec_map::VecMap;
use crate::{
parser::{json::Parser, Ignore, JsonObjectParser, Token},
request::reference::ResultReference,
};
use super::{
acl::Acl,
blob::BlobId,
date::UTCDate,
id::Id,
keyword::Keyword,
property::{HeaderForm, IntoProperty, ObjectProperty, Property},
type_state::TypeState,
};
#[derive(Debug, 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(VecMap<Property, Value>),
Null,
}
#[derive(Debug, Clone)]
pub enum SetValue {
Value(Value),
Patch(Vec<Value>),
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 = VecMap::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),
}
}
}
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)
}
}

1540
crates/store/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

40
crates/store/Cargo.toml Normal file
View file

@ -0,0 +1,40 @@
[package]
name = "store"
version = "0.1.0"
edition = "2021"
[dependencies]
utils = { path = "/home/vagrant/code/utils" }
rocksdb = { version = "0.20.1", optional = true }
foundationdb = { version = "0.7.0", optional = true }
rusqlite = { version = "0.29.0", features = ["bundled"], optional = true }
tokio = { version = "1.23", features = ["sync"], optional = true }
r2d2 = { version = "0.8.10", optional = true }
futures = { version = "0.3", optional = true }
rand = "0.8.5"
roaring = "0.10.1"
rayon = { version = "1.5.1", optional = true }
serde = { version = "1.0", features = ["derive"]}
ahash = { version = "0.8.0", features = ["serde"] }
bitpacking = "0.8.4"
lazy_static = "1.4"
whatlang = "0.16" # Language detection
rust-stemmers = "1.2" # Stemmers
tinysegmenter = "0.1" # Japanese tokenizer
jieba-rs = "0.6" # Chinese stemmer
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
farmhash = "1.1.5"
siphasher = "0.3"
maybe-async = "0.2"
parking_lot = { version = "0.12.1", optional = true }
lru-cache = { version = "0.1.2", optional = true }
blake3 = "1.3.3"
[features]
default = ["sqlite"]
rocks = ["rocksdb", "rayon", "is_sync"]
sqlite = ["rusqlite", "rayon", "r2d2", "tokio", "is_sync"]
foundation = ["foundationdb", "futures", "is_async"]
is_sync = ["maybe-async/is_sync", "parking_lot", "lru-cache"]
is_async = []

View file

@ -230,7 +230,6 @@ impl Store {
.await
}
#[cfg(test)]
pub async fn destroy(&self) {
use crate::{
SUBSPACE_ACLS, SUBSPACE_BITMAPS, SUBSPACE_BLOBS, SUBSPACE_INDEXES, SUBSPACE_LOGS,

View file

@ -8,8 +8,7 @@ pub mod fts;
pub mod query;
pub mod write;
#[cfg(test)]
pub mod tests;
pub use ahash;
#[cfg(feature = "rocks")]
pub struct Store {

View file

@ -165,7 +165,6 @@ impl Filter {
}
}
#[cfg(test)]
pub fn match_english(field: impl Into<u8>, text: impl Into<String>) -> Self {
Self::match_text(field, text, Language::English)
}

15
tests/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "tests"
version = "0.1.0"
edition = "2021"
[dependencies]
store = { path = "../crates/store" }
utils = { path = "/home/vagrant/code/utils" }
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }
csv = "1.1"
rayon = { version = "1.5.1" }
flate2 = { version = "1.0.17", features = ["zlib"], default-features = false }

Some files were not shown because too many files have changed in this diff Show more