mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-17 14:19:21 +08:00
Reordering
This commit is contained in:
parent
cfb2637b55
commit
9f63c86db9
105 changed files with 8813 additions and 105 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
/Cargo.lock
|
||||
.vscode
|
||||
|
|
|
|||
61
Cargo.toml
61
Cargo.toml
|
|
@ -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
11
crates/core/Cargo.toml
Normal 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
0
crates/core/src/lib.rs
Normal file
159
crates/protocol/Cargo.lock
generated
Normal file
159
crates/protocol/Cargo.lock
generated
Normal 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"
|
||||
11
crates/protocol/Cargo.toml
Normal file
11
crates/protocol/Cargo.toml
Normal 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"] }
|
||||
166
crates/protocol/src/error/method.rs
Normal file
166
crates/protocol/src/error/method.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
26
crates/protocol/src/error/mod.rs
Normal file
26
crates/protocol/src/error/mod.rs
Normal 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;
|
||||
187
crates/protocol/src/error/request.rs
Normal file
187
crates/protocol/src/error/request.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
173
crates/protocol/src/error/set.rs
Normal file
173
crates/protocol/src/error/set.rs
Normal 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
109
crates/protocol/src/lib.rs
Normal 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!("}}");
|
||||
}*/
|
||||
}
|
||||
}
|
||||
105
crates/protocol/src/method/changes.rs
Normal file
105
crates/protocol/src/method/changes.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
195
crates/protocol/src/method/copy.rs
Normal file
195
crates/protocol/src/method/copy.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
126
crates/protocol/src/method/get.rs
Normal file
126
crates/protocol/src/method/get.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
147
crates/protocol/src/method/import.rs
Normal file
147
crates/protocol/src/method/import.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
17
crates/protocol/src/method/mod.rs
Normal file
17
crates/protocol/src/method/mod.rs
Normal 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()
|
||||
}
|
||||
105
crates/protocol/src/method/parse.rs
Normal file
105
crates/protocol/src/method/parse.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
596
crates/protocol/src/method/query.rs
Normal file
596
crates/protocol/src/method/query.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
136
crates/protocol/src/method/query_changes.rs
Normal file
136
crates/protocol/src/method/query_changes.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
91
crates/protocol/src/method/search_snippet.rs
Normal file
91
crates/protocol/src/method/search_snippet.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
352
crates/protocol/src/method/set.rs
Normal file
352
crates/protocol/src/method/set.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
56
crates/protocol/src/method/validate.rs
Normal file
56
crates/protocol/src/method/validate.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
73
crates/protocol/src/object/email.rs
Normal file
73
crates/protocol/src/object/email.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
crates/protocol/src/object/email_submission.rs
Normal file
39
crates/protocol/src/object/email_submission.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
crates/protocol/src/object/mailbox.rs
Normal file
58
crates/protocol/src/object/mailbox.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
13
crates/protocol/src/object/mod.rs
Normal file
13
crates/protocol/src/object/mod.rs
Normal 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>,
|
||||
}
|
||||
37
crates/protocol/src/object/sieve.rs
Normal file
37
crates/protocol/src/object/sieve.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/protocol/src/parser/base32.rs
Normal file
59
crates/protocol/src/parser/base32.rs
Normal 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> {}
|
||||
276
crates/protocol/src/parser/impls.rs
Normal file
276
crates/protocol/src/parser/impls.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
383
crates/protocol/src/parser/json.rs
Normal file
383
crates/protocol/src/parser/json.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
164
crates/protocol/src/parser/mod.rs
Normal file
164
crates/protocol/src/parser/mod.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
69
crates/protocol/src/request/capability.rs
Normal file
69
crates/protocol/src/request/capability.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/protocol/src/request/echo.rs
Normal file
32
crates/protocol/src/request/echo.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
196
crates/protocol/src/request/method.rs
Normal file
196
crates/protocol/src/request/method.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
109
crates/protocol/src/request/mod.rs
Normal file
109
crates/protocol/src/request/mod.rs
Normal 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>;
|
||||
}
|
||||
220
crates/protocol/src/request/parser.rs
Normal file
220
crates/protocol/src/request/parser.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
101
crates/protocol/src/request/reference.rs
Normal file
101
crates/protocol/src/request/reference.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/protocol/src/types/acl.rs
Normal file
82
crates/protocol/src/types/acl.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
219
crates/protocol/src/types/blob.rs
Normal file
219
crates/protocol/src/types/blob.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
12
crates/protocol/src/types/collection.rs
Normal file
12
crates/protocol/src/types/collection.rs
Normal 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,
|
||||
}
|
||||
266
crates/protocol/src/types/date.rs
Normal file
266
crates/protocol/src/types/date.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
283
crates/protocol/src/types/id.rs
Normal file
283
crates/protocol/src/types/id.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
116
crates/protocol/src/types/keyword.rs
Normal file
116
crates/protocol/src/types/keyword.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
14
crates/protocol/src/types/mod.rs
Normal file
14
crates/protocol/src/types/mod.rs
Normal 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;
|
||||
276
crates/protocol/src/types/pointer.rs
Normal file
276
crates/protocol/src/types/pointer.rs
Normal 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
807
crates/protocol/src/types/property.rs
Normal file
807
crates/protocol/src/types/property.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
191
crates/protocol/src/types/state.rs
Normal file
191
crates/protocol/src/types/state.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
crates/protocol/src/types/type_state.rs
Normal file
69
crates/protocol/src/types/type_state.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
206
crates/protocol/src/types/value.rs
Normal file
206
crates/protocol/src/types/value.rs
Normal 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
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
40
crates/store/Cargo.toml
Normal 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 = []
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
15
tests/Cargo.toml
Normal 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
Loading…
Add table
Reference in a new issue