Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "jmap_proto"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
types = { path = "../types" }
|
||||
trc = { path = "../trc" }
|
||||
registry = { path = "../registry" }
|
||||
jmap-tools = { version = "0.1" }
|
||||
calcard = { version = "0.3" }
|
||||
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
|
||||
serde = { version = "1.0", features = ["derive"]}
|
||||
ahash = { version = "0.8.12", features = ["serde"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
hashify = "0.2"
|
||||
rkyv = { version = "0.8.18", features = ["little_endian"] }
|
||||
compact_str = { version = "0.10.0", features = ["rkyv", "serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::ser::SerializeMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[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,
|
||||
CannotCalculateChanges,
|
||||
UnknownDataType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MethodErrorWrapper(trc::Error);
|
||||
|
||||
impl From<trc::Error> for MethodErrorWrapper {
|
||||
fn from(value: trc::Error) -> Self {
|
||||
MethodErrorWrapper(value)
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
MethodError::UnknownDataType => write!(f, "Unknown data type"),
|
||||
MethodError::CannotCalculateChanges => write!(f, "Cannot calculate changes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for MethodErrorWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(2.into())?;
|
||||
|
||||
let description = self.0.value(trc::Key::Details).and_then(|v| v.as_str());
|
||||
|
||||
let (error_type, description) = match self.0.as_ref() {
|
||||
trc::EventType::Jmap(cause) => match cause {
|
||||
trc::JmapEvent::InvalidArguments => {
|
||||
("invalidArguments", description.unwrap_or_default())
|
||||
}
|
||||
trc::JmapEvent::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."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::StateMismatch => (
|
||||
"stateMismatch",
|
||||
concat!(
|
||||
"An \"ifInState\" argument was supplied, but ",
|
||||
"it does not match the current state."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::AnchorNotFound => (
|
||||
"anchorNotFound",
|
||||
concat!(
|
||||
"An anchor argument was supplied, but it ",
|
||||
"cannot be found in the results of the query."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::UnsupportedFilter => {
|
||||
("unsupportedFilter", description.unwrap_or_default())
|
||||
}
|
||||
trc::JmapEvent::UnsupportedSort => {
|
||||
("unsupportedSort", description.unwrap_or_default())
|
||||
}
|
||||
trc::JmapEvent::NotFound => ("serverPartialFail", {
|
||||
concat!(
|
||||
"One or more items are no longer available on the ",
|
||||
"server, please try again."
|
||||
)
|
||||
}),
|
||||
trc::JmapEvent::UnknownMethod => ("unknownMethod", description.unwrap_or_default()),
|
||||
trc::JmapEvent::InvalidResultReference => {
|
||||
("invalidResultReference", description.unwrap_or_default())
|
||||
}
|
||||
trc::JmapEvent::Forbidden => ("forbidden", description.unwrap_or_default()),
|
||||
trc::JmapEvent::AccountNotFound => (
|
||||
"accountNotFound",
|
||||
"The accountId does not correspond to a valid account",
|
||||
),
|
||||
trc::JmapEvent::AccountNotSupportedByMethod => (
|
||||
"accountNotSupportedByMethod",
|
||||
concat!(
|
||||
"The accountId given corresponds to a valid account, ",
|
||||
"but the account does not support this method or data type."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::AccountReadOnly => (
|
||||
"accountReadOnly",
|
||||
"This method modifies state, but the account is read-only.",
|
||||
),
|
||||
trc::JmapEvent::UnknownDataType => (
|
||||
"unknownDataType",
|
||||
concat!(
|
||||
"The server does not recognise this data type, ",
|
||||
"or the capability to enable it is not present ",
|
||||
"in the current Request Object."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::CannotCalculateChanges => (
|
||||
"cannotCalculateChanges",
|
||||
concat!(
|
||||
"The server cannot calculate the changes ",
|
||||
"between the old and new states."
|
||||
),
|
||||
),
|
||||
trc::JmapEvent::UnknownCapability
|
||||
| trc::JmapEvent::NotJson
|
||||
| trc::JmapEvent::NotRequest => (
|
||||
"serverUnavailable",
|
||||
concat!(
|
||||
"This server is temporarily unavailable. ",
|
||||
"Attempting this same operation later may succeed."
|
||||
),
|
||||
),
|
||||
_ => (
|
||||
"serverUnavailable",
|
||||
"This server is temporarily unavailable.",
|
||||
),
|
||||
},
|
||||
_ => (
|
||||
"serverUnavailable",
|
||||
concat!(
|
||||
"This server is temporarily unavailable. ",
|
||||
"Attempting this same operation later may succeed."
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
map.serialize_entry("type", error_type)?;
|
||||
if !description.is_empty() {
|
||||
map.serialize_entry("description", description)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod method;
|
||||
pub mod request;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RequestLimitError {
|
||||
#[serde(rename = "maxSizeRequest")]
|
||||
SizeRequest,
|
||||
#[serde(rename = "maxSizeUpload")]
|
||||
SizeUpload,
|
||||
#[serde(rename = "maxCallsInRequest")]
|
||||
CallsIn,
|
||||
#[serde(rename = "maxConcurrentRequests")]
|
||||
ConcurrentRequest,
|
||||
#[serde(rename = "maxConcurrentUpload")]
|
||||
ConcurrentUpload,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RequestErrorType {
|
||||
#[serde(rename = "urn:ietf:params:jmap:error:unknownCapability")]
|
||||
UnknownCapability,
|
||||
#[serde(rename = "urn:ietf:params:jmap:error:notJSON")]
|
||||
NotJSON,
|
||||
#[serde(rename = "urn:ietf:params:jmap:error:notRequest")]
|
||||
NotRequest,
|
||||
#[serde(rename = "urn:ietf:params:jmap:error:limit")]
|
||||
Limit,
|
||||
#[serde(rename = "about:blank")]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimitPolicy {
|
||||
pub name: &'static str,
|
||||
pub limit: u64,
|
||||
pub remaining: u64,
|
||||
pub window: Option<u64>,
|
||||
pub reset: Option<u64>,
|
||||
pub unit: RateLimitUnit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RateLimitUnit {
|
||||
Requests,
|
||||
ContentBytes,
|
||||
ConcurrentRequests,
|
||||
}
|
||||
|
||||
impl RateLimitUnit {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
RateLimitUnit::Requests => "requests",
|
||||
RateLimitUnit::ContentBytes => "content-bytes",
|
||||
RateLimitUnit::ConcurrentRequests => "concurrent-requests",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RequestError<'x> {
|
||||
#[serde(rename = "type")]
|
||||
pub p_type: RequestErrorType,
|
||||
pub status: u16,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<Cow<'x, str>>,
|
||||
pub detail: Cow<'x, str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<RequestLimitError>,
|
||||
#[serde(skip)]
|
||||
pub rate_limit: Vec<RateLimitPolicy>,
|
||||
#[serde(skip)]
|
||||
pub retry_after: Option<u64>,
|
||||
}
|
||||
|
||||
impl<'x> RequestError<'x> {
|
||||
pub fn blank(
|
||||
status: u16,
|
||||
title: impl Into<Cow<'x, str>>,
|
||||
detail: impl Into<Cow<'x, str>>,
|
||||
) -> Self {
|
||||
RequestError {
|
||||
p_type: RequestErrorType::Other,
|
||||
status,
|
||||
title: Some(title.into()),
|
||||
detail: detail.into(),
|
||||
limit: None,
|
||||
rate_limit: Vec::new(),
|
||||
retry_after: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_rate_limit(mut self, policy: RateLimitPolicy) -> Self {
|
||||
if let Some(reset) = policy.reset
|
||||
&& self.retry_after.is_none_or(|r| reset > r)
|
||||
{
|
||||
self.retry_after = Some(reset);
|
||||
}
|
||||
self.rate_limit.push(policy);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_retry_after(mut self, seconds: u64) -> Self {
|
||||
if self.retry_after.is_none_or(|r| seconds > r) {
|
||||
self.retry_after = Some(seconds);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
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 if this problem persists."
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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 over_blob_quota(max_files: usize, max_bytes: usize) -> Self {
|
||||
RequestError::blank(
|
||||
429,
|
||||
"Quota exceeded",
|
||||
format!(
|
||||
"You have exceeded the blob upload quota of {} files or {} bytes.",
|
||||
max_files, max_bytes
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn over_quota() -> Self {
|
||||
RequestError::blank(
|
||||
403,
|
||||
"Quota exceeded",
|
||||
"You have exceeded your account quota.",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tenant_over_quota() -> Self {
|
||||
RequestError::blank(
|
||||
403,
|
||||
"Tenant quota exceeded",
|
||||
"Your organization has exceeded its quota.",
|
||||
)
|
||||
}
|
||||
|
||||
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::SizeRequest => concat!(
|
||||
"The request is larger than the server ",
|
||||
"is willing to process."
|
||||
),
|
||||
RequestLimitError::SizeUpload => concat!(
|
||||
"The uploaded file 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::ConcurrentRequest => concat!(
|
||||
"The request exceeds the maximum number ",
|
||||
"of concurrent requests."
|
||||
),
|
||||
RequestLimitError::ConcurrentUpload => concat!(
|
||||
"The request exceeds the maximum number ",
|
||||
"of concurrent uploads."
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
limit: Some(limit_type),
|
||||
rate_limit: Vec::new(),
|
||||
retry_after: None,
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
rate_limit: Vec::new(),
|
||||
retry_after: None,
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
rate_limit: Vec::new(),
|
||||
retry_after: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_request(detail: impl Into<Cow<'x, str>>) -> RequestError<'x> {
|
||||
RequestError {
|
||||
p_type: RequestErrorType::NotRequest,
|
||||
limit: None,
|
||||
title: None,
|
||||
status: 400,
|
||||
detail: detail.into(),
|
||||
rate_limit: Vec::new(),
|
||||
retry_after: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimitPolicy {
|
||||
pub fn new(name: &'static str, limit: u64) -> Self {
|
||||
RateLimitPolicy {
|
||||
name,
|
||||
limit,
|
||||
remaining: 0,
|
||||
window: None,
|
||||
reset: None,
|
||||
unit: RateLimitUnit::Requests,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_window(mut self, window: u64) -> Self {
|
||||
self.window = Some(window);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_reset(mut self, reset: u64) -> Self {
|
||||
self.reset = Some(reset);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_remaining(mut self, remaining: u64) -> Self {
|
||||
self.remaining = remaining;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_unit(mut self, unit: RateLimitUnit) -> Self {
|
||||
self.unit = unit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fmt_policy(&self, out: &mut String) {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(out, "\"{}\";q={}", self.name, self.limit);
|
||||
if let Some(window) = self.window {
|
||||
let _ = write!(out, ";w={window}");
|
||||
}
|
||||
if !matches!(self.unit, RateLimitUnit::Requests) {
|
||||
let _ = write!(out, ";qu=\"{}\"", self.unit.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fmt_state(&self, out: &mut String) {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(out, "\"{}\";r={}", self.name, self.remaining);
|
||||
if let Some(reset) = self.reset {
|
||||
let _ = write!(out, ";t={reset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> RequestError<'x> {
|
||||
pub fn rate_limit_policy_header(&self) -> Option<String> {
|
||||
if self.rate_limit.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (i, policy) in self.rate_limit.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
policy.fmt_policy(&mut out);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
pub fn rate_limit_state_header(&self) -> Option<String> {
|
||||
if self.rate_limit.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (i, policy) in self.rate_limit.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
policy.fmt_state(&mut out);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RequestError<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rate_limit_headers_match_spec() {
|
||||
// Spec example: RateLimit-Policy: "burst";q=100;w=60,"daily";q=1000;w=86400
|
||||
let mut p1 = String::new();
|
||||
RateLimitPolicy::new("burst", 100)
|
||||
.with_window(60)
|
||||
.fmt_policy(&mut p1);
|
||||
assert_eq!(p1, r#""burst";q=100;w=60"#);
|
||||
|
||||
// Spec example: RateLimit-Policy: "peruser";q=65535;qu="content-bytes";w=10
|
||||
let mut p2 = String::new();
|
||||
RateLimitPolicy::new("peruser", 65535)
|
||||
.with_window(10)
|
||||
.with_unit(RateLimitUnit::ContentBytes)
|
||||
.fmt_policy(&mut p2);
|
||||
assert_eq!(p2, r#""peruser";q=65535;w=10;qu="content-bytes""#);
|
||||
|
||||
// Spec example: RateLimit: "default";r=50;t=30
|
||||
let mut s = String::new();
|
||||
RateLimitPolicy::new("default", 100)
|
||||
.with_remaining(50)
|
||||
.with_reset(30)
|
||||
.fmt_state(&mut s);
|
||||
assert_eq!(s, r#""default";r=50;t=30"#);
|
||||
|
||||
// Two policies in one header
|
||||
let err = RequestError::too_many_requests()
|
||||
.with_rate_limit(
|
||||
RateLimitPolicy::new("burst", 100)
|
||||
.with_window(60)
|
||||
.with_reset(30),
|
||||
)
|
||||
.with_rate_limit(
|
||||
RateLimitPolicy::new("daily", 1000)
|
||||
.with_window(86400)
|
||||
.with_reset(3600),
|
||||
);
|
||||
assert_eq!(
|
||||
err.rate_limit_policy_header().as_deref(),
|
||||
Some(r#""burst";q=100;w=60, "daily";q=1000;w=86400"#),
|
||||
);
|
||||
assert_eq!(
|
||||
err.rate_limit_state_header().as_deref(),
|
||||
Some(r#""burst";r=0;t=30, "daily";r=0;t=3600"#),
|
||||
);
|
||||
assert_eq!(err.retry_after, Some(3600));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_tools::{Key, Property};
|
||||
use registry::types::{
|
||||
error::{PatchError, ValidationError},
|
||||
id::ObjectId,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(bound(serialize = "InvalidProperty<P>: serde::Serialize"))]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub struct SetError<P: Property>(Box<SetErrorInner<P>>);
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(bound(serialize = "InvalidProperty<P>: serde::Serialize"))]
|
||||
struct SetErrorInner<P: Property> {
|
||||
#[serde(rename = "type")]
|
||||
type_: SetErrorType,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<Cow<'static, str>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
properties: Option<Vec<InvalidProperty<P>>>,
|
||||
|
||||
#[serde(rename = "existingId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
existing_id: Option<Id>,
|
||||
|
||||
#[serde(rename = "objectId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
object_id: Option<ObjectId>,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(rename = "linkedObjects")]
|
||||
linked_objects: Vec<ObjectId>,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(rename = "validationErrors")]
|
||||
validation_errors: Vec<ValidationError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InvalidProperty<T: Property> {
|
||||
Property(Key<'static, T>),
|
||||
Path(Vec<Key<'static, T>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
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,
|
||||
#[serde(rename = "addressBookHasContents")]
|
||||
AddressBookHasContents,
|
||||
#[serde(rename = "nodeHasChildren")]
|
||||
NodeHasChildren,
|
||||
#[serde(rename = "calendarHasEvent")]
|
||||
CalendarHasEvent,
|
||||
#[serde(rename = "noSupportedScheduleMethods")]
|
||||
NoSupportedScheduleMethods,
|
||||
// Stalwart registry errors
|
||||
#[serde(rename = "objectIsLinked")]
|
||||
ObjectIsLinked,
|
||||
#[serde(rename = "invalidForeignKey")]
|
||||
InvalidForeignKey,
|
||||
#[serde(rename = "primaryKeyViolation")]
|
||||
PrimaryKeyViolation,
|
||||
#[serde(rename = "validationFailed")]
|
||||
ValidationFailed,
|
||||
}
|
||||
|
||||
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",
|
||||
SetErrorType::AddressBookHasContents => "addressBookHasContents",
|
||||
SetErrorType::NodeHasChildren => "nodeHasChildren",
|
||||
SetErrorType::CalendarHasEvent => "calendarHasEvent",
|
||||
SetErrorType::NoSupportedScheduleMethods => "noSupportedScheduleMethods",
|
||||
SetErrorType::ObjectIsLinked => "objectIsLinked",
|
||||
SetErrorType::InvalidForeignKey => "invalidForeignKey",
|
||||
SetErrorType::PrimaryKeyViolation => "primaryKeyViolation",
|
||||
SetErrorType::ValidationFailed => "validationFailed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> SetError<T> {
|
||||
pub fn new(type_: SetErrorType) -> Self {
|
||||
SetError(Box::new(SetErrorInner {
|
||||
type_,
|
||||
description: None,
|
||||
properties: None,
|
||||
existing_id: None,
|
||||
object_id: None,
|
||||
linked_objects: Vec::new(),
|
||||
validation_errors: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.0.description = description.into().into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> &SetErrorType {
|
||||
&self.0.type_
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.0.description.as_deref()
|
||||
}
|
||||
|
||||
pub fn validation_errors(&self) -> &[ValidationError] {
|
||||
&self.0.validation_errors
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, property: impl Into<InvalidProperty<T>>) -> Self {
|
||||
self.0.properties = vec![property.into()].into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_properties(
|
||||
mut self,
|
||||
properties: impl IntoIterator<Item = impl Into<InvalidProperty<T>>>,
|
||||
) -> Self {
|
||||
self.0.properties = properties
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_object_id(mut self, object_id: ObjectId) -> Self {
|
||||
self.0.object_id = object_id.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_object_id_opt(mut self, object_id: Option<ObjectId>) -> Self {
|
||||
self.0.object_id = object_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_linked_objects(mut self, linked_objects: Vec<ObjectId>) -> Self {
|
||||
self.0.linked_objects = linked_objects;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_validation_errors(mut self, validation_errors: Vec<ValidationError>) -> Self {
|
||||
self.0.validation_errors = validation_errors;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_existing_id(mut self, id: Id) -> Self {
|
||||
self.0.existing_id = id.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn invalid_properties() -> Self {
|
||||
Self::new(SetErrorType::InvalidProperties)
|
||||
}
|
||||
|
||||
pub fn invalid_patch() -> Self {
|
||||
Self::new(SetErrorType::InvalidPatch)
|
||||
}
|
||||
|
||||
pub fn forbidden() -> Self {
|
||||
Self::new(SetErrorType::Forbidden)
|
||||
}
|
||||
|
||||
pub fn not_found() -> Self {
|
||||
Self::new(SetErrorType::NotFound)
|
||||
}
|
||||
|
||||
pub fn blob_not_found() -> Self {
|
||||
Self::new(SetErrorType::BlobNotFound)
|
||||
}
|
||||
|
||||
pub fn over_quota() -> Self {
|
||||
Self::new(SetErrorType::OverQuota).with_description("Account quota exceeded.")
|
||||
}
|
||||
|
||||
pub fn already_exists() -> Self {
|
||||
Self::new(SetErrorType::AlreadyExists)
|
||||
}
|
||||
|
||||
pub fn no_supported_schedule_methods(calendar_address: &str) -> Self {
|
||||
Self::new(SetErrorType::NoSupportedScheduleMethods).with_description(format!(
|
||||
"No supported scheduling method for calendar address {calendar_address}."
|
||||
))
|
||||
}
|
||||
|
||||
pub fn too_large() -> Self {
|
||||
Self::new(SetErrorType::TooLarge)
|
||||
}
|
||||
|
||||
pub fn will_destroy() -> Self {
|
||||
Self::new(SetErrorType::WillDestroy).with_description("ID will be destroyed.")
|
||||
}
|
||||
|
||||
pub fn singleton() -> Self {
|
||||
Self::new(SetErrorType::Singleton)
|
||||
.with_description("Singletons cannot be created or destroyed.")
|
||||
}
|
||||
|
||||
pub fn address_book_has_contents() -> Self {
|
||||
Self::new(SetErrorType::AddressBookHasContents)
|
||||
.with_description("Address book is not empty.")
|
||||
}
|
||||
|
||||
pub fn node_has_children() -> Self {
|
||||
Self::new(SetErrorType::NodeHasChildren).with_description("Cannot delete non-empty folder.")
|
||||
}
|
||||
|
||||
pub fn calendar_has_event() -> Self {
|
||||
Self::new(SetErrorType::CalendarHasEvent).with_description("Calendar is not empty.")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> From<T> for InvalidProperty<T> {
|
||||
fn from(property: T) -> Self {
|
||||
InvalidProperty::Property(Key::Property(property))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> From<(T, T)> for InvalidProperty<T> {
|
||||
fn from((a, b): (T, T)) -> Self {
|
||||
InvalidProperty::Path(vec![Key::Property(a), Key::Property(b)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> From<Key<'static, T>> for InvalidProperty<T> {
|
||||
fn from(property: Key<'static, T>) -> Self {
|
||||
InvalidProperty::Property(property)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> serde::Serialize for InvalidProperty<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
InvalidProperty::Property(p) => p.serialize(serializer),
|
||||
InvalidProperty::Path(p) => {
|
||||
use std::fmt::Write;
|
||||
let mut path = String::with_capacity(64);
|
||||
for (i, p) in p.iter().enumerate() {
|
||||
if i > 0 {
|
||||
path.push('/');
|
||||
}
|
||||
let _ = write!(path, "{}", p.to_string());
|
||||
}
|
||||
path.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatchError> for SetError<registry::schema::properties::Property> {
|
||||
fn from(err: PatchError) -> Self {
|
||||
SetError(Box::new(SetErrorInner {
|
||||
type_: SetErrorType::InvalidPatch,
|
||||
description: err.message.into(),
|
||||
properties: Some(vec![InvalidProperty::Property(Key::Owned(err.path))]),
|
||||
existing_id: None,
|
||||
object_id: None,
|
||||
linked_objects: Vec::new(),
|
||||
validation_errors: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod error;
|
||||
pub mod method;
|
||||
pub mod object;
|
||||
pub mod references;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod types;
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use calcard::jscalendar::{JSCalendar, JSCalendarProperty};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GetAvailabilityRequest {
|
||||
pub account_id: Id,
|
||||
pub id: Id,
|
||||
pub utc_start: UTCDate,
|
||||
pub utc_end: UTCDate,
|
||||
pub show_details: bool,
|
||||
pub event_properties: Option<Vec<MaybeInvalid<JSCalendarProperty<Id>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetAvailabilityResponse {
|
||||
pub list: Vec<BusyPeriod>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BusyPeriod {
|
||||
pub utc_start: UTCDate,
|
||||
pub utc_end: UTCDate,
|
||||
pub busy_status: Option<BusyStatus>,
|
||||
pub event: Option<JSCalendar<'static, Id, BlobId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BusyStatus {
|
||||
Confirmed,
|
||||
Tentative,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for GetAvailabilityRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"utcStart" => {
|
||||
self.utc_start = map.next_value()?;
|
||||
},
|
||||
b"utcEnd" => {
|
||||
self.utc_end = map.next_value()?;
|
||||
},
|
||||
b"id" => {
|
||||
self.id = map.next_value()?;
|
||||
},
|
||||
b"showDetails" => {
|
||||
self.show_details = map.next_value()?;
|
||||
},
|
||||
b"eventProperties" => {
|
||||
self.event_properties = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GetAvailabilityRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
method::PropertyWrapper,
|
||||
object::JmapObject,
|
||||
request::deserialize::{DeserializeArguments, deserialize_request},
|
||||
types::state::State,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ChangesRequest {
|
||||
pub account_id: Id,
|
||||
pub since_state: State,
|
||||
pub max_changes: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ChangesResponse<T: JmapObject> {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "oldState")]
|
||||
pub old_state: State,
|
||||
|
||||
#[serde(rename = "newState")]
|
||||
pub new_state: State,
|
||||
|
||||
#[serde(rename = "hasMoreChanges")]
|
||||
pub has_more_changes: bool,
|
||||
|
||||
pub created: Vec<Id>,
|
||||
|
||||
pub updated: Vec<Id>,
|
||||
|
||||
pub destroyed: Vec<Id>,
|
||||
|
||||
#[serde(rename = "updatedProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_properties: Option<Vec<PropertyWrapper<T::Property>>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ChangesRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"sinceState" => {
|
||||
self.since_state = map.next_value()?;
|
||||
},
|
||||
b"maxChanges" => {
|
||||
self.max_changes = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ChangesRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ChangesResponse<T> {
|
||||
pub fn has_changes(&self) -> bool {
|
||||
!self.created.is_empty() || !self.updated.is_empty() || !self.destroyed.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
object::{JmapObject, blob::BlobProperty},
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::MaybeIdReference,
|
||||
},
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CopyRequest<'x, T: JmapObject> {
|
||||
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<MaybeIdReference<Id>, Value<'x, T::Property, T::Element>>,
|
||||
pub on_success_destroy_original: Option<bool>,
|
||||
pub destroy_from_if_in_state: Option<State>,
|
||||
pub arguments: T::CopyArguments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct CopyResponse<T: JmapObject> {
|
||||
#[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, Value<'static, T::Property, T::Element>>,
|
||||
|
||||
#[serde(rename = "notCreated")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_created: VecMap<Id, SetError<T::Property>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CopyBlobRequest {
|
||||
pub from_account_id: Id,
|
||||
pub account_id: Id,
|
||||
pub blob_ids: Vec<MaybeInvalid<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 = "VecMap::is_empty")]
|
||||
pub copied: VecMap<BlobId, BlobId>,
|
||||
|
||||
#[serde(rename = "notCopied")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_copied: VecMap<MaybeInvalid<BlobId>, SetError<BlobProperty>>,
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for CopyRequest<'de, T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"ifInState" => {
|
||||
self.if_in_state = map.next_value()?;
|
||||
},
|
||||
b"fromAccountId" => {
|
||||
self.from_account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"ifFromInState" => {
|
||||
self.if_from_in_state = map.next_value()?;
|
||||
},
|
||||
b"create" => {
|
||||
self.create = map.next_value()?;
|
||||
},
|
||||
b"onSuccessDestroyOriginal" => {
|
||||
self.on_success_destroy_original = map.next_value()?;
|
||||
},
|
||||
b"destroyFromIfInState" => {
|
||||
self.destroy_from_if_in_state = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CopyBlobRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"fromAccountId" => {
|
||||
self.from_account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"blobIds" => {
|
||||
self.blob_ids = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for CopyRequest<'de, T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CopyBlobRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Default for CopyRequest<'de, T> {
|
||||
fn default() -> Self {
|
||||
CopyRequest {
|
||||
from_account_id: Id::default(),
|
||||
if_from_in_state: None,
|
||||
account_id: Id::default(),
|
||||
if_in_state: None,
|
||||
create: VecMap::new(),
|
||||
on_success_destroy_original: None,
|
||||
destroy_from_if_in_state: None,
|
||||
arguments: T::CopyArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> CopyResponse<T> {
|
||||
pub fn created(&mut self, id: Id, document_id: impl Into<T::Id>) {
|
||||
let document_id = document_id.into();
|
||||
self.created.append(
|
||||
id,
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(T::ID_PROPERTY),
|
||||
Value::Element(document_id.into()),
|
||||
)])),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::JmapObject,
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::{MaybeIdReference, MaybeResultReference, ResultReference},
|
||||
},
|
||||
types::state::State,
|
||||
};
|
||||
use jmap_tools::Value;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetRequest<T: JmapObject> {
|
||||
pub account_id: Id,
|
||||
pub ids: Option<MaybeResultReference<Vec<MaybeIdReference<T::Id>>>>,
|
||||
pub properties: Option<MaybeResultReference<Vec<MaybeInvalid<T::Property>>>>,
|
||||
pub arguments: T::GetArguments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct GetResponse<T: JmapObject> {
|
||||
#[serde(rename = "accountId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub account_id: Option<Id>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<State>,
|
||||
|
||||
pub list: Vec<Value<'static, T::Property, T::Element>>,
|
||||
|
||||
#[serde(rename = "notFound")]
|
||||
pub not_found: Vec<MaybeInvalid<T::Id>>,
|
||||
}
|
||||
|
||||
impl<T: JmapObject> GetResponse<T> {
|
||||
pub fn push_not_found(&mut self, id: T::Id) {
|
||||
self.not_found.push(MaybeInvalid::Value(id));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for GetRequest<T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"ids" => {
|
||||
self.ids = map.next_value::<Option<Vec<MaybeIdReference<T::Id>>>>()?.map(MaybeResultReference::Value);
|
||||
},
|
||||
b"properties" => {
|
||||
self.properties = map.next_value::<Option<Vec<MaybeInvalid<T::Property>>>>()?.map(MaybeResultReference::Value);
|
||||
},
|
||||
b"#ids" => {
|
||||
self.ids = Some(MaybeResultReference::Reference(map.next_value::<ResultReference>()?));
|
||||
},
|
||||
b"#properties" => {
|
||||
self.properties = Some(MaybeResultReference::Reference(map.next_value::<ResultReference>()?));
|
||||
},
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for GetRequest<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> Default for GetRequest<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
ids: None,
|
||||
properties: None,
|
||||
arguments: T::GetArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> GetRequest<T> {
|
||||
pub fn unwrap_properties(&mut self, default: &[T::Property]) -> Vec<T::Property> {
|
||||
if let Some(properties_) = self.properties.take().map(|p| p.unwrap()) {
|
||||
let mut properties = Vec::with_capacity(properties_.len());
|
||||
let id_prop = T::ID_PROPERTY;
|
||||
let mut has_id = false;
|
||||
|
||||
for prop in properties_ {
|
||||
if let MaybeInvalid::Value(p) = prop {
|
||||
if p == id_prop {
|
||||
has_id = true;
|
||||
}
|
||||
properties.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
if !has_id {
|
||||
properties.push(id_prop);
|
||||
}
|
||||
|
||||
properties
|
||||
} else {
|
||||
default.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn unwrap_ids(
|
||||
&mut self,
|
||||
max_objects_in_get: usize,
|
||||
) -> trc::Result<(Option<Vec<T::Id>>, Vec<MaybeInvalid<T::Id>>)> {
|
||||
if let Some(ids) = self.ids.take() {
|
||||
let ids = ids.unwrap();
|
||||
if ids.len() <= max_objects_in_get {
|
||||
let mut valid = Vec::with_capacity(ids.len());
|
||||
let mut invalid = Vec::new();
|
||||
for id in ids {
|
||||
match id {
|
||||
MaybeIdReference::Id(id) => valid.push(id),
|
||||
MaybeIdReference::Invalid(s) | MaybeIdReference::Reference(s) => {
|
||||
invalid.push(MaybeInvalid::Invalid(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((Some(valid), invalid))
|
||||
} else {
|
||||
Err(trc::JmapEvent::RequestTooLarge.into_err())
|
||||
}
|
||||
} else {
|
||||
Ok((None, Vec::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
method::JmapDict,
|
||||
object::{
|
||||
AnyId,
|
||||
email::{EmailProperty, EmailValue},
|
||||
},
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::{MaybeIdReference, MaybeResultReference, ResultReference},
|
||||
},
|
||||
response::Response,
|
||||
types::{date::UTCDate, state::State},
|
||||
};
|
||||
use jmap_tools::{Key, Value};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{blob::BlobId, id::Id, keyword::Keyword};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ImportEmailRequest {
|
||||
pub account_id: Id,
|
||||
pub if_in_state: Option<State>,
|
||||
pub emails: VecMap<String, ImportEmail>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ImportEmail {
|
||||
pub blob_id: MaybeInvalid<BlobId>,
|
||||
pub mailbox_ids: MaybeResultReference<Vec<MaybeIdReference<Id>>>,
|
||||
pub keywords: Vec<Keyword>,
|
||||
pub received_at: Option<UTCDate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ImportEmailResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "oldState")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub old_state: Option<State>,
|
||||
|
||||
#[serde(rename = "newState")]
|
||||
pub new_state: State,
|
||||
|
||||
#[serde(rename = "created")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub created: VecMap<String, Value<'static, EmailProperty, EmailValue>>,
|
||||
|
||||
#[serde(rename = "notCreated")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_created: VecMap<String, SetError<EmailProperty>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ImportEmailRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"ifInState" => {
|
||||
self.if_in_state = map.next_value()?;
|
||||
},
|
||||
b"emails" => {
|
||||
self.emails = map.next_value()?;
|
||||
}
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ImportEmail {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"blobId" => {
|
||||
self.blob_id = map.next_value()?;
|
||||
},
|
||||
b"keywords" => {
|
||||
self.keywords = map.next_value::<JmapDict<Keyword>>()?.0;
|
||||
},
|
||||
b"receivedAt" => {
|
||||
self.received_at = map.next_value()?;
|
||||
},
|
||||
b"mailboxIds" => {
|
||||
self.mailbox_ids = MaybeResultReference::Value(map.next_value::<JmapDict<MaybeIdReference<Id>>>()?.0);
|
||||
},
|
||||
b"#mailboxIds" => {
|
||||
self.mailbox_ids = MaybeResultReference::Reference(map.next_value::<ResultReference>()?);
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ImportEmail {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ImportEmailRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportEmailResponse {
|
||||
pub fn update_created_ids(&self, response: &mut Response) {
|
||||
for (user_id, obj) in &self.created {
|
||||
if let Value::Object(obj) = obj
|
||||
&& let Some(Value::Element(EmailValue::Id(id))) =
|
||||
obj.get(&Key::Property(EmailProperty::Id))
|
||||
{
|
||||
response.created_ids.insert(user_id.clone(), AnyId::Id(*id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{blob::BlobId, id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BlobLookupRequest {
|
||||
pub account_id: Id,
|
||||
pub type_names: Vec<MaybeInvalid<DataType>>,
|
||||
pub ids: Vec<MaybeInvalid<BlobId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct BlobLookupResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "list")]
|
||||
pub list: Vec<BlobInfo>,
|
||||
|
||||
#[serde(rename = "notFound")]
|
||||
pub not_found: Vec<BlobId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct BlobInfo {
|
||||
pub id: BlobId,
|
||||
#[serde(rename = "matchedIds")]
|
||||
pub matched_ids: VecMap<DataType, Vec<Id>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for BlobLookupRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"typeNames" => {
|
||||
self.type_names = map.next_value()?;
|
||||
},
|
||||
b"ids" => {
|
||||
self.ids = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BlobLookupRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use jmap_tools::Property;
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{self, MapAccess, Visitor},
|
||||
};
|
||||
use std::{borrow::Cow, fmt, str::FromStr};
|
||||
|
||||
pub mod availability;
|
||||
pub mod changes;
|
||||
pub mod copy;
|
||||
pub mod get;
|
||||
pub mod import;
|
||||
pub mod lookup;
|
||||
pub mod parse;
|
||||
pub mod query;
|
||||
pub mod query_changes;
|
||||
pub mod search_snippet;
|
||||
pub mod set;
|
||||
pub mod upload;
|
||||
pub mod validate;
|
||||
|
||||
#[inline(always)]
|
||||
fn ahash_is_empty<K, V>(map: &AHashMap<K, V>) -> bool {
|
||||
map.is_empty()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct PropertyWrapper<T: Property>(pub T);
|
||||
|
||||
impl<T: Property> From<T> for PropertyWrapper<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> Serialize for PropertyWrapper<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.0.to_cow().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct JmapDict<T: FromStr>(pub Vec<T>);
|
||||
|
||||
struct JmapDictVisitor<'de, T: FromStr> {
|
||||
marker: std::marker::PhantomData<&'de T>,
|
||||
}
|
||||
|
||||
impl<'de, T: FromStr> Visitor<'de> for JmapDictVisitor<'de, T> {
|
||||
type Value = JmapDict<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut vec = Vec::with_capacity(3);
|
||||
|
||||
while let Some(key) = access.next_key::<Cow<'de, str>>()? {
|
||||
let key = T::from_str(&key).map_err(|_| de::Error::custom("invalid dictionary key"))?;
|
||||
if access.next_value::<Option<bool>>()?.unwrap_or(false) {
|
||||
vec.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(JmapDict(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: FromStr + 'static> Deserialize<'de> for JmapDict<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_map(JmapDictVisitor {
|
||||
marker: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::JmapObject,
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::MaybeIdReference,
|
||||
},
|
||||
};
|
||||
use jmap_tools::Value;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParseRequest<T: JmapObject> {
|
||||
pub account_id: Id,
|
||||
pub blob_ids: Vec<MaybeIdReference<BlobId>>,
|
||||
pub properties: Option<Vec<MaybeInvalid<T::Property>>>,
|
||||
pub arguments: T::ParseArguments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ParseResponse<T: JmapObject> {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "parsed")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub parsed: VecMap<BlobId, Value<'static, T::Property, T::Element>>,
|
||||
|
||||
#[serde(rename = "notParsable")]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub not_parsable: Vec<BlobId>,
|
||||
|
||||
#[serde(rename = "notFound")]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub not_found: Vec<MaybeInvalid<BlobId>>,
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for ParseRequest<T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"blobIds" => {
|
||||
self.blob_ids = map.next_value()?;
|
||||
},
|
||||
b"properties" => {
|
||||
self.properties = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for ParseRequest<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> Default for ParseRequest<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
blob_ids: Vec::default(),
|
||||
properties: None,
|
||||
arguments: T::ParseArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::JmapObject,
|
||||
request::deserialize::{DeserializeArguments, deserialize_request},
|
||||
types::state::State,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryRequest<T: JmapObject> {
|
||||
pub account_id: Id,
|
||||
pub filter: Vec<Filter<T::Filter>>,
|
||||
pub sort: Option<Vec<Comparator<T::Comparator>>>,
|
||||
pub position: Option<i32>,
|
||||
pub anchor: Option<Id>,
|
||||
pub anchor_offset: Option<i32>,
|
||||
pub limit: Option<usize>,
|
||||
pub calculate_total: Option<bool>,
|
||||
pub arguments: T::QueryArguments,
|
||||
}
|
||||
|
||||
#[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, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
||||
pub enum Filter<T>
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default,
|
||||
{
|
||||
Property(T),
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Comparator<T>
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default,
|
||||
{
|
||||
pub is_ascending: bool,
|
||||
pub collation: Option<String>,
|
||||
pub property: T,
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryRequest<T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"filter" => {
|
||||
self.filter = map.next_value::<FilterWrapper<T::Filter>>()?.0;
|
||||
},
|
||||
b"sort" => {
|
||||
self.sort = map.next_value()?;
|
||||
},
|
||||
b"calculateTotal" => {
|
||||
self.calculate_total = map.next_value()?;
|
||||
},
|
||||
b"position" => {
|
||||
self.position = map.next_value()?;
|
||||
},
|
||||
b"anchor" => {
|
||||
self.anchor = map
|
||||
.next_value::<Option<crate::request::MaybeInvalid<Id>>>()?
|
||||
.map(|anchor| anchor.try_unwrap().unwrap_or(Id::from(u64::MAX)));
|
||||
},
|
||||
b"anchorOffset" => {
|
||||
self.anchor_offset = map.next_value()?;
|
||||
},
|
||||
b"limit" => {
|
||||
self.limit = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for QueryRequest<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> Default for QueryRequest<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
filter: vec![],
|
||||
sort: None,
|
||||
position: None,
|
||||
anchor: None,
|
||||
anchor_offset: None,
|
||||
limit: None,
|
||||
calculate_total: None,
|
||||
arguments: T::QueryArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FilterMapCollector<'x, T: 'x>(&'x mut Vec<Filter<T>>)
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default;
|
||||
|
||||
struct FilterListCollector<'x, T: 'x>(&'x mut Vec<Filter<T>>)
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default;
|
||||
|
||||
pub struct FilterWrapper<T>(pub Vec<Filter<T>>)
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default;
|
||||
|
||||
impl<'de, T> Deserialize<'de> for FilterWrapper<T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
FilterMapCollector(&mut items)
|
||||
.deserialize(deserializer)
|
||||
.map(|_| FilterWrapper(items))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, 'x, T> DeserializeSeed<'de> for FilterMapCollector<'x, T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
type Value = ();
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FilterVisitor<'x, T: 'x>(&'x mut Vec<Filter<T>>)
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default;
|
||||
|
||||
impl<'de, 'x, T> Visitor<'de> for FilterVisitor<'x, T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
type Value = ();
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "a filter object")
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<(), E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<(), E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<(), V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut filter = None;
|
||||
let mut has_multiple_filters = false;
|
||||
let mut has_conditions = None;
|
||||
let mut op = None;
|
||||
|
||||
while let Some(key) = map.next_key::<Cow<str>>()? {
|
||||
match key.len() {
|
||||
8 if key == "operator" => {
|
||||
let op_ = hashify::tiny_map!(
|
||||
map.next_value::<&str>()?.as_bytes(),
|
||||
"AND" => Filter::And,
|
||||
"OR" => Filter::Or,
|
||||
"NOT" => Filter::Not,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
de::Error::custom(format!("Unknown filter operator: {}", key))
|
||||
})?;
|
||||
|
||||
if let Some(pos) = has_conditions {
|
||||
self.0[pos] = op_;
|
||||
} else {
|
||||
op = Some(op_);
|
||||
}
|
||||
}
|
||||
10 if key == "conditions" => {
|
||||
has_conditions = Some(self.0.len());
|
||||
self.0.push(op.take().unwrap_or(Filter::And));
|
||||
map.next_value_seed(FilterListCollector(self.0))?;
|
||||
self.0.push(Filter::Close);
|
||||
}
|
||||
_ => {
|
||||
if let Some(filter) = filter {
|
||||
if !has_multiple_filters {
|
||||
self.0.push(Filter::And);
|
||||
has_multiple_filters = true;
|
||||
}
|
||||
self.0.push(Filter::Property(filter));
|
||||
}
|
||||
let mut new_filter = T::default();
|
||||
new_filter.deserialize_argument(&key, &mut map)?;
|
||||
filter = Some(new_filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(filter) = filter {
|
||||
if has_conditions.is_some() {
|
||||
return Err(de::Error::custom(
|
||||
"Cannot mix conditions with property filters",
|
||||
));
|
||||
}
|
||||
|
||||
self.0.push(Filter::Property(filter));
|
||||
if has_multiple_filters {
|
||||
self.0.push(Filter::Close);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(FilterVisitor(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, 'x, T> DeserializeSeed<'de> for FilterListCollector<'x, T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
type Value = ();
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FilterVisitor<'x, T: 'x>(&'x mut Vec<Filter<T>>)
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default;
|
||||
|
||||
impl<'de, 'x, T> Visitor<'de> for FilterVisitor<'x, T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
type Value = ();
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "a filter list")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<(), A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
while let Some(()) = seq.next_element_seed(FilterMapCollector(self.0))? {}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(FilterVisitor(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeArguments<'de> for Comparator<T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"isAscending" => {
|
||||
self.is_ascending = map.next_value()?;
|
||||
},
|
||||
b"collation" => {
|
||||
self.collation = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
self.property.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for Comparator<T>
|
||||
where
|
||||
T: for<'de2> DeserializeArguments<'de2> + Default,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Comparator<T>
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default,
|
||||
{
|
||||
pub fn descending(property: T) -> Self {
|
||||
Self {
|
||||
property,
|
||||
is_ascending: false,
|
||||
collation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ascending(property: T) -> Self {
|
||||
Self {
|
||||
property,
|
||||
is_ascending: true,
|
||||
collation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Comparator<T>
|
||||
where
|
||||
T: for<'de> DeserializeArguments<'de> + Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_ascending: true,
|
||||
collation: None,
|
||||
property: T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
method::query::{Comparator, Filter, FilterWrapper, QueryRequest},
|
||||
object::JmapObject,
|
||||
request::deserialize::{DeserializeArguments, deserialize_request},
|
||||
types::state::State,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryChangesRequest<T: JmapObject> {
|
||||
pub account_id: Id,
|
||||
pub filter: Vec<Filter<T::Filter>>,
|
||||
pub sort: Option<Vec<Comparator<T::Comparator>>>,
|
||||
pub since_query_state: State,
|
||||
pub max_changes: Option<usize>,
|
||||
pub up_to_id: Option<Id>,
|
||||
pub calculate_total: Option<bool>,
|
||||
pub arguments: T::QueryArguments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct QueryChangesResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "oldQueryState")]
|
||||
pub old_query_state: State,
|
||||
|
||||
#[serde(rename = "newQueryState")]
|
||||
pub new_query_state: State,
|
||||
|
||||
#[serde(rename = "total")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total: Option<usize>,
|
||||
|
||||
#[serde(rename = "removed")]
|
||||
pub removed: Vec<Id>,
|
||||
|
||||
#[serde(rename = "added")]
|
||||
pub added: Vec<AddedItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct AddedItem {
|
||||
pub id: Id,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl AddedItem {
|
||||
pub fn new(id: Id, index: usize) -> Self {
|
||||
Self { id, index }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for QueryChangesRequest<T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"filter" => {
|
||||
self.filter = map.next_value::<FilterWrapper<T::Filter>>()?.0;
|
||||
},
|
||||
b"sort" => {
|
||||
self.sort = map.next_value()?;
|
||||
},
|
||||
b"sinceQueryState" => {
|
||||
self.since_query_state = map.next_value()?;
|
||||
},
|
||||
b"maxChanges" => {
|
||||
self.max_changes = map.next_value()?;
|
||||
},
|
||||
b"upToId" => {
|
||||
self.up_to_id = map.next_value()?;
|
||||
},
|
||||
b"calculateTotal" => {
|
||||
self.calculate_total = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for QueryChangesRequest<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> Default for QueryChangesRequest<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
filter: Vec::new(),
|
||||
sort: None,
|
||||
since_query_state: State::default(),
|
||||
max_changes: None,
|
||||
up_to_id: None,
|
||||
calculate_total: None,
|
||||
arguments: T::QueryArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> From<QueryChangesRequest<T>> for QueryRequest<T> {
|
||||
fn from(request: QueryChangesRequest<T>) -> Self {
|
||||
QueryRequest {
|
||||
account_id: request.account_id,
|
||||
filter: request.filter,
|
||||
sort: request.sort,
|
||||
position: None,
|
||||
anchor: None,
|
||||
anchor_offset: None,
|
||||
limit: None,
|
||||
calculate_total: request.calculate_total,
|
||||
arguments: request.arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::query::Filter;
|
||||
use crate::{
|
||||
method::query::FilterWrapper,
|
||||
object::email::EmailFilter,
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::{MaybeResultReference, ResultReference},
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetSearchSnippetRequest {
|
||||
pub account_id: Id,
|
||||
pub filter: Vec<Filter<EmailFilter>>,
|
||||
pub email_ids: MaybeResultReference<Vec<MaybeInvalid<Id>>>,
|
||||
}
|
||||
|
||||
#[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")]
|
||||
pub not_found: Option<Vec<MaybeInvalid<Id>>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone, Debug)]
|
||||
pub struct SearchSnippet {
|
||||
#[serde(rename = "emailId")]
|
||||
pub email_id: Id,
|
||||
|
||||
pub subject: Option<String>,
|
||||
|
||||
pub preview: Option<String>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for GetSearchSnippetRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"filter" => {
|
||||
self.filter = map.next_value::<FilterWrapper<EmailFilter>>()?.0;
|
||||
},
|
||||
b"emailIds" => {
|
||||
self.email_ids = MaybeResultReference::Value(map.next_value::<Vec<MaybeInvalid<Id>>>()?);
|
||||
},
|
||||
b"#emailIds" => {
|
||||
self.email_ids = MaybeResultReference::Reference(map.next_value::<ResultReference>()?);
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GetSearchSnippetRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GetSearchSnippetRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
filter: Vec::new(),
|
||||
email_ids: MaybeResultReference::Value(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ahash_is_empty;
|
||||
use crate::{
|
||||
error::set::{InvalidProperty, SetError},
|
||||
object::{JmapObject, JmapObjectId},
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::{MaybeResultReference, ResultReference},
|
||||
},
|
||||
response::Response,
|
||||
types::state::State,
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::id::Id;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct SetRequest<'x, T: JmapObject> {
|
||||
pub account_id: Id,
|
||||
pub if_in_state: Option<State>,
|
||||
pub create: Option<VecMap<String, Value<'x, T::Property, T::Element>>>,
|
||||
pub update: Option<VecMap<MaybeInvalid<Id>, Value<'x, T::Property, T::Element>>>,
|
||||
pub destroy: Option<MaybeResultReference<Vec<MaybeInvalid<Id>>>>,
|
||||
pub arguments: T::SetArguments<'x>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct SetResponse<T: JmapObject> {
|
||||
#[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, Value<'static, T::Property, T::Element>>,
|
||||
|
||||
#[serde(rename = "updated")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub updated: VecMap<Id, Option<Value<'static, T::Property, T::Element>>>,
|
||||
|
||||
#[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<T::Property>>,
|
||||
|
||||
#[serde(rename = "notUpdated")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_updated: VecMap<MaybeInvalid<Id>, SetError<T::Property>>,
|
||||
|
||||
#[serde(rename = "notDestroyed")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_destroyed: VecMap<MaybeInvalid<Id>, SetError<T::Property>>,
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> DeserializeArguments<'de> for SetRequest<'de, T> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"ifInState" => {
|
||||
self.if_in_state = map.next_value()?;
|
||||
},
|
||||
b"create" => {
|
||||
self.create = map.next_value()?;
|
||||
},
|
||||
b"update" => {
|
||||
self.update = map.next_value()?;
|
||||
},
|
||||
b"destroy" => {
|
||||
self.destroy = map.next_value::<Option<Vec<MaybeInvalid<Id>>>>()?.map(MaybeResultReference::Value);
|
||||
},
|
||||
b"#destroy" => {
|
||||
self.destroy = Some(MaybeResultReference::Reference(map.next_value::<ResultReference>()?));
|
||||
}
|
||||
_ => {
|
||||
self.arguments.deserialize_argument(key, map)?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JmapObject> Deserialize<'de> for SetRequest<'de, T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: JmapObject> Default for SetRequest<'x, T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
account_id: Id::default(),
|
||||
if_in_state: None,
|
||||
create: None,
|
||||
update: None,
|
||||
destroy: None,
|
||||
arguments: T::SetArguments::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: JmapObject> SetRequest<'x, T> {
|
||||
pub fn validate(&self, max_objects_in_set: usize) -> trc::Result<()> {
|
||||
if self.create.as_ref().map_or(0, |objs| objs.len())
|
||||
+ self.update.as_ref().map_or(0, |objs| objs.len())
|
||||
+ self.destroy.as_ref().map_or(0, |objs| {
|
||||
if let MaybeResultReference::Value(ids) = objs {
|
||||
ids.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
> max_objects_in_set
|
||||
{
|
||||
Err(trc::JmapEvent::RequestTooLarge.into_err())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_updates(&self) -> bool {
|
||||
self.update.as_ref().is_some_and(|objs| !objs.is_empty())
|
||||
}
|
||||
|
||||
pub fn has_creates(&self) -> bool {
|
||||
self.create.as_ref().is_some_and(|objs| !objs.is_empty())
|
||||
}
|
||||
|
||||
pub fn unwrap_create(&mut self) -> VecMap<String, Value<'x, T::Property, T::Element>> {
|
||||
self.create.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn unwrap_update(
|
||||
&mut self,
|
||||
) -> VecMap<MaybeInvalid<Id>, Value<'x, T::Property, T::Element>> {
|
||||
self.update.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn unwrap_destroy(&mut self) -> Vec<MaybeInvalid<Id>> {
|
||||
self.destroy
|
||||
.take()
|
||||
.map(|ids| ids.unwrap())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> SetResponse<T> {
|
||||
pub fn from_request(request: &SetRequest<T>, max_objects: usize) -> trc::Result<Self> {
|
||||
let n_create = request.create.as_ref().map_or(0, |objs| objs.len());
|
||||
let n_update = request.update.as_ref().map_or(0, |objs| objs.len());
|
||||
let n_destroy = request.destroy.as_ref().map_or(0, |objs| {
|
||||
if let MaybeResultReference::Value(ids) = objs {
|
||||
ids.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
if n_create + n_update + n_destroy <= max_objects {
|
||||
Ok(SetResponse {
|
||||
account_id: if request.account_id.is_valid() {
|
||||
request.account_id.into()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
new_state: None,
|
||||
old_state: None,
|
||||
created: AHashMap::with_capacity(n_create),
|
||||
updated: VecMap::with_capacity(n_update),
|
||||
destroyed: Vec::with_capacity(n_destroy),
|
||||
not_created: VecMap::new(),
|
||||
not_updated: VecMap::new(),
|
||||
not_destroyed: VecMap::new(),
|
||||
})
|
||||
} else {
|
||||
Err(trc::JmapEvent::RequestTooLarge.into_err())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_state(mut self, state: State) -> Self {
|
||||
self.old_state = Some(state.clone());
|
||||
self.new_state = Some(state);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn collect_will_destroy(&mut self, ids: Vec<MaybeInvalid<Id>>) -> Vec<Id> {
|
||||
let mut will_destroy = Vec::with_capacity(ids.len());
|
||||
for id in ids {
|
||||
match id {
|
||||
MaybeInvalid::Value(id) => will_destroy.push(id),
|
||||
invalid => self.not_destroyed.append(invalid, SetError::not_found()),
|
||||
}
|
||||
}
|
||||
will_destroy
|
||||
}
|
||||
|
||||
pub fn created(&mut self, id: String, document_id: impl Into<T::Id>) {
|
||||
self.created.insert(
|
||||
id,
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(T::ID_PROPERTY),
|
||||
Value::Element(document_id.into().into()),
|
||||
)])),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn invalid_property_create(
|
||||
&mut self,
|
||||
id: String,
|
||||
property: impl Into<InvalidProperty<T::Property>>,
|
||||
) {
|
||||
self.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description("Invalid property or value.".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn invalid_property_update(
|
||||
&mut self,
|
||||
id: Id,
|
||||
property: impl Into<InvalidProperty<T::Property>>,
|
||||
) {
|
||||
self.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description("Invalid property or value.".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_created_ids(&self, response: &mut Response) {
|
||||
for (user_id, obj) in &self.created {
|
||||
if let Value::Object(obj) = obj
|
||||
&& let Some(Value::Element(id)) = obj.get(&Key::Property(T::ID_PROPERTY))
|
||||
&& let Some(id) = id.as_any_id()
|
||||
{
|
||||
response.created_ids.insert(user_id.clone(), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_object_by_id(
|
||||
&mut self,
|
||||
id: Id,
|
||||
) -> Option<&mut Value<'static, T::Property, T::Element>> {
|
||||
if let Some(obj) = self.updated.get_mut(&id) {
|
||||
if let Some(obj) = obj {
|
||||
return Some(obj);
|
||||
} else {
|
||||
*obj = Some(Value::Object(Map::with_capacity(1)));
|
||||
return obj.as_mut().unwrap().into();
|
||||
}
|
||||
}
|
||||
|
||||
(&mut self.created)
|
||||
.into_iter()
|
||||
.map(|(_, obj)| obj)
|
||||
.find(|obj| {
|
||||
obj.as_object_and_get(&Key::Property(T::ID_PROPERTY))
|
||||
.and_then(|v| v.as_element())
|
||||
.and_then(|v| v.as_id())
|
||||
.is_some_and(|oid| oid == id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_changes(&self) -> bool {
|
||||
!self.created.is_empty() || !self.updated.is_empty() || !self.destroyed.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::ahash_is_empty;
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
object::{AnyId, blob::BlobProperty},
|
||||
request::{
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
reference::MaybeIdReference,
|
||||
},
|
||||
response::Response,
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BlobUploadRequest {
|
||||
pub account_id: Id,
|
||||
pub create: VecMap<String, UploadObject>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UploadObject {
|
||||
pub type_: Option<String>,
|
||||
pub data: Vec<DataSourceObject>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum DataSourceObject {
|
||||
Id {
|
||||
id: MaybeIdReference<BlobId>,
|
||||
length: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
},
|
||||
Value(Vec<u8>),
|
||||
#[default]
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct BlobUploadResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
|
||||
#[serde(rename = "created")]
|
||||
#[serde(skip_serializing_if = "ahash_is_empty")]
|
||||
pub created: AHashMap<String, BlobUploadResponseObject>,
|
||||
|
||||
#[serde(rename = "notCreated")]
|
||||
#[serde(skip_serializing_if = "VecMap::is_empty")]
|
||||
pub not_created: VecMap<String, SetError<BlobProperty>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct BlobUploadResponseObject {
|
||||
pub id: BlobId,
|
||||
#[serde(rename = "type")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub type_: Option<String>,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for BlobUploadRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"create" => {
|
||||
self.create = map.next_value()?;
|
||||
}
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for UploadObject {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"type" => {
|
||||
self.type_ = map.next_value()?;
|
||||
},
|
||||
b"data" => {
|
||||
self.data = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for DataSourceObject {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"data:asText" => {
|
||||
*self = DataSourceObject::Value(map.next_value::<String>().map(|v| v.into_bytes())?);
|
||||
},
|
||||
b"data:asBase64" => {
|
||||
*self = DataSourceObject::Value(base64_decode(map.next_value::<Cow<'_, str>>()?.as_bytes()).ok_or_else(|| serde::de::Error::custom("Failed to decode base64 data"))?);
|
||||
},
|
||||
b"blobId" => {
|
||||
match self {
|
||||
DataSourceObject::Id { id, .. } => {
|
||||
*id = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
*self = DataSourceObject::Id {
|
||||
id: map.next_value()?,
|
||||
length: None,
|
||||
offset: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
b"offset" => {
|
||||
match self {
|
||||
DataSourceObject::Id { offset, .. } => {
|
||||
*offset = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
*self = DataSourceObject::Id {
|
||||
id: MaybeIdReference::Invalid("".into()),
|
||||
length: None,
|
||||
offset: map.next_value()?,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
b"length" => {
|
||||
match self {
|
||||
DataSourceObject::Id { length, .. } => {
|
||||
*length = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
*self = DataSourceObject::Id {
|
||||
id: MaybeIdReference::Invalid("".into()),
|
||||
length: map.next_value()?,
|
||||
offset: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BlobUploadResponse {
|
||||
pub fn update_created_ids(&self, response: &mut Response) {
|
||||
for (user_id, obj) in &self.created {
|
||||
response
|
||||
.created_ids
|
||||
.insert(user_id.clone(), AnyId::BlobId(obj.id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DataSourceObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for UploadObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BlobUploadRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
object::sieve::SieveProperty,
|
||||
request::{
|
||||
MaybeInvalid,
|
||||
deserialize::{DeserializeArguments, deserialize_request},
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ValidateSieveScriptRequest {
|
||||
pub account_id: Id,
|
||||
pub blob_id: MaybeInvalid<BlobId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ValidateSieveScriptResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
pub account_id: Id,
|
||||
pub error: Option<SetError<SieveProperty>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ValidateSieveScriptRequest {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountId" => {
|
||||
self.account_id = crate::request::deserialize_account_id(map)?;
|
||||
},
|
||||
b"blobId" => {
|
||||
self.blob_id = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ValidateSieveScriptRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_request(deserializer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref,
|
||||
},
|
||||
request::{deserialize::DeserializeArguments, reference::MaybeIdReference},
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{acl::Acl, id::Id, special_use::SpecialUse};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AddressBook;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AddressBookProperty {
|
||||
Id,
|
||||
Name,
|
||||
Description,
|
||||
SortOrder,
|
||||
IsDefault,
|
||||
IsSubscribed,
|
||||
ShareWith,
|
||||
MyRights,
|
||||
|
||||
// Other
|
||||
IdValue(Id),
|
||||
Rights(AddressBookRight),
|
||||
Pointer(JsonPointer<AddressBookProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AddressBookRight {
|
||||
MayRead,
|
||||
MayWrite,
|
||||
MayShare,
|
||||
MayDelete,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AddressBookValue {
|
||||
Id(Id),
|
||||
IdReference(String),
|
||||
Role(SpecialUse),
|
||||
}
|
||||
|
||||
impl Property for AddressBookProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
AddressBookProperty::ShareWith => {
|
||||
Id::from_str(value).ok().map(AddressBookProperty::IdValue)
|
||||
}
|
||||
_ => AddressBookProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
AddressBookProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
AddressBookProperty::Id => "id",
|
||||
AddressBookProperty::Name => "name",
|
||||
AddressBookProperty::Description => "description",
|
||||
AddressBookProperty::SortOrder => "sortOrder",
|
||||
AddressBookProperty::IsDefault => "isDefault",
|
||||
AddressBookProperty::IsSubscribed => "isSubscribed",
|
||||
AddressBookProperty::ShareWith => "shareWith",
|
||||
AddressBookProperty::MyRights => "myRights",
|
||||
AddressBookProperty::Rights(addressbook_right) => addressbook_right.as_str(),
|
||||
AddressBookProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
AddressBookProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressBookRight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AddressBookRight::MayRead => "mayRead",
|
||||
AddressBookRight::MayWrite => "mayWrite",
|
||||
AddressBookRight::MayShare => "mayShare",
|
||||
AddressBookRight::MayDelete => "mayDelete",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AddressBookValue {
|
||||
type Property = AddressBookProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
AddressBookProperty::Id => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(AddressBookValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(AddressBookValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
AddressBookValue::Id(id) => id.to_string().into(),
|
||||
AddressBookValue::IdReference(r) => format!("#{r}").into(),
|
||||
AddressBookValue::Role(special_use) => special_use.as_str().unwrap_or_default().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressBookProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => AddressBookProperty::Id,
|
||||
b"name" => AddressBookProperty::Name,
|
||||
b"description" => AddressBookProperty::Description,
|
||||
b"sortOrder" => AddressBookProperty::SortOrder,
|
||||
b"isDefault" => AddressBookProperty::IsDefault,
|
||||
b"isSubscribed" => AddressBookProperty::IsSubscribed,
|
||||
b"shareWith" => AddressBookProperty::ShareWith,
|
||||
b"myRights" => AddressBookProperty::MyRights,
|
||||
b"mayRead" => AddressBookProperty::Rights(AddressBookRight::MayRead),
|
||||
b"mayWrite" => AddressBookProperty::Rights(AddressBookRight::MayWrite),
|
||||
b"mayShare" => AddressBookProperty::Rights(AddressBookRight::MayShare),
|
||||
b"mayDelete" => AddressBookProperty::Rights(AddressBookRight::MayDelete)
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
AddressBookProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &AddressBookProperty {
|
||||
if let AddressBookProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AddressBookSetArguments {
|
||||
pub on_destroy_remove_contents: Option<bool>,
|
||||
pub on_success_set_is_default: Option<MaybeIdReference<Id>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for AddressBookSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onDestroyRemoveContents" => {
|
||||
self.on_destroy_remove_contents = map.next_value()?;
|
||||
},
|
||||
b"onSuccessSetIsDefault" => {
|
||||
self.on_success_set_is_default = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AddressBookProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
AddressBookProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for AddressBook {
|
||||
type Property = AddressBookProperty;
|
||||
|
||||
type Element = AddressBookValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = AddressBookSetArguments;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = AddressBookProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapSharedObject for AddressBook {
|
||||
type Right = AddressBookRight;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property = AddressBookProperty::ShareWith;
|
||||
}
|
||||
|
||||
impl From<Id> for AddressBookProperty {
|
||||
fn from(id: Id) -> Self {
|
||||
AddressBookProperty::IdValue(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AddressBookProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AddressBookProperty) -> Result<Self, Self::Error> {
|
||||
if let AddressBookProperty::IdValue(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AddressBookProperty> for AddressBookRight {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AddressBookProperty) -> Result<Self, Self::Error> {
|
||||
if let AddressBookProperty::Rights(right) = value {
|
||||
Ok(right)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for AddressBookValue {
|
||||
fn from(id: Id) -> Self {
|
||||
AddressBookValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for AddressBookValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let AddressBookValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let AddressBookValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let AddressBookValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(new_id) = new_id {
|
||||
*self = AddressBookValue::Id(new_id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapRight for AddressBookRight {
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
match self {
|
||||
AddressBookRight::MayDelete => &[Acl::Delete, Acl::RemoveItems],
|
||||
AddressBookRight::MayShare => &[Acl::Share],
|
||||
AddressBookRight::MayRead => &[Acl::Read, Acl::ReadItems],
|
||||
AddressBookRight::MayWrite => &[Acl::Modify, Acl::AddItems, Acl::ModifyItems],
|
||||
}
|
||||
}
|
||||
|
||||
fn all_rights() -> &'static [Self] {
|
||||
&[
|
||||
AddressBookRight::MayRead,
|
||||
AddressBookRight::MayWrite,
|
||||
AddressBookRight::MayDelete,
|
||||
AddressBookRight::MayShare,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressBookRight> for AddressBookProperty {
|
||||
fn from(right: AddressBookRight) -> Self {
|
||||
AddressBookProperty::Rights(right)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for AddressBookProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let AddressBookProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let AddressBookProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(new_id) = new_id {
|
||||
*self = AddressBookProperty::IdValue(new_id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AddressBookProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_cow())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref},
|
||||
request::deserialize::DeserializeArguments,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Blob;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum BlobProperty {
|
||||
Id,
|
||||
BlobId,
|
||||
Type,
|
||||
Size,
|
||||
Digest(DigestProperty),
|
||||
Data(DataProperty),
|
||||
IsEncodingProblem,
|
||||
IsTruncated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DigestProperty {
|
||||
Sha,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DataProperty {
|
||||
AsText,
|
||||
AsBase64,
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum BlobValue {
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
impl Property for BlobProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
BlobProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
BlobProperty::BlobId => "blobId",
|
||||
BlobProperty::Id => "id",
|
||||
BlobProperty::Size => "size",
|
||||
BlobProperty::Type => "type",
|
||||
BlobProperty::IsEncodingProblem => "isEncodingProblem",
|
||||
BlobProperty::IsTruncated => "isTruncated",
|
||||
BlobProperty::Data(data) => match data {
|
||||
DataProperty::AsText => "data:asText",
|
||||
DataProperty::AsBase64 => "data:asBase64",
|
||||
DataProperty::Default => "data",
|
||||
},
|
||||
BlobProperty::Digest(digest) => match digest {
|
||||
DigestProperty::Sha => "digest:sha",
|
||||
DigestProperty::Sha256 => "digest:sha-256",
|
||||
DigestProperty::Sha512 => "digest:sha-512",
|
||||
},
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for BlobValue {
|
||||
type Property = BlobProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
BlobProperty::BlobId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(BlobValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(BlobValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
BlobValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
BlobValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlobProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"blobId" => BlobProperty::BlobId,
|
||||
b"id" => BlobProperty::Id,
|
||||
b"size" => BlobProperty::Size,
|
||||
b"type" => BlobProperty::Type,
|
||||
b"isEncodingProblem" => BlobProperty::IsEncodingProblem,
|
||||
b"isTruncated" => BlobProperty::IsTruncated,
|
||||
b"data:asText" => BlobProperty::Data(DataProperty::AsText),
|
||||
b"data:asBase64" => BlobProperty::Data(DataProperty::AsBase64),
|
||||
b"data" => BlobProperty::Data(DataProperty::Default),
|
||||
b"digest:sha" => BlobProperty::Digest(DigestProperty::Sha),
|
||||
b"digest:sha-256" => BlobProperty::Digest(DigestProperty::Sha256),
|
||||
b"digest:sha-512" => BlobProperty::Digest(DigestProperty::Sha512),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BlobProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
BlobProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BlobGetArguments {
|
||||
pub offset: Option<usize>,
|
||||
pub length: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for BlobGetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"offset" => {
|
||||
self.offset = map.next_value()?;
|
||||
},
|
||||
b"length" => {
|
||||
self.length = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Blob {
|
||||
type Property = BlobProperty;
|
||||
|
||||
type Element = BlobValue;
|
||||
|
||||
type Id = BlobId;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = BlobGetArguments;
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = BlobProperty::Id;
|
||||
}
|
||||
|
||||
impl From<BlobId> for BlobValue {
|
||||
fn from(id: BlobId) -> Self {
|
||||
BlobValue::BlobId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for BlobValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
BlobValue::BlobId(id) => Some(AnyId::BlobId(id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let BlobValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::BlobId(id) = new_id {
|
||||
*self = BlobValue::BlobId(id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for BlobProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref,
|
||||
},
|
||||
request::{deserialize::DeserializeArguments, reference::MaybeIdReference},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use calcard::{
|
||||
common::{IanaParse, timezone::Tz},
|
||||
icalendar::ICalendarDuration,
|
||||
jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType},
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::{acl::Acl, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Calendar;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarProperty {
|
||||
Id,
|
||||
Name,
|
||||
Description,
|
||||
Color,
|
||||
SortOrder,
|
||||
IsSubscribed,
|
||||
IsVisible,
|
||||
IsDefault,
|
||||
IncludeInAvailability,
|
||||
DefaultAlertsWithTime,
|
||||
DefaultAlertsWithoutTime,
|
||||
TimeZone,
|
||||
ShareWith,
|
||||
MyRights,
|
||||
|
||||
// Alert object properties
|
||||
When,
|
||||
Trigger,
|
||||
Offset,
|
||||
RelativeTo,
|
||||
Action,
|
||||
Type,
|
||||
|
||||
// Other
|
||||
IdValue(Id),
|
||||
Rights(CalendarRight),
|
||||
Pointer(JsonPointer<CalendarProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarRight {
|
||||
MayReadFreeBusy,
|
||||
MayReadItems,
|
||||
MayWriteAll,
|
||||
MayWriteOwn,
|
||||
MayUpdatePrivate,
|
||||
MayRSVP,
|
||||
MayShare,
|
||||
MayDelete,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarValue {
|
||||
Id(Id),
|
||||
IdReference(String),
|
||||
IncludeInAvailability(IncludeInAvailability),
|
||||
Date(UTCDate),
|
||||
Timezone(Tz),
|
||||
Action(JSCalendarAlertAction),
|
||||
RelativeTo(JSCalendarRelativeTo),
|
||||
Type(JSCalendarType),
|
||||
Duration(ICalendarDuration),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum IncludeInAvailability {
|
||||
#[default]
|
||||
All,
|
||||
Attending,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Property for CalendarProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
CalendarProperty::ShareWith => {
|
||||
Id::from_str(value).ok().map(CalendarProperty::IdValue)
|
||||
}
|
||||
_ => CalendarProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
CalendarProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarProperty::Id => "id",
|
||||
CalendarProperty::Name => "name",
|
||||
CalendarProperty::Description => "description",
|
||||
CalendarProperty::Color => "color",
|
||||
CalendarProperty::SortOrder => "sortOrder",
|
||||
CalendarProperty::IsSubscribed => "isSubscribed",
|
||||
CalendarProperty::IsVisible => "isVisible",
|
||||
CalendarProperty::IsDefault => "isDefault",
|
||||
CalendarProperty::IncludeInAvailability => "includeInAvailability",
|
||||
CalendarProperty::DefaultAlertsWithTime => "defaultAlertsWithTime",
|
||||
CalendarProperty::DefaultAlertsWithoutTime => "defaultAlertsWithoutTime",
|
||||
CalendarProperty::TimeZone => "timeZone",
|
||||
CalendarProperty::ShareWith => "shareWith",
|
||||
CalendarProperty::MyRights => "myRights",
|
||||
CalendarProperty::When => "when",
|
||||
CalendarProperty::Trigger => "trigger",
|
||||
CalendarProperty::Offset => "offset",
|
||||
CalendarProperty::RelativeTo => "relativeTo",
|
||||
CalendarProperty::Action => "action",
|
||||
CalendarProperty::Type => "@type",
|
||||
CalendarProperty::Rights(calendar_right) => calendar_right.as_str(),
|
||||
CalendarProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
CalendarProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarRight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarRight::MayReadFreeBusy => "mayReadFreeBusy",
|
||||
CalendarRight::MayReadItems => "mayReadItems",
|
||||
CalendarRight::MayWriteAll => "mayWriteAll",
|
||||
CalendarRight::MayWriteOwn => "mayWriteOwn",
|
||||
CalendarRight::MayUpdatePrivate => "mayUpdatePrivate",
|
||||
CalendarRight::MayRSVP => "mayRSVP",
|
||||
CalendarRight::MayShare => "mayShare",
|
||||
CalendarRight::MayDelete => "mayDelete",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IncludeInAvailability {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"all" => IncludeInAvailability::All,
|
||||
b"attending" => IncludeInAvailability::Attending,
|
||||
b"none" => IncludeInAvailability::None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
IncludeInAvailability::All => "all",
|
||||
IncludeInAvailability::Attending => "attending",
|
||||
IncludeInAvailability::None => "none",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for CalendarValue {
|
||||
type Property = CalendarProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
CalendarProperty::Id => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(CalendarValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(CalendarValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
CalendarProperty::TimeZone => Tz::from_str(value).ok().map(CalendarValue::Timezone),
|
||||
CalendarProperty::IncludeInAvailability => {
|
||||
IncludeInAvailability::parse(value).map(CalendarValue::IncludeInAvailability)
|
||||
}
|
||||
CalendarProperty::Action => JSCalendarAlertAction::from_str(value)
|
||||
.ok()
|
||||
.map(CalendarValue::Action),
|
||||
CalendarProperty::RelativeTo => JSCalendarRelativeTo::from_str(value)
|
||||
.ok()
|
||||
.map(CalendarValue::RelativeTo),
|
||||
CalendarProperty::When => UTCDate::from_str(value).ok().map(CalendarValue::Date),
|
||||
CalendarProperty::Offset => {
|
||||
ICalendarDuration::parse(value.as_bytes()).map(CalendarValue::Duration)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarValue::Id(id) => id.to_string().into(),
|
||||
CalendarValue::IdReference(r) => format!("#{r}").into(),
|
||||
CalendarValue::IncludeInAvailability(include) => include.as_str().into(),
|
||||
CalendarValue::Date(date) => date.to_string().into(),
|
||||
CalendarValue::Action(action) => action.as_str().into(),
|
||||
CalendarValue::RelativeTo(relative) => relative.as_str().into(),
|
||||
CalendarValue::Type(typ) => typ.as_str().into(),
|
||||
CalendarValue::Duration(dur) => dur.to_string().into(),
|
||||
CalendarValue::Timezone(tz) => tz.name().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => CalendarProperty::Id,
|
||||
b"name" => CalendarProperty::Name,
|
||||
b"description" => CalendarProperty::Description,
|
||||
b"color" => CalendarProperty::Color,
|
||||
b"sortOrder" => CalendarProperty::SortOrder,
|
||||
b"isSubscribed" => CalendarProperty::IsSubscribed,
|
||||
b"isVisible" => CalendarProperty::IsVisible,
|
||||
b"isDefault" => CalendarProperty::IsDefault,
|
||||
b"includeInAvailability" => CalendarProperty::IncludeInAvailability,
|
||||
b"defaultAlertsWithTime" => CalendarProperty::DefaultAlertsWithTime,
|
||||
b"defaultAlertsWithoutTime" => CalendarProperty::DefaultAlertsWithoutTime,
|
||||
b"timeZone" => CalendarProperty::TimeZone,
|
||||
b"shareWith" => CalendarProperty::ShareWith,
|
||||
b"myRights" => CalendarProperty::MyRights,
|
||||
b"mayReadFreeBusy" => CalendarProperty::Rights(CalendarRight::MayReadFreeBusy),
|
||||
b"mayReadItems" => CalendarProperty::Rights(CalendarRight::MayReadItems),
|
||||
b"mayWriteAll" => CalendarProperty::Rights(CalendarRight::MayWriteAll),
|
||||
b"mayWriteOwn" => CalendarProperty::Rights(CalendarRight::MayWriteOwn),
|
||||
b"mayUpdatePrivate" => CalendarProperty::Rights(CalendarRight::MayUpdatePrivate),
|
||||
b"mayRSVP" => CalendarProperty::Rights(CalendarRight::MayRSVP),
|
||||
b"mayShare" => CalendarProperty::Rights(CalendarRight::MayShare),
|
||||
b"mayDelete" => CalendarProperty::Rights(CalendarRight::MayDelete),
|
||||
b"@type" => CalendarProperty::Type,
|
||||
b"when" => CalendarProperty::When,
|
||||
b"trigger" => CalendarProperty::Trigger,
|
||||
b"offset" => CalendarProperty::Offset,
|
||||
b"relativeTo" => CalendarProperty::RelativeTo,
|
||||
b"action" => CalendarProperty::Action,
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
CalendarProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &CalendarProperty {
|
||||
if let CalendarProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarSetArguments {
|
||||
pub on_destroy_remove_events: Option<bool>,
|
||||
pub on_success_set_is_default: Option<MaybeIdReference<Id>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onDestroyRemoveEvents" => {
|
||||
self.on_destroy_remove_events = map.next_value()?;
|
||||
},
|
||||
b"onSuccessSetIsDefault" => {
|
||||
self.on_success_set_is_default = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CalendarProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
CalendarProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Calendar {
|
||||
type Property = CalendarProperty;
|
||||
|
||||
type Element = CalendarValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = CalendarSetArguments;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = CalendarProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapSharedObject for Calendar {
|
||||
type Right = CalendarRight;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property = CalendarProperty::ShareWith;
|
||||
}
|
||||
|
||||
impl From<Id> for CalendarProperty {
|
||||
fn from(id: Id) -> Self {
|
||||
CalendarProperty::IdValue(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CalendarProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: CalendarProperty) -> Result<Self, Self::Error> {
|
||||
if let CalendarProperty::IdValue(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CalendarProperty> for CalendarRight {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: CalendarProperty) -> Result<Self, Self::Error> {
|
||||
if let CalendarProperty::Rights(right) = value {
|
||||
Ok(right)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for CalendarValue {
|
||||
fn from(id: Id) -> Self {
|
||||
CalendarValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for CalendarValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let CalendarValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let CalendarValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let CalendarValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(new_id) = new_id {
|
||||
*self = CalendarValue::Id(new_id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapRight for CalendarRight {
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
match self {
|
||||
CalendarRight::MayReadFreeBusy => &[Acl::SchedulingReadFreeBusy],
|
||||
CalendarRight::MayReadItems => &[Acl::Read, Acl::ReadItems],
|
||||
CalendarRight::MayWriteAll => &[
|
||||
Acl::Modify,
|
||||
Acl::AddItems,
|
||||
Acl::ModifyItems,
|
||||
Acl::RemoveItems,
|
||||
],
|
||||
CalendarRight::MayWriteOwn => &[Acl::ModifyItemsOwn],
|
||||
CalendarRight::MayUpdatePrivate => &[Acl::ModifyPrivateProperties],
|
||||
CalendarRight::MayRSVP => &[Acl::ModifyRSVP],
|
||||
CalendarRight::MayShare => &[Acl::Share],
|
||||
CalendarRight::MayDelete => &[Acl::Delete, Acl::RemoveItems],
|
||||
}
|
||||
}
|
||||
|
||||
fn all_rights() -> &'static [Self] {
|
||||
&[
|
||||
CalendarRight::MayReadFreeBusy,
|
||||
CalendarRight::MayReadItems,
|
||||
CalendarRight::MayWriteAll,
|
||||
CalendarRight::MayWriteOwn,
|
||||
CalendarRight::MayUpdatePrivate,
|
||||
CalendarRight::MayRSVP,
|
||||
CalendarRight::MayShare,
|
||||
CalendarRight::MayDelete,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarRight> for CalendarProperty {
|
||||
fn from(right: CalendarRight) -> Self {
|
||||
CalendarProperty::Rights(right)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for CalendarProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let CalendarProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let CalendarProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(new_id) = new_id {
|
||||
*self = CalendarProperty::IdValue(new_id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CalendarProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_cow())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
};
|
||||
use calcard::{
|
||||
common::timezone::Tz,
|
||||
jscalendar::{JSCalendarDateTime, JSCalendarProperty, JSCalendarValue},
|
||||
};
|
||||
use jmap_tools::{JsonPointerItem, Key};
|
||||
use mail_parser::DateTime;
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarEvent;
|
||||
|
||||
impl JmapObject for CalendarEvent {
|
||||
type Property = JSCalendarProperty<Id>;
|
||||
|
||||
type Element = JSCalendarValue<Id, BlobId>;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = CalendarEventFilter;
|
||||
|
||||
type Comparator = CalendarEventComparator;
|
||||
|
||||
type GetArguments = CalendarEventGetArguments;
|
||||
|
||||
type SetArguments<'de> = CalendarEventSetArguments;
|
||||
|
||||
type QueryArguments = CalendarEventQueryArguments;
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = JSCalendarProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapObjectId for JSCalendarValue<Id, BlobId> {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let JSCalendarValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
JSCalendarValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
JSCalendarValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
JSCalendarValue::IdReference(r) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = JSCalendarValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CalendarEventFilter {
|
||||
InCalendar(MaybeInvalid<Id>),
|
||||
After(JSCalendarDateTime),
|
||||
Before(JSCalendarDateTime),
|
||||
Text(String),
|
||||
Title(String),
|
||||
Description(String),
|
||||
Location(String),
|
||||
Owner(String),
|
||||
Attendee(String),
|
||||
Uid(String),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CalendarEventComparator {
|
||||
Start,
|
||||
Uid,
|
||||
RecurrenceId,
|
||||
Created,
|
||||
Updated,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarEventGetArguments {
|
||||
pub recurrence_overrides_before: Option<JSCalendarDateTime>,
|
||||
pub recurrence_overrides_after: Option<JSCalendarDateTime>,
|
||||
pub reduce_participants: Option<bool>,
|
||||
pub time_zone: Option<Tz>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarEventSetArguments {
|
||||
pub send_scheduling_messages: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarEventQueryArguments {
|
||||
pub expand_recurrences: Option<bool>,
|
||||
pub time_zone: Option<Tz>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"inCalendar" => {
|
||||
*self = CalendarEventFilter::InCalendar(map.next_value()?);
|
||||
},
|
||||
b"after" => {
|
||||
*self = CalendarEventFilter::After(map.next_value::<LocalTime>()?.0);
|
||||
},
|
||||
b"before" => {
|
||||
*self = CalendarEventFilter::Before(map.next_value::<LocalTime>()?.0);
|
||||
},
|
||||
b"text" => {
|
||||
*self = CalendarEventFilter::Text(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"title" => {
|
||||
*self = CalendarEventFilter::Title(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"description" => {
|
||||
*self = CalendarEventFilter::Description(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"location" => {
|
||||
*self = CalendarEventFilter::Location(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"owner" => {
|
||||
*self = CalendarEventFilter::Owner(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"attendee" => {
|
||||
*self = CalendarEventFilter::Attendee(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"uid" => {
|
||||
*self = CalendarEventFilter::Uid(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = CalendarEventFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"start" => {
|
||||
*self = CalendarEventComparator::Start;
|
||||
},
|
||||
b"uid" => {
|
||||
*self = CalendarEventComparator::Uid;
|
||||
},
|
||||
b"recurrenceId" => {
|
||||
*self = CalendarEventComparator::RecurrenceId;
|
||||
},
|
||||
b"created" => {
|
||||
*self = CalendarEventComparator::Created;
|
||||
},
|
||||
b"updated" => {
|
||||
*self = CalendarEventComparator::Updated;
|
||||
},
|
||||
_ => {
|
||||
*self = CalendarEventComparator::_T(value.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventGetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"recurrenceOverridesBefore" => {
|
||||
self.recurrence_overrides_before = map.next_value::<Option<LocalTime>>()?.map(|lt| lt.0)
|
||||
},
|
||||
b"recurrenceOverridesAfter" => {
|
||||
self.recurrence_overrides_after = map.next_value::<Option<LocalTime>>()?.map(|lt| lt.0);
|
||||
},
|
||||
b"reduceParticipants" => {
|
||||
self.reduce_participants = map.next_value()?;
|
||||
},
|
||||
b"timeZone" => {
|
||||
self.time_zone = map.next_value::<Option<&str>>()?.and_then(|s| Tz::from_str(s).ok());
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"sendSchedulingMessages" => {
|
||||
self.send_scheduling_messages = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventQueryArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"expandRecurrences" => {
|
||||
self.expand_recurrences = map.next_value()?;
|
||||
},
|
||||
b"timeZone" => {
|
||||
self.time_zone = map.next_value::<Option<&str>>()?.and_then(|s| Tz::from_str(s).ok());
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventFilter::InCalendar(_) => "inCalendar",
|
||||
CalendarEventFilter::After(_) => "after",
|
||||
CalendarEventFilter::Before(_) => "before",
|
||||
CalendarEventFilter::Text(_) => "text",
|
||||
CalendarEventFilter::Title(_) => "title",
|
||||
CalendarEventFilter::Description(_) => "description",
|
||||
CalendarEventFilter::Location(_) => "location",
|
||||
CalendarEventFilter::Owner(_) => "owner",
|
||||
CalendarEventFilter::Attendee(_) => "attendee",
|
||||
CalendarEventFilter::Uid(_) => "uid",
|
||||
CalendarEventFilter::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventComparator {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventComparator::Start => "start",
|
||||
CalendarEventComparator::Uid => "uid",
|
||||
CalendarEventComparator::RecurrenceId => "recurrenceId",
|
||||
CalendarEventComparator::Created => "created",
|
||||
CalendarEventComparator::Updated => "updated",
|
||||
CalendarEventComparator::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventFilter {
|
||||
fn default() -> Self {
|
||||
CalendarEventFilter::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventComparator {
|
||||
fn default() -> Self {
|
||||
CalendarEventComparator::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalTime(JSCalendarDateTime);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for LocalTime {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <&str>::deserialize(deserializer)?;
|
||||
|
||||
if let Some(dt) = DateTime::parse_rfc3339(value) {
|
||||
Ok(LocalTime(JSCalendarDateTime {
|
||||
timestamp: dt.to_timestamp_local(),
|
||||
is_local: true,
|
||||
}))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Invalid datetime: {}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for JSCalendarProperty<Id> {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let JSCalendarProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let JSCalendarProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
JSCalendarProperty::IdReference(r) => Some(r),
|
||||
JSCalendarProperty::Pointer(value) => {
|
||||
let value = value.as_slice();
|
||||
match (value.first(), value.get(1)) {
|
||||
(
|
||||
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::CalendarIds))),
|
||||
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdReference(
|
||||
r,
|
||||
)))),
|
||||
) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
if let JSCalendarProperty::Pointer(value) = self {
|
||||
let value = value.as_mut_slice();
|
||||
if let Some(value) = value.get_mut(1) {
|
||||
*value = JsonPointerItem::Key(Key::Property(JSCalendarProperty::IdValue(id)));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
*self = JSCalendarProperty::IdValue(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
types::{date::UTCDate, state::State},
|
||||
};
|
||||
use calcard::jscalendar::JSCalendar;
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CalendarEventNotification;
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CalendarEventNotificationObject {
|
||||
pub id: Id,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created: Option<UTCDate>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub changed_by: Option<PersonObject>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "type")]
|
||||
pub notification_type: Option<CalendarEventNotificationType>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub calendar_event_id: Option<Id>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_draft: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event: Option<JSCalendar<'static, Id, BlobId>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event_patch: Option<JSCalendar<'static, Id, BlobId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonObject {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub principal_id: Option<Id>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub calendar_address: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct CalendarEventNotificationGetResponse {
|
||||
#[serde(rename = "accountId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub account_id: Option<Id>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<State>,
|
||||
|
||||
pub list: Vec<CalendarEventNotificationObject>,
|
||||
|
||||
#[serde(rename = "notFound")]
|
||||
pub not_found: Vec<crate::request::MaybeInvalid<Id>>,
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationGetResponse {
|
||||
pub fn push_not_found(&mut self, id: Id) {
|
||||
self.not_found.push(crate::request::MaybeInvalid::Value(id));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarEventNotificationProperty {
|
||||
Id,
|
||||
Created,
|
||||
ChangedBy,
|
||||
Comment,
|
||||
Type,
|
||||
CalendarEventId,
|
||||
IsDraft,
|
||||
Event,
|
||||
EventPatch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarEventNotificationValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
Type(CalendarEventNotificationType),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CalendarEventNotificationType {
|
||||
Created,
|
||||
Updated,
|
||||
Destroyed,
|
||||
}
|
||||
|
||||
impl Property for CalendarEventNotificationProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
CalendarEventNotificationProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventNotificationProperty::Id => "id",
|
||||
CalendarEventNotificationProperty::Created => "created",
|
||||
CalendarEventNotificationProperty::ChangedBy => "changedBy",
|
||||
CalendarEventNotificationProperty::Comment => "comment",
|
||||
CalendarEventNotificationProperty::Type => "type",
|
||||
CalendarEventNotificationProperty::CalendarEventId => "calendarEventId",
|
||||
CalendarEventNotificationProperty::IsDraft => "isDraft",
|
||||
CalendarEventNotificationProperty::Event => "event",
|
||||
CalendarEventNotificationProperty::EventPatch => "eventPatch",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for CalendarEventNotificationValue {
|
||||
type Property = CalendarEventNotificationProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
CalendarEventNotificationProperty::Id
|
||||
| CalendarEventNotificationProperty::CalendarEventId => Id::from_str(value)
|
||||
.ok()
|
||||
.map(CalendarEventNotificationValue::Id),
|
||||
CalendarEventNotificationProperty::Created => UTCDate::from_str(value)
|
||||
.ok()
|
||||
.map(CalendarEventNotificationValue::Date),
|
||||
CalendarEventNotificationProperty::Type => {
|
||||
CalendarEventNotificationType::parse(value)
|
||||
.map(CalendarEventNotificationValue::Type)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventNotificationValue::Id(id) => id.to_string().into(),
|
||||
CalendarEventNotificationValue::Date(date) => date.to_string().into(),
|
||||
CalendarEventNotificationValue::Type(t) => t.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationType {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"created" => CalendarEventNotificationType::Created,
|
||||
b"updated" => CalendarEventNotificationType::Updated,
|
||||
b"destroyed" => CalendarEventNotificationType::Destroyed,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarEventNotificationType::Created => "created",
|
||||
CalendarEventNotificationType::Updated => "updated",
|
||||
CalendarEventNotificationType::Destroyed => "destroyed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => CalendarEventNotificationProperty::Id,
|
||||
b"created" => CalendarEventNotificationProperty::Created,
|
||||
b"changedBy" => CalendarEventNotificationProperty::ChangedBy,
|
||||
b"comment" => CalendarEventNotificationProperty::Comment,
|
||||
b"type" => CalendarEventNotificationProperty::Type,
|
||||
b"calendarEventId" => CalendarEventNotificationProperty::CalendarEventId,
|
||||
b"isDraft" => CalendarEventNotificationProperty::IsDraft,
|
||||
b"event" => CalendarEventNotificationProperty::Event,
|
||||
b"eventPatch" => CalendarEventNotificationProperty::EventPatch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CalendarEventNotificationProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
CalendarEventNotificationProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for CalendarEventNotification {
|
||||
type Property = CalendarEventNotificationProperty;
|
||||
|
||||
type Element = CalendarEventNotificationValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = CalendarEventNotificationFilter;
|
||||
|
||||
type Comparator = CalendarEventNotificationComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = CalendarEventNotificationProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CalendarEventNotificationFilter {
|
||||
After(UTCDate),
|
||||
Before(UTCDate),
|
||||
Type(CalendarEventNotificationType),
|
||||
CalendarEventIds(Vec<MaybeInvalid<Id>>),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CalendarEventNotificationComparator {
|
||||
Created,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventNotificationFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"after" => {
|
||||
*self = CalendarEventNotificationFilter::After(map.next_value()?);
|
||||
},
|
||||
b"before" => {
|
||||
*self = CalendarEventNotificationFilter::Before(map.next_value()?);
|
||||
},
|
||||
b"type" => {
|
||||
*self = CalendarEventNotificationFilter::Type(map.next_value()?);
|
||||
},
|
||||
b"calendarEventIds" => {
|
||||
*self = CalendarEventNotificationFilter::CalendarEventIds(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = CalendarEventNotificationFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for CalendarEventNotificationComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"created" => {
|
||||
*self = CalendarEventNotificationComparator::Created;
|
||||
},
|
||||
_ => {
|
||||
*self = CalendarEventNotificationComparator::_T(value.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for CalendarEventNotificationType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
CalendarEventNotificationType::parse(<&str>::deserialize(deserializer)?)
|
||||
.ok_or_else(|| serde::de::Error::custom("invalid CalendarEventNotificationType"))
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventNotificationFilter::After(_) => "after",
|
||||
CalendarEventNotificationFilter::Before(_) => "before",
|
||||
CalendarEventNotificationFilter::Type(_) => "type",
|
||||
CalendarEventNotificationFilter::CalendarEventIds(_) => "calendarEventIds",
|
||||
CalendarEventNotificationFilter::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarEventNotificationComparator {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
CalendarEventNotificationComparator::Created => "created",
|
||||
CalendarEventNotificationComparator::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventNotificationFilter {
|
||||
fn default() -> Self {
|
||||
CalendarEventNotificationFilter::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarEventNotificationComparator {
|
||||
fn default() -> Self {
|
||||
CalendarEventNotificationComparator::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CalendarEventNotificationProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_: CalendarEventNotificationProperty) -> Result<Self, Self::Error> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for CalendarEventNotificationValue {
|
||||
fn from(id: Id) -> Self {
|
||||
CalendarEventNotificationValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for CalendarEventNotificationValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let CalendarEventNotificationValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let CalendarEventNotificationValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for CalendarEventNotificationProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for CalendarEventNotificationType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CalendarEventNotificationProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_cow())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use calcard::jscontact::{JSContactProperty, JSContactValue};
|
||||
use jmap_tools::{JsonPointerItem, Key};
|
||||
use std::borrow::Cow;
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContactCard;
|
||||
|
||||
impl JmapObject for ContactCard {
|
||||
type Property = JSContactProperty<Id>;
|
||||
|
||||
type Element = JSContactValue<Id, BlobId>;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ContactCardFilter;
|
||||
|
||||
type Comparator = ContactCardComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = JSContactProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapObjectId for JSContactValue<Id, BlobId> {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let JSContactValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
JSContactValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
JSContactValue::BlobId(id) => Some(AnyId::BlobId(id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
JSContactValue::IdReference(r) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = JSContactValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(id) => {
|
||||
*self = JSContactValue::BlobId(id);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ContactCardFilter {
|
||||
InAddressBook(MaybeInvalid<Id>),
|
||||
Uid(String),
|
||||
HasMember(String),
|
||||
Kind(String),
|
||||
CreatedBefore(UTCDate),
|
||||
CreatedAfter(UTCDate),
|
||||
UpdatedBefore(UTCDate),
|
||||
UpdatedAfter(UTCDate),
|
||||
Text(String),
|
||||
Name(String),
|
||||
NameGiven(String),
|
||||
NameSurname(String),
|
||||
NameSurname2(String),
|
||||
Nickname(String),
|
||||
Organization(String),
|
||||
Email(String),
|
||||
Phone(String),
|
||||
OnlineService(String),
|
||||
Address(String),
|
||||
Note(String),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ContactCardComparator {
|
||||
Created,
|
||||
Updated,
|
||||
NameGiven,
|
||||
NameSurname,
|
||||
NameSurname2,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ContactCardFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"inAddressBook" => {
|
||||
*self = ContactCardFilter::InAddressBook(map.next_value()?);
|
||||
},
|
||||
b"uid" => {
|
||||
*self = ContactCardFilter::Uid(map.next_value()?);
|
||||
},
|
||||
b"hasMember" => {
|
||||
*self = ContactCardFilter::HasMember(map.next_value()?);
|
||||
},
|
||||
b"kind" => {
|
||||
*self = ContactCardFilter::Kind(map.next_value()?);
|
||||
},
|
||||
b"createdBefore" => {
|
||||
*self = ContactCardFilter::CreatedBefore(map.next_value()?);
|
||||
},
|
||||
b"createdAfter" => {
|
||||
*self = ContactCardFilter::CreatedAfter(map.next_value()?);
|
||||
},
|
||||
b"updatedBefore" => {
|
||||
*self = ContactCardFilter::UpdatedBefore(map.next_value()?);
|
||||
},
|
||||
b"updatedAfter" => {
|
||||
*self = ContactCardFilter::UpdatedAfter(map.next_value()?);
|
||||
},
|
||||
b"text" => {
|
||||
*self = ContactCardFilter::Text(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"name" => {
|
||||
*self = ContactCardFilter::Name(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"name/given" => {
|
||||
*self = ContactCardFilter::NameGiven(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"name/surname" => {
|
||||
*self = ContactCardFilter::NameSurname(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"name/surname2" => {
|
||||
*self = ContactCardFilter::NameSurname2(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"nickname" => {
|
||||
*self = ContactCardFilter::Nickname(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"organization" => {
|
||||
*self = ContactCardFilter::Organization(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"email" => {
|
||||
*self = ContactCardFilter::Email(map.next_value()?);
|
||||
},
|
||||
b"phone" => {
|
||||
*self = ContactCardFilter::Phone(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"onlineService" => {
|
||||
*self = ContactCardFilter::OnlineService(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"address" => {
|
||||
*self = ContactCardFilter::Address(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
b"note" => {
|
||||
*self = ContactCardFilter::Note(map.next_value::<Cow<str>>()?.to_lowercase());
|
||||
},
|
||||
_ => {
|
||||
*self = ContactCardFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ContactCardComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"created" => {
|
||||
*self = ContactCardComparator::Created;
|
||||
},
|
||||
b"updated" => {
|
||||
*self = ContactCardComparator::Updated;
|
||||
},
|
||||
b"name/given" => {
|
||||
*self = ContactCardComparator::NameGiven;
|
||||
},
|
||||
b"name/surname" => {
|
||||
*self = ContactCardComparator::NameSurname;
|
||||
},
|
||||
b"name/surname2" => {
|
||||
*self = ContactCardComparator::NameSurname2;
|
||||
},
|
||||
_ => {
|
||||
*self = ContactCardComparator::_T(value.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactCardFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ContactCardFilter::InAddressBook(_) => "inAddressBook",
|
||||
ContactCardFilter::Uid(_) => "uid",
|
||||
ContactCardFilter::HasMember(_) => "hasMember",
|
||||
ContactCardFilter::Kind(_) => "kind",
|
||||
ContactCardFilter::CreatedBefore(_) => "createdBefore",
|
||||
ContactCardFilter::CreatedAfter(_) => "createdAfter",
|
||||
ContactCardFilter::UpdatedBefore(_) => "updatedBefore",
|
||||
ContactCardFilter::UpdatedAfter(_) => "updatedAfter",
|
||||
ContactCardFilter::Text(_) => "text",
|
||||
ContactCardFilter::Name(_) => "name",
|
||||
ContactCardFilter::NameGiven(_) => "name/given",
|
||||
ContactCardFilter::NameSurname(_) => "name/surname",
|
||||
ContactCardFilter::NameSurname2(_) => "name/surname2",
|
||||
ContactCardFilter::Nickname(_) => "nickname",
|
||||
ContactCardFilter::Organization(_) => "organization",
|
||||
ContactCardFilter::Email(_) => "email",
|
||||
ContactCardFilter::Phone(_) => "phone",
|
||||
ContactCardFilter::OnlineService(_) => "onlineService",
|
||||
ContactCardFilter::Address(_) => "address",
|
||||
ContactCardFilter::Note(_) => "note",
|
||||
ContactCardFilter::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactCardComparator {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ContactCardComparator::Created => "created",
|
||||
ContactCardComparator::Updated => "updated",
|
||||
ContactCardComparator::NameGiven => "name/given",
|
||||
ContactCardComparator::NameSurname => "name/surname",
|
||||
ContactCardComparator::NameSurname2 => "name/surname2",
|
||||
ContactCardComparator::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContactCardFilter {
|
||||
fn default() -> Self {
|
||||
ContactCardFilter::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContactCardComparator {
|
||||
fn default() -> Self {
|
||||
ContactCardComparator::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for JSContactProperty<Id> {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let JSContactProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let JSContactProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
JSContactProperty::IdReference(r) => Some(r),
|
||||
JSContactProperty::Pointer(value) => {
|
||||
let value = value.as_slice();
|
||||
match (value.first(), value.get(1)) {
|
||||
(
|
||||
Some(JsonPointerItem::Key(Key::Property(
|
||||
JSContactProperty::AddressBookIds,
|
||||
))),
|
||||
Some(JsonPointerItem::Key(Key::Property(JSContactProperty::IdReference(
|
||||
r,
|
||||
)))),
|
||||
) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
if let JSContactProperty::Pointer(value) = self {
|
||||
let value = value.as_mut_slice();
|
||||
if let Some(value) = value.get_mut(1) {
|
||||
*value = JsonPointerItem::Key(Key::Property(JSContactProperty::IdValue(id)));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
*self = JSContactProperty::IdValue(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
method::query::{Comparator, Filter},
|
||||
object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use mail_parser::HeaderName;
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id, keyword::Keyword};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Email;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EmailProperty {
|
||||
// Metadata
|
||||
Id,
|
||||
BlobId,
|
||||
ThreadId,
|
||||
MailboxIds,
|
||||
Keywords,
|
||||
Size,
|
||||
ReceivedAt,
|
||||
|
||||
// Address
|
||||
Name,
|
||||
Email,
|
||||
|
||||
// GroupedAddresses
|
||||
Addresses,
|
||||
|
||||
// Header Fields Properties
|
||||
Value,
|
||||
Header(HeaderProperty),
|
||||
|
||||
// Convenience properties
|
||||
MessageId,
|
||||
InReplyTo,
|
||||
References,
|
||||
Sender,
|
||||
From,
|
||||
To,
|
||||
Cc,
|
||||
Bcc,
|
||||
ReplyTo,
|
||||
Subject,
|
||||
SentAt,
|
||||
|
||||
// Body Parts
|
||||
TextBody,
|
||||
HtmlBody,
|
||||
Attachments,
|
||||
PartId,
|
||||
Headers,
|
||||
Type,
|
||||
Charset,
|
||||
Disposition,
|
||||
Cid,
|
||||
Language,
|
||||
Location,
|
||||
SubParts,
|
||||
BodyStructure,
|
||||
BodyValues,
|
||||
IsEncodingProblem,
|
||||
IsTruncated,
|
||||
HasAttachment,
|
||||
Preview,
|
||||
|
||||
// Other
|
||||
Keyword(Keyword),
|
||||
IdValue(Id),
|
||||
IdReference(String),
|
||||
Pointer(JsonPointer<EmailProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct HeaderProperty {
|
||||
pub form: HeaderForm,
|
||||
pub header: String,
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum HeaderForm {
|
||||
Raw,
|
||||
Text,
|
||||
Addresses,
|
||||
GroupedAddresses,
|
||||
MessageIds,
|
||||
Date,
|
||||
URLs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EmailValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
impl Property for EmailProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
EmailProperty::Keywords => EmailProperty::Keyword(Keyword::parse(value)).into(),
|
||||
EmailProperty::MailboxIds => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(EmailProperty::IdValue(v)),
|
||||
MaybeReference::Reference(v) => Some(EmailProperty::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
_ => EmailProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
EmailProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
EmailProperty::Attachments => "attachments",
|
||||
EmailProperty::Bcc => "bcc",
|
||||
EmailProperty::BlobId => "blobId",
|
||||
EmailProperty::BodyStructure => "bodyStructure",
|
||||
EmailProperty::BodyValues => "bodyValues",
|
||||
EmailProperty::Cc => "cc",
|
||||
EmailProperty::Charset => "charset",
|
||||
EmailProperty::Cid => "cid",
|
||||
EmailProperty::Disposition => "disposition",
|
||||
EmailProperty::Email => "email",
|
||||
EmailProperty::From => "from",
|
||||
EmailProperty::HasAttachment => "hasAttachment",
|
||||
EmailProperty::Headers => "headers",
|
||||
EmailProperty::HtmlBody => "htmlBody",
|
||||
EmailProperty::Id => "id",
|
||||
EmailProperty::InReplyTo => "inReplyTo",
|
||||
EmailProperty::Keywords => "keywords",
|
||||
EmailProperty::Language => "language",
|
||||
EmailProperty::Location => "location",
|
||||
EmailProperty::MailboxIds => "mailboxIds",
|
||||
EmailProperty::MessageId => "messageId",
|
||||
EmailProperty::Name => "name",
|
||||
EmailProperty::PartId => "partId",
|
||||
EmailProperty::Preview => "preview",
|
||||
EmailProperty::ReceivedAt => "receivedAt",
|
||||
EmailProperty::References => "references",
|
||||
EmailProperty::ReplyTo => "replyTo",
|
||||
EmailProperty::Sender => "sender",
|
||||
EmailProperty::SentAt => "sentAt",
|
||||
EmailProperty::Size => "size",
|
||||
EmailProperty::Subject => "subject",
|
||||
EmailProperty::SubParts => "subParts",
|
||||
EmailProperty::TextBody => "textBody",
|
||||
EmailProperty::ThreadId => "threadId",
|
||||
EmailProperty::To => "to",
|
||||
EmailProperty::Type => "type",
|
||||
EmailProperty::Addresses => "addresses",
|
||||
EmailProperty::Value => "value",
|
||||
EmailProperty::IsEncodingProblem => "isEncodingProblem",
|
||||
EmailProperty::IsTruncated => "isTruncated",
|
||||
EmailProperty::Header(header) => return header.to_string().into(),
|
||||
EmailProperty::Keyword(keyword) => return keyword.to_string().into(),
|
||||
EmailProperty::IdValue(id) => return id.to_string().into(),
|
||||
EmailProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
EmailProperty::IdReference(r) => return format!("#{r}").into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for EmailValue {
|
||||
type Property = EmailProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
EmailProperty::Id | EmailProperty::ThreadId | EmailProperty::MailboxIds => {
|
||||
match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(EmailValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(EmailValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
}
|
||||
}
|
||||
EmailProperty::BlobId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(EmailValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(EmailValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
EmailProperty::Header(HeaderProperty {
|
||||
form: HeaderForm::Date,
|
||||
..
|
||||
})
|
||||
| EmailProperty::ReceivedAt
|
||||
| EmailProperty::SentAt => UTCDate::from_str(value).ok().map(EmailValue::Date),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
EmailValue::Id(id) => id.to_string().into(),
|
||||
EmailValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
EmailValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
EmailValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
"id" => EmailProperty::Id,
|
||||
"blobId" => EmailProperty::BlobId,
|
||||
"threadId" => EmailProperty::ThreadId,
|
||||
"mailboxIds" => EmailProperty::MailboxIds,
|
||||
"keywords" => EmailProperty::Keywords,
|
||||
"size" => EmailProperty::Size,
|
||||
"receivedAt" => EmailProperty::ReceivedAt,
|
||||
"name" => EmailProperty::Name,
|
||||
"email" => EmailProperty::Email,
|
||||
"addresses" => EmailProperty::Addresses,
|
||||
"value" => EmailProperty::Value,
|
||||
"messageId" => EmailProperty::MessageId,
|
||||
"inReplyTo" => EmailProperty::InReplyTo,
|
||||
"references" => EmailProperty::References,
|
||||
"sender" => EmailProperty::Sender,
|
||||
"from" => EmailProperty::From,
|
||||
"to" => EmailProperty::To,
|
||||
"cc" => EmailProperty::Cc,
|
||||
"bcc" => EmailProperty::Bcc,
|
||||
"replyTo" => EmailProperty::ReplyTo,
|
||||
"subject" => EmailProperty::Subject,
|
||||
"sentAt" => EmailProperty::SentAt,
|
||||
"textBody" => EmailProperty::TextBody,
|
||||
"htmlBody" => EmailProperty::HtmlBody,
|
||||
"attachments" => EmailProperty::Attachments,
|
||||
"partId" => EmailProperty::PartId,
|
||||
"headers" => EmailProperty::Headers,
|
||||
"type" => EmailProperty::Type,
|
||||
"charset" => EmailProperty::Charset,
|
||||
"disposition" => EmailProperty::Disposition,
|
||||
"cid" => EmailProperty::Cid,
|
||||
"language" => EmailProperty::Language,
|
||||
"location" => EmailProperty::Location,
|
||||
"subParts" => EmailProperty::SubParts,
|
||||
"bodyStructure" => EmailProperty::BodyStructure,
|
||||
"bodyValues" => EmailProperty::BodyValues,
|
||||
"isEncodingProblem" => EmailProperty::IsEncodingProblem,
|
||||
"isTruncated" => EmailProperty::IsTruncated,
|
||||
"hasAttachment" => EmailProperty::HasAttachment,
|
||||
"preview" => EmailProperty::Preview
|
||||
)
|
||||
.or_else(|| {
|
||||
if let Some(header) = value.strip_prefix("header:") {
|
||||
HeaderProperty::parse(header).map(EmailProperty::Header)
|
||||
} else if allow_patch && value.contains('/') {
|
||||
EmailProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &EmailProperty {
|
||||
if let EmailProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_rfc_header(&self) -> HeaderName<'static> {
|
||||
match self {
|
||||
EmailProperty::MessageId => HeaderName::MessageId,
|
||||
EmailProperty::InReplyTo => HeaderName::InReplyTo,
|
||||
EmailProperty::References => HeaderName::References,
|
||||
EmailProperty::Sender => HeaderName::Sender,
|
||||
EmailProperty::From => HeaderName::From,
|
||||
EmailProperty::To => HeaderName::To,
|
||||
EmailProperty::Cc => HeaderName::Cc,
|
||||
EmailProperty::Bcc => HeaderName::Bcc,
|
||||
EmailProperty::ReplyTo => HeaderName::ReplyTo,
|
||||
EmailProperty::Subject => HeaderName::Subject,
|
||||
EmailProperty::SentAt => HeaderName::Date,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_into_id(self) -> Option<Id> {
|
||||
match self {
|
||||
EmailProperty::IdValue(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_into_keyword(self) -> Option<Keyword> {
|
||||
match self {
|
||||
EmailProperty::Keyword(keyword) => Some(keyword),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
let mut result = HeaderProperty {
|
||||
form: HeaderForm::Raw,
|
||||
header: String::new(),
|
||||
all: false,
|
||||
};
|
||||
|
||||
for (pos, value) in value.split(':').enumerate() {
|
||||
match pos {
|
||||
0 => {
|
||||
result.header = value.to_string();
|
||||
}
|
||||
1 => {
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"asText" => { result.form = HeaderForm::Text;},
|
||||
b"asAddresses" => { result.form = HeaderForm::Addresses;},
|
||||
b"asGroupedAddresses" => { result.form = HeaderForm::GroupedAddresses;},
|
||||
b"asMessageIds" => { result.form = HeaderForm::MessageIds;},
|
||||
b"asDate" => { result.form = HeaderForm::Date;},
|
||||
b"asURLs" => { result.form = HeaderForm::URLs;},
|
||||
b"asRaw" => { result.form = HeaderForm::Raw; },
|
||||
b"all" => { result.all = true; },
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
);
|
||||
}
|
||||
2 if value == "all" && !result.all => {
|
||||
result.all = true;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
if !result.header.is_empty() {
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HeaderProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "header:{}", self.header)?;
|
||||
self.form.fmt(f)?;
|
||||
if self.all { write!(f, ":all") } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HeaderForm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
HeaderForm::Raw => Ok(()),
|
||||
HeaderForm::Text => write!(f, ":asText"),
|
||||
HeaderForm::Addresses => write!(f, ":asAddresses"),
|
||||
HeaderForm::GroupedAddresses => write!(f, ":asGroupedAddresses"),
|
||||
HeaderForm::MessageIds => write!(f, ":asMessageIds"),
|
||||
HeaderForm::Date => write!(f, ":asDate"),
|
||||
HeaderForm::URLs => write!(f, ":asURLs"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EmailGetArguments {
|
||||
pub body_properties: Option<Vec<MaybeInvalid<EmailProperty>>>,
|
||||
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 EmailQueryArguments {
|
||||
pub collapse_threads: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EmailParseArguments {
|
||||
pub body_properties: Option<Vec<MaybeInvalid<EmailProperty>>>,
|
||||
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>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailGetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"bodyProperties" => {
|
||||
self.body_properties = map.next_value()?;
|
||||
},
|
||||
b"fetchTextBodyValues" => {
|
||||
self.fetch_text_body_values = map.next_value()?;
|
||||
},
|
||||
b"fetchHTMLBodyValues" => {
|
||||
self.fetch_html_body_values = map.next_value()?;
|
||||
},
|
||||
b"fetchAllBodyValues" => {
|
||||
self.fetch_all_body_values = map.next_value()?;
|
||||
},
|
||||
b"maxBodyValueBytes" => {
|
||||
self.max_body_value_bytes = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailQueryArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "collapseThreads" {
|
||||
self.collapse_threads = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailParseArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"bodyProperties" => {
|
||||
self.body_properties = map.next_value()?;
|
||||
},
|
||||
b"fetchTextBodyValues" => {
|
||||
self.fetch_text_body_values = map.next_value()?;
|
||||
},
|
||||
b"fetchHTMLBodyValues" => {
|
||||
self.fetch_html_body_values = map.next_value()?;
|
||||
},
|
||||
b"fetchAllBodyValues" => {
|
||||
self.fetch_all_body_values = map.next_value()?;
|
||||
},
|
||||
b"maxBodyValueBytes" => {
|
||||
self.max_body_value_bytes = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Email {
|
||||
type Property = EmailProperty;
|
||||
|
||||
type Element = EmailValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = EmailFilter;
|
||||
|
||||
type Comparator = EmailComparator;
|
||||
|
||||
type GetArguments = EmailGetArguments;
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = EmailQueryArguments;
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = EmailParseArguments;
|
||||
|
||||
const ID_PROPERTY: Self::Property = EmailProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EmailFilter {
|
||||
InMailbox(Id),
|
||||
InMailboxOtherThan(Vec<Id>),
|
||||
Before(UTCDate),
|
||||
After(UTCDate),
|
||||
MinSize(u32),
|
||||
MaxSize(u32),
|
||||
AllInThreadHaveKeyword(Keyword),
|
||||
SomeInThreadHaveKeyword(Keyword),
|
||||
NoneInThreadHaveKeyword(Keyword),
|
||||
HasKeyword(Keyword),
|
||||
NotKeyword(Keyword),
|
||||
HasAttachment(bool),
|
||||
From(String),
|
||||
To(String),
|
||||
Cc(String),
|
||||
Bcc(String),
|
||||
Subject(String),
|
||||
Body(String),
|
||||
Header(Vec<String>),
|
||||
Text(String),
|
||||
SentBefore(UTCDate),
|
||||
SentAfter(UTCDate),
|
||||
InThread(Id),
|
||||
Id(Vec<Id>),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EmailComparator {
|
||||
ReceivedAt,
|
||||
Size,
|
||||
From,
|
||||
To,
|
||||
Subject,
|
||||
Cc,
|
||||
SentAt,
|
||||
ThreadId,
|
||||
HasKeyword(Keyword),
|
||||
AllInThreadHaveKeyword(Keyword),
|
||||
SomeInThreadHaveKeyword(Keyword),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"inMailbox" => {
|
||||
*self = EmailFilter::InMailbox(map.next_value()?);
|
||||
},
|
||||
b"inMailboxOtherThan" => {
|
||||
*self = EmailFilter::InMailboxOtherThan(map.next_value()?);
|
||||
},
|
||||
b"before" => {
|
||||
*self = EmailFilter::Before(map.next_value()?);
|
||||
},
|
||||
b"after" => {
|
||||
*self = EmailFilter::After(map.next_value()?);
|
||||
},
|
||||
b"minSize" => {
|
||||
*self = EmailFilter::MinSize(map.next_value()?);
|
||||
},
|
||||
b"maxSize" => {
|
||||
*self = EmailFilter::MaxSize(map.next_value()?);
|
||||
},
|
||||
b"allInThreadHaveKeyword" => {
|
||||
*self = EmailFilter::AllInThreadHaveKeyword(map.next_value()?);
|
||||
},
|
||||
b"someInThreadHaveKeyword" => {
|
||||
*self = EmailFilter::SomeInThreadHaveKeyword(map.next_value()?);
|
||||
},
|
||||
b"noneInThreadHaveKeyword" => {
|
||||
*self = EmailFilter::NoneInThreadHaveKeyword(map.next_value()?);
|
||||
},
|
||||
b"hasKeyword" => {
|
||||
*self = EmailFilter::HasKeyword(map.next_value()?);
|
||||
},
|
||||
b"notKeyword" => {
|
||||
*self = EmailFilter::NotKeyword(map.next_value()?);
|
||||
},
|
||||
b"hasAttachment" => {
|
||||
*self = EmailFilter::HasAttachment(map.next_value()?);
|
||||
},
|
||||
b"from" => {
|
||||
*self = EmailFilter::From(map.next_value()?);
|
||||
},
|
||||
b"to" => {
|
||||
*self = EmailFilter::To(map.next_value()?);
|
||||
},
|
||||
b"cc" => {
|
||||
*self = EmailFilter::Cc(map.next_value()?);
|
||||
},
|
||||
b"bcc" => {
|
||||
*self = EmailFilter::Bcc(map.next_value()?);
|
||||
},
|
||||
b"subject" => {
|
||||
*self = EmailFilter::Subject(map.next_value()?);
|
||||
},
|
||||
b"body" => {
|
||||
*self = EmailFilter::Body(map.next_value()?);
|
||||
},
|
||||
b"header" => {
|
||||
*self = EmailFilter::Header(map.next_value()?);
|
||||
},
|
||||
b"text" => {
|
||||
*self = EmailFilter::Text(map.next_value()?);
|
||||
},
|
||||
b"sentBefore" => {
|
||||
*self = EmailFilter::SentBefore(map.next_value()?);
|
||||
},
|
||||
b"sentAfter" => {
|
||||
*self = EmailFilter::SentAfter(map.next_value()?);
|
||||
},
|
||||
b"inThread" => {
|
||||
*self = EmailFilter::InThread(map.next_value()?);
|
||||
},
|
||||
b"id" => {
|
||||
*self = EmailFilter::Id(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = EmailFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"receivedAt" => {
|
||||
*self = EmailComparator::ReceivedAt;
|
||||
},
|
||||
b"size" => {
|
||||
*self = EmailComparator::Size;
|
||||
},
|
||||
b"from" => {
|
||||
*self = EmailComparator::From;
|
||||
},
|
||||
b"to" => {
|
||||
*self = EmailComparator::To;
|
||||
},
|
||||
b"cc" => {
|
||||
*self = EmailComparator::Cc;
|
||||
},
|
||||
b"subject" => {
|
||||
*self = EmailComparator::Subject;
|
||||
},
|
||||
b"sentAt" => {
|
||||
*self = EmailComparator::SentAt;
|
||||
},
|
||||
b"threadId" => {
|
||||
*self = EmailComparator::ThreadId;
|
||||
},
|
||||
b"hasKeyword" => {
|
||||
*self = EmailComparator::HasKeyword(self.take_keyword());
|
||||
},
|
||||
b"allInThreadHaveKeyword" => {
|
||||
*self = EmailComparator::AllInThreadHaveKeyword(self.take_keyword());
|
||||
},
|
||||
b"someInThreadHaveKeyword" => {
|
||||
*self = EmailComparator::SomeInThreadHaveKeyword(self.take_keyword());
|
||||
},
|
||||
_ => {
|
||||
*self = EmailComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else if key == "keyword" {
|
||||
let keyword: Keyword = map.next_value()?;
|
||||
match self {
|
||||
EmailComparator::HasKeyword(_) => *self = EmailComparator::HasKeyword(keyword),
|
||||
EmailComparator::AllInThreadHaveKeyword(_) => {
|
||||
*self = EmailComparator::AllInThreadHaveKeyword(keyword)
|
||||
}
|
||||
EmailComparator::SomeInThreadHaveKeyword(_) => {
|
||||
*self = EmailComparator::SomeInThreadHaveKeyword(keyword)
|
||||
}
|
||||
_ => {
|
||||
*self = EmailComparator::HasKeyword(keyword);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmailFilter {
|
||||
fn default() -> Self {
|
||||
EmailFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmailComparator {
|
||||
fn default() -> Self {
|
||||
EmailComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailComparator {
|
||||
fn take_keyword(&mut self) -> Keyword {
|
||||
match self {
|
||||
EmailComparator::HasKeyword(k) => {
|
||||
std::mem::replace(k, Keyword::Other(Default::default()))
|
||||
}
|
||||
EmailComparator::AllInThreadHaveKeyword(k) => {
|
||||
std::mem::replace(k, Keyword::Other(Default::default()))
|
||||
}
|
||||
EmailComparator::SomeInThreadHaveKeyword(k) => {
|
||||
std::mem::replace(k, Keyword::Other(Default::default()))
|
||||
}
|
||||
_ => Keyword::Other(Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
EmailFilter::InMailbox(_) => "inMailbox",
|
||||
EmailFilter::InMailboxOtherThan(_) => "inMailboxOtherThan",
|
||||
EmailFilter::Before(_) => "before",
|
||||
EmailFilter::After(_) => "after",
|
||||
EmailFilter::MinSize(_) => "minSize",
|
||||
EmailFilter::MaxSize(_) => "maxSize",
|
||||
EmailFilter::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword",
|
||||
EmailFilter::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword",
|
||||
EmailFilter::NoneInThreadHaveKeyword(_) => "noneInThreadHaveKeyword",
|
||||
EmailFilter::HasKeyword(_) => "hasKeyword",
|
||||
EmailFilter::NotKeyword(_) => "notKeyword",
|
||||
EmailFilter::HasAttachment(_) => "hasAttachment",
|
||||
EmailFilter::From(_) => "from",
|
||||
EmailFilter::To(_) => "to",
|
||||
EmailFilter::Cc(_) => "cc",
|
||||
EmailFilter::Bcc(_) => "bcc",
|
||||
EmailFilter::Subject(_) => "subject",
|
||||
EmailFilter::Body(_) => "body",
|
||||
EmailFilter::Header(_) => "header",
|
||||
EmailFilter::Text(_) => "text",
|
||||
EmailFilter::SentBefore(_) => "sentBefore",
|
||||
EmailFilter::SentAfter(_) => "sentAfter",
|
||||
EmailFilter::InThread(_) => "inThread",
|
||||
EmailFilter::Id(_) => "id",
|
||||
EmailFilter::_T(v) => v.as_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailComparator {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailComparator {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
EmailComparator::ReceivedAt => "receivedAt",
|
||||
EmailComparator::Size => "size",
|
||||
EmailComparator::From => "from",
|
||||
EmailComparator::To => "to",
|
||||
EmailComparator::Subject => "subject",
|
||||
EmailComparator::Cc => "cc",
|
||||
EmailComparator::SentAt => "sentAt",
|
||||
EmailComparator::ThreadId => "threadId",
|
||||
EmailComparator::HasKeyword(_) => "hasKeyword",
|
||||
EmailComparator::AllInThreadHaveKeyword(_) => "allInThreadHaveKeyword",
|
||||
EmailComparator::SomeInThreadHaveKeyword(_) => "someInThreadHaveKeyword",
|
||||
EmailComparator::_T(v) => v.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for EmailComparator {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter<EmailFilter> {
|
||||
pub fn is_immutable(&self) -> bool {
|
||||
match self {
|
||||
Filter::Property(f) => f.is_immutable(),
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFilter {
|
||||
pub fn is_immutable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EmailFilter::Before(_)
|
||||
| EmailFilter::After(_)
|
||||
| EmailFilter::MinSize(_)
|
||||
| EmailFilter::MaxSize(_)
|
||||
| EmailFilter::HasAttachment(_)
|
||||
| EmailFilter::From(_)
|
||||
| EmailFilter::To(_)
|
||||
| EmailFilter::Cc(_)
|
||||
| EmailFilter::Bcc(_)
|
||||
| EmailFilter::Subject(_)
|
||||
| EmailFilter::Body(_)
|
||||
| EmailFilter::Header(_)
|
||||
| EmailFilter::Text(_)
|
||||
| EmailFilter::Id(_)
|
||||
| EmailFilter::SentBefore(_)
|
||||
| EmailFilter::SentAfter(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Comparator<EmailComparator> {
|
||||
pub fn is_immutable(&self) -> bool {
|
||||
self.property.is_immutable()
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailComparator {
|
||||
pub fn is_immutable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EmailComparator::ReceivedAt
|
||||
| EmailComparator::Size
|
||||
| EmailComparator::From
|
||||
| EmailComparator::To
|
||||
| EmailComparator::Subject
|
||||
| EmailComparator::Cc
|
||||
| EmailComparator::SentAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let EmailValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
EmailValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
EmailValue::BlobId(id) => Some(AnyId::BlobId(id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let EmailValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = EmailValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(id) => {
|
||||
*self = EmailValue::BlobId(id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for EmailValue {
|
||||
fn from(id: Id) -> Self {
|
||||
EmailValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobId> for EmailValue {
|
||||
fn from(id: BlobId) -> Self {
|
||||
EmailValue::BlobId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UTCDate> for EmailValue {
|
||||
fn from(date: UTCDate) -> Self {
|
||||
EmailValue::Date(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let EmailProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let EmailProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
match self {
|
||||
EmailProperty::IdReference(r) => Some(r),
|
||||
EmailProperty::Pointer(value) => {
|
||||
let value = value.as_slice();
|
||||
match (value.first(), value.get(1)) {
|
||||
(
|
||||
Some(JsonPointerItem::Key(Key::Property(EmailProperty::MailboxIds))),
|
||||
Some(JsonPointerItem::Key(Key::Property(EmailProperty::IdReference(r)))),
|
||||
) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
if let EmailProperty::Pointer(value) = self {
|
||||
let value = value.as_mut_slice();
|
||||
if let Some(value) = value.get_mut(1) {
|
||||
*value = JsonPointerItem::Key(Key::Property(EmailProperty::IdValue(id)));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
*self = EmailProperty::IdValue(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, MaybeReference,
|
||||
email::{EmailProperty, EmailValue},
|
||||
parse_ref,
|
||||
},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments, reference::MaybeIdReference},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property, Value};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EmailSubmission;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EmailSubmissionProperty {
|
||||
Id,
|
||||
IdentityId,
|
||||
ThreadId,
|
||||
EmailId,
|
||||
Envelope,
|
||||
MailFrom,
|
||||
RcptTo,
|
||||
Email,
|
||||
Parameters,
|
||||
SendAt,
|
||||
UndoStatus,
|
||||
DeliveryStatus,
|
||||
SmtpReply,
|
||||
Delivered,
|
||||
Displayed,
|
||||
DsnBlobIds,
|
||||
MdnBlobIds,
|
||||
|
||||
Pointer(JsonPointer<EmailSubmissionProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EmailSubmissionValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
BlobId(BlobId),
|
||||
UndoStatus(UndoStatus),
|
||||
Delivered(Delivered),
|
||||
Displayed(Displayed),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum UndoStatus {
|
||||
Pending,
|
||||
Final,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Delivered {
|
||||
Queued,
|
||||
Yes,
|
||||
No,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Displayed {
|
||||
Yes,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Property for EmailSubmissionProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
EmailSubmissionProperty::parse(value, key.is_none())
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
EmailSubmissionProperty::DeliveryStatus => "deliveryStatus",
|
||||
EmailSubmissionProperty::DsnBlobIds => "dsnBlobIds",
|
||||
EmailSubmissionProperty::Email => "email",
|
||||
EmailSubmissionProperty::Envelope => "envelope",
|
||||
EmailSubmissionProperty::Id => "id",
|
||||
EmailSubmissionProperty::IdentityId => "identityId",
|
||||
EmailSubmissionProperty::MdnBlobIds => "mdnBlobIds",
|
||||
EmailSubmissionProperty::SendAt => "sendAt",
|
||||
EmailSubmissionProperty::ThreadId => "threadId",
|
||||
EmailSubmissionProperty::UndoStatus => "undoStatus",
|
||||
EmailSubmissionProperty::Parameters => "parameters",
|
||||
EmailSubmissionProperty::SmtpReply => "smtpReply",
|
||||
EmailSubmissionProperty::Delivered => "delivered",
|
||||
EmailSubmissionProperty::Displayed => "displayed",
|
||||
EmailSubmissionProperty::MailFrom => "mailFrom",
|
||||
EmailSubmissionProperty::RcptTo => "rcptTo",
|
||||
EmailSubmissionProperty::EmailId => "emailId",
|
||||
EmailSubmissionProperty::Pointer(json_pointer) => {
|
||||
return json_pointer.to_string().into();
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for EmailSubmissionValue {
|
||||
type Property = EmailSubmissionProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
EmailSubmissionProperty::Id
|
||||
| EmailSubmissionProperty::ThreadId
|
||||
| EmailSubmissionProperty::IdentityId
|
||||
| EmailSubmissionProperty::EmailId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(EmailSubmissionValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(EmailSubmissionValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
EmailSubmissionProperty::MdnBlobIds | EmailSubmissionProperty::DsnBlobIds => {
|
||||
match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(EmailSubmissionValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(EmailSubmissionValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
}
|
||||
}
|
||||
EmailSubmissionProperty::SendAt => UTCDate::from_str(value)
|
||||
.ok()
|
||||
.map(EmailSubmissionValue::Date),
|
||||
EmailSubmissionProperty::UndoStatus => {
|
||||
UndoStatus::parse(value).map(EmailSubmissionValue::UndoStatus)
|
||||
}
|
||||
EmailSubmissionProperty::Delivered => {
|
||||
Delivered::parse(value).map(EmailSubmissionValue::Delivered)
|
||||
}
|
||||
EmailSubmissionProperty::Displayed => {
|
||||
Displayed::parse(value).map(EmailSubmissionValue::Displayed)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
EmailSubmissionValue::Id(id) => id.to_string().into(),
|
||||
EmailSubmissionValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
EmailSubmissionValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
EmailSubmissionValue::IdReference(r) => format!("#{r}").into(),
|
||||
EmailSubmissionValue::UndoStatus(undo_status) => undo_status.as_str().into(),
|
||||
EmailSubmissionValue::Delivered(delivered) => delivered.as_str().into(),
|
||||
EmailSubmissionValue::Displayed(displayed) => displayed.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailSubmissionProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
"id" => EmailSubmissionProperty::Id,
|
||||
"identityId" => EmailSubmissionProperty::IdentityId,
|
||||
"threadId" => EmailSubmissionProperty::ThreadId,
|
||||
"emailId" => EmailSubmissionProperty::EmailId,
|
||||
"envelope" => EmailSubmissionProperty::Envelope,
|
||||
"mailFrom" => EmailSubmissionProperty::MailFrom,
|
||||
"rcptTo" => EmailSubmissionProperty::RcptTo,
|
||||
"email" => EmailSubmissionProperty::Email,
|
||||
"parameters" => EmailSubmissionProperty::Parameters,
|
||||
"sendAt" => EmailSubmissionProperty::SendAt,
|
||||
"undoStatus" => EmailSubmissionProperty::UndoStatus,
|
||||
"deliveryStatus" => EmailSubmissionProperty::DeliveryStatus,
|
||||
"smtpReply" => EmailSubmissionProperty::SmtpReply,
|
||||
"delivered" => EmailSubmissionProperty::Delivered,
|
||||
"displayed" => EmailSubmissionProperty::Displayed,
|
||||
"dsnBlobIds" => EmailSubmissionProperty::DsnBlobIds,
|
||||
"mdnBlobIds" => EmailSubmissionProperty::MdnBlobIds,
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
EmailSubmissionProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &EmailSubmissionProperty {
|
||||
if let EmailSubmissionProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UndoStatus {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"pending" => UndoStatus::Pending,
|
||||
b"final" => UndoStatus::Final,
|
||||
b"canceled" => UndoStatus::Canceled,
|
||||
)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
UndoStatus::Pending => "pending",
|
||||
UndoStatus::Final => "final",
|
||||
UndoStatus::Canceled => "canceled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Delivered {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"queued" => Delivered::Queued,
|
||||
b"yes" => Delivered::Yes,
|
||||
b"no" => Delivered::No,
|
||||
b"unknown" => Delivered::Unknown,
|
||||
)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Delivered::Queued => "queued",
|
||||
Delivered::Yes => "yes",
|
||||
Delivered::No => "no",
|
||||
Delivered::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Displayed {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"yes" => Displayed::Yes,
|
||||
b"unknown" => Displayed::Unknown,
|
||||
)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Displayed::Yes => "yes",
|
||||
Displayed::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EmailSubmissionSetArguments<'x> {
|
||||
pub on_success_update_email:
|
||||
Option<VecMap<MaybeIdReference<Id>, Value<'x, EmailProperty, EmailValue>>>,
|
||||
pub on_success_destroy_email: Option<Vec<MaybeIdReference<Id>>>,
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for EmailSubmissionSetArguments<'x> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onSuccessUpdateEmail" => {
|
||||
self.on_success_update_email = map.next_value()?;
|
||||
},
|
||||
b"onSuccessDestroyEmail" => {
|
||||
self.on_success_destroy_email = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailSubmissionProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailSubmissionProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for UndoStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
UndoStatus::parse(<&str>::deserialize(deserializer)?)
|
||||
.ok_or_else(|| serde::de::Error::custom("invalid JMAP UndoStatus"))
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for EmailSubmission {
|
||||
type Property = EmailSubmissionProperty;
|
||||
|
||||
type Element = EmailSubmissionValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = EmailSubmissionFilter;
|
||||
|
||||
type Comparator = EmailSubmissionComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = EmailSubmissionSetArguments<'de>;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = EmailSubmissionProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EmailSubmissionFilter {
|
||||
IdentityIds(Vec<MaybeInvalid<Id>>),
|
||||
EmailIds(Vec<MaybeInvalid<Id>>),
|
||||
ThreadIds(Vec<MaybeInvalid<Id>>),
|
||||
Before(UTCDate),
|
||||
After(UTCDate),
|
||||
UndoStatus(UndoStatus),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EmailSubmissionComparator {
|
||||
EmailId,
|
||||
ThreadId,
|
||||
SentAt,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailSubmissionFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"identityIds" => {
|
||||
*self = EmailSubmissionFilter::IdentityIds(map.next_value()?);
|
||||
},
|
||||
b"emailIds" => {
|
||||
*self = EmailSubmissionFilter::EmailIds(map.next_value()?);
|
||||
},
|
||||
b"threadIds" => {
|
||||
*self = EmailSubmissionFilter::ThreadIds(map.next_value()?);
|
||||
},
|
||||
b"before" => {
|
||||
*self = EmailSubmissionFilter::Before(map.next_value()?);
|
||||
},
|
||||
b"after" => {
|
||||
*self = EmailSubmissionFilter::After(map.next_value()?);
|
||||
},
|
||||
b"undoStatus" => {
|
||||
*self = EmailSubmissionFilter::UndoStatus(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = EmailSubmissionFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for EmailSubmissionComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
|
||||
b"emailId" => {
|
||||
*self = EmailSubmissionComparator::EmailId;
|
||||
},
|
||||
b"threadId" => {
|
||||
*self = EmailSubmissionComparator::ThreadId;
|
||||
},
|
||||
b"sentAt" => {
|
||||
*self = EmailSubmissionComparator::SentAt;
|
||||
},
|
||||
_ => {
|
||||
*self = EmailSubmissionComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmailSubmissionFilter {
|
||||
fn default() -> Self {
|
||||
EmailSubmissionFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EmailSubmissionComparator {
|
||||
fn default() -> Self {
|
||||
EmailSubmissionComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for EmailSubmissionValue {
|
||||
fn from(id: Id) -> Self {
|
||||
EmailSubmissionValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailSubmissionValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
EmailSubmissionValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
EmailSubmissionValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
EmailSubmissionValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let EmailSubmissionValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = EmailSubmissionValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(blob_id) => {
|
||||
*self = EmailSubmissionValue::BlobId(blob_id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for EmailSubmissionProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref,
|
||||
},
|
||||
request::{MaybeInvalid, deserialize::DeserializeArguments},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::{acl::Acl, blob::BlobId, id::Id};
|
||||
use utils::glob::GlobPattern;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNode;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeProperty {
|
||||
Id,
|
||||
ParentId,
|
||||
BlobId,
|
||||
Size,
|
||||
Name,
|
||||
Type,
|
||||
NodeType,
|
||||
Target,
|
||||
Created,
|
||||
Modified,
|
||||
Accessed,
|
||||
Changed,
|
||||
Executable,
|
||||
Role,
|
||||
MyRights,
|
||||
ShareWith,
|
||||
IsSubscribed,
|
||||
|
||||
IdValue(Id),
|
||||
Rights(FileNodeRight),
|
||||
Pointer(JsonPointer<FileNodeProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeRight {
|
||||
MayRead,
|
||||
MayAddChildren,
|
||||
MayRename,
|
||||
MayDelete,
|
||||
MayModifyContent,
|
||||
MayShare,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeNodeType {
|
||||
File,
|
||||
Directory,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeRole {
|
||||
Root,
|
||||
Home,
|
||||
Temp,
|
||||
Trash,
|
||||
Documents,
|
||||
Downloads,
|
||||
Music,
|
||||
Pictures,
|
||||
Videos,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileNodeValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
impl Property for FileNodeProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
FileNodeProperty::ShareWith => {
|
||||
Id::from_str(value).ok().map(FileNodeProperty::IdValue)
|
||||
}
|
||||
_ => FileNodeProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
FileNodeProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeProperty::Id => "id",
|
||||
FileNodeProperty::ParentId => "parentId",
|
||||
FileNodeProperty::BlobId => "blobId",
|
||||
FileNodeProperty::Size => "size",
|
||||
FileNodeProperty::Name => "name",
|
||||
FileNodeProperty::Type => "type",
|
||||
FileNodeProperty::NodeType => "nodeType",
|
||||
FileNodeProperty::Target => "target",
|
||||
FileNodeProperty::Created => "created",
|
||||
FileNodeProperty::Modified => "modified",
|
||||
FileNodeProperty::Accessed => "accessed",
|
||||
FileNodeProperty::Changed => "changed",
|
||||
FileNodeProperty::Executable => "executable",
|
||||
FileNodeProperty::Role => "role",
|
||||
FileNodeProperty::MyRights => "myRights",
|
||||
FileNodeProperty::ShareWith => "shareWith",
|
||||
FileNodeProperty::IsSubscribed => "isSubscribed",
|
||||
FileNodeProperty::Rights(file_right) => file_right.as_str(),
|
||||
FileNodeProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
FileNodeProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeRight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileNodeRight::MayRead => "mayRead",
|
||||
FileNodeRight::MayAddChildren => "mayAddChildren",
|
||||
FileNodeRight::MayRename => "mayRename",
|
||||
FileNodeRight::MayDelete => "mayDelete",
|
||||
FileNodeRight::MayModifyContent => "mayModifyContent",
|
||||
FileNodeRight::MayShare => "mayShare",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeNodeType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileNodeNodeType::File => "file",
|
||||
FileNodeNodeType::Directory => "directory",
|
||||
FileNodeNodeType::Symlink => "symlink",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"file" => FileNodeNodeType::File,
|
||||
b"directory" => FileNodeNodeType::Directory,
|
||||
b"symlink" => FileNodeNodeType::Symlink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileNodeNodeType {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
FileNodeNodeType::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileNodeNodeType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeRole {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
FileNodeRole::Root => "root",
|
||||
FileNodeRole::Home => "home",
|
||||
FileNodeRole::Temp => "temp",
|
||||
FileNodeRole::Trash => "trash",
|
||||
FileNodeRole::Documents => "documents",
|
||||
FileNodeRole::Downloads => "downloads",
|
||||
FileNodeRole::Music => "music",
|
||||
FileNodeRole::Pictures => "pictures",
|
||||
FileNodeRole::Videos => "videos",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"root" => FileNodeRole::Root,
|
||||
b"home" => FileNodeRole::Home,
|
||||
b"temp" => FileNodeRole::Temp,
|
||||
b"trash" => FileNodeRole::Trash,
|
||||
b"documents" => FileNodeRole::Documents,
|
||||
b"downloads" => FileNodeRole::Downloads,
|
||||
b"music" => FileNodeRole::Music,
|
||||
b"pictures" => FileNodeRole::Pictures,
|
||||
b"videos" => FileNodeRole::Videos,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileNodeRole {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
FileNodeRole::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileNodeRole {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FileNodeValue {
|
||||
type Property = FileNodeProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
FileNodeProperty::Id | FileNodeProperty::ParentId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(FileNodeValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(FileNodeValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
FileNodeProperty::BlobId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(FileNodeValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(FileNodeValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
FileNodeProperty::Created
|
||||
| FileNodeProperty::Modified
|
||||
| FileNodeProperty::Accessed
|
||||
| FileNodeProperty::Changed => {
|
||||
UTCDate::from_str(value).ok().map(FileNodeValue::Date)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => id.to_string().into(),
|
||||
FileNodeValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
FileNodeValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
FileNodeValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => FileNodeProperty::Id,
|
||||
b"parentId" => FileNodeProperty::ParentId,
|
||||
b"blobId" => FileNodeProperty::BlobId,
|
||||
b"size" => FileNodeProperty::Size,
|
||||
b"name" => FileNodeProperty::Name,
|
||||
b"type" => FileNodeProperty::Type,
|
||||
b"nodeType" => FileNodeProperty::NodeType,
|
||||
b"target" => FileNodeProperty::Target,
|
||||
b"created" => FileNodeProperty::Created,
|
||||
b"modified" => FileNodeProperty::Modified,
|
||||
b"accessed" => FileNodeProperty::Accessed,
|
||||
b"changed" => FileNodeProperty::Changed,
|
||||
b"executable" => FileNodeProperty::Executable,
|
||||
b"role" => FileNodeProperty::Role,
|
||||
b"myRights" => FileNodeProperty::MyRights,
|
||||
b"shareWith" => FileNodeProperty::ShareWith,
|
||||
b"isSubscribed" => FileNodeProperty::IsSubscribed,
|
||||
b"mayRead" => FileNodeProperty::Rights(FileNodeRight::MayRead),
|
||||
b"mayAddChildren" => FileNodeProperty::Rights(FileNodeRight::MayAddChildren),
|
||||
b"mayRename" => FileNodeProperty::Rights(FileNodeRight::MayRename),
|
||||
b"mayDelete" => FileNodeProperty::Rights(FileNodeRight::MayDelete),
|
||||
b"mayModifyContent" => FileNodeProperty::Rights(FileNodeRight::MayModifyContent),
|
||||
b"mayShare" => FileNodeProperty::Rights(FileNodeRight::MayShare),
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
FileNodeProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &FileNodeProperty {
|
||||
if let FileNodeProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNodeSetArguments {
|
||||
pub on_destroy_remove_children: Option<bool>,
|
||||
pub on_exists: OnExists,
|
||||
pub compare_case_insensitively: Option<bool>,
|
||||
}
|
||||
|
||||
pub type FileNodeCopyArguments = FileNodeSetArguments;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum OnExists {
|
||||
#[default]
|
||||
Reject,
|
||||
Replace,
|
||||
Rename,
|
||||
Newest,
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for OnExists {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value: Option<Cow<'_, str>> = Option::deserialize(deserializer)?;
|
||||
match value.as_deref() {
|
||||
Some("replace") => Ok(OnExists::Replace),
|
||||
Some("rename") => Ok(OnExists::Rename),
|
||||
Some("newest") => Ok(OnExists::Newest),
|
||||
None | Some("") => Ok(OnExists::Reject),
|
||||
Some(other) => Err(serde::de::Error::custom(format!(
|
||||
"Invalid onExists value: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for FileNodeSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onDestroyRemoveChildren" => {
|
||||
self.on_destroy_remove_children = map.next_value()?;
|
||||
},
|
||||
b"onExists" => {
|
||||
self.on_exists = map.next_value()?;
|
||||
},
|
||||
b"compareCaseInsensitively" => {
|
||||
self.compare_case_insensitively = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNodeGetArguments {
|
||||
pub fetch_parents: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for FileNodeGetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
if key == "fetchParents" {
|
||||
self.fetch_parents = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileNodeQueryArguments {
|
||||
pub depth: Option<u32>,
|
||||
}
|
||||
|
||||
impl<'x> DeserializeArguments<'x> for FileNodeQueryArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'x>,
|
||||
{
|
||||
if key == "depth" {
|
||||
self.depth = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileNodeProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
FileNodeProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for FileNode {
|
||||
type Property = FileNodeProperty;
|
||||
|
||||
type Element = FileNodeValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = FileNodeFilter;
|
||||
|
||||
type Comparator = FileNodeComparator;
|
||||
|
||||
type GetArguments = FileNodeGetArguments;
|
||||
|
||||
type SetArguments<'de> = FileNodeSetArguments;
|
||||
|
||||
type QueryArguments = FileNodeQueryArguments;
|
||||
|
||||
type CopyArguments = FileNodeCopyArguments;
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = FileNodeProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapSharedObject for FileNode {
|
||||
type Right = FileNodeRight;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property = FileNodeProperty::ShareWith;
|
||||
}
|
||||
|
||||
impl From<Id> for FileNodeProperty {
|
||||
fn from(id: Id) -> Self {
|
||||
FileNodeProperty::IdValue(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapRight for FileNodeRight {
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
match self {
|
||||
FileNodeRight::MayRead => &[Acl::Read, Acl::ReadItems],
|
||||
FileNodeRight::MayAddChildren => &[Acl::AddItems],
|
||||
FileNodeRight::MayRename => &[Acl::Modify],
|
||||
FileNodeRight::MayDelete => &[Acl::Delete, Acl::RemoveItems],
|
||||
FileNodeRight::MayModifyContent => &[Acl::ModifyItems],
|
||||
FileNodeRight::MayShare => &[Acl::Share],
|
||||
}
|
||||
}
|
||||
|
||||
fn all_rights() -> &'static [Self] {
|
||||
&[
|
||||
FileNodeRight::MayRead,
|
||||
FileNodeRight::MayAddChildren,
|
||||
FileNodeRight::MayRename,
|
||||
FileNodeRight::MayDelete,
|
||||
FileNodeRight::MayModifyContent,
|
||||
FileNodeRight::MayShare,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileNodeRight> for FileNodeProperty {
|
||||
fn from(right: FileNodeRight) -> Self {
|
||||
FileNodeProperty::Rights(right)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FileNodeFilter {
|
||||
IsTopLevel(bool),
|
||||
ParentId(MaybeInvalid<Id>),
|
||||
AncestorId(MaybeInvalid<Id>),
|
||||
DescendantId(MaybeInvalid<Id>),
|
||||
NodeType(String),
|
||||
Role(String),
|
||||
HasAnyRole(bool),
|
||||
BlobId(MaybeInvalid<BlobId>),
|
||||
IsExecutable(bool),
|
||||
CreatedBefore(UTCDate),
|
||||
CreatedAfter(UTCDate),
|
||||
ModifiedBefore(UTCDate),
|
||||
ModifiedAfter(UTCDate),
|
||||
AccessedBefore(UTCDate),
|
||||
AccessedAfter(UTCDate),
|
||||
MinSize(u64),
|
||||
MaxSize(u64),
|
||||
Name(String),
|
||||
NameMatch(GlobPattern),
|
||||
Type(String),
|
||||
TypeMatch(GlobPattern),
|
||||
Text(String),
|
||||
Body(String),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FileNodeComparator {
|
||||
Name,
|
||||
Size,
|
||||
Created,
|
||||
Modified,
|
||||
Type,
|
||||
NodeType,
|
||||
Tree,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for FileNodeFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"isTopLevel" => {
|
||||
*self = FileNodeFilter::IsTopLevel(map.next_value()?);
|
||||
},
|
||||
b"parentId" => {
|
||||
*self = FileNodeFilter::ParentId(map.next_value()?);
|
||||
},
|
||||
b"ancestorId" => {
|
||||
*self = FileNodeFilter::AncestorId(map.next_value()?);
|
||||
},
|
||||
b"descendantId" => {
|
||||
*self = FileNodeFilter::DescendantId(map.next_value()?);
|
||||
},
|
||||
b"nodeType" => {
|
||||
*self = FileNodeFilter::NodeType(map.next_value()?);
|
||||
},
|
||||
b"role" => {
|
||||
*self = FileNodeFilter::Role(map.next_value()?);
|
||||
},
|
||||
b"hasAnyRole" => {
|
||||
*self = FileNodeFilter::HasAnyRole(map.next_value()?);
|
||||
},
|
||||
b"blobId" => {
|
||||
*self = FileNodeFilter::BlobId(map.next_value()?);
|
||||
},
|
||||
b"isExecutable" => {
|
||||
*self = FileNodeFilter::IsExecutable(map.next_value()?);
|
||||
},
|
||||
b"createdBefore" => {
|
||||
*self = FileNodeFilter::CreatedBefore(map.next_value()?);
|
||||
},
|
||||
b"createdAfter" => {
|
||||
*self = FileNodeFilter::CreatedAfter(map.next_value()?);
|
||||
},
|
||||
b"modifiedBefore" => {
|
||||
*self = FileNodeFilter::ModifiedBefore(map.next_value()?);
|
||||
},
|
||||
b"modifiedAfter" => {
|
||||
*self = FileNodeFilter::ModifiedAfter(map.next_value()?);
|
||||
},
|
||||
b"accessedBefore" => {
|
||||
*self = FileNodeFilter::AccessedBefore(map.next_value()?);
|
||||
},
|
||||
b"accessedAfter" => {
|
||||
*self = FileNodeFilter::AccessedAfter(map.next_value()?);
|
||||
},
|
||||
b"minSize" => {
|
||||
*self = FileNodeFilter::MinSize(map.next_value()?);
|
||||
},
|
||||
b"maxSize" => {
|
||||
*self = FileNodeFilter::MaxSize(map.next_value()?);
|
||||
},
|
||||
b"name" => {
|
||||
*self = FileNodeFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"nameMatch" => {
|
||||
*self = FileNodeFilter::NameMatch(map.next_value()?);
|
||||
},
|
||||
b"type" => {
|
||||
*self = FileNodeFilter::Type(map.next_value()?);
|
||||
},
|
||||
b"typeMatch" => {
|
||||
*self = FileNodeFilter::TypeMatch(map.next_value()?);
|
||||
},
|
||||
b"body" => {
|
||||
*self = FileNodeFilter::Body(map.next_value()?);
|
||||
},
|
||||
b"text" => {
|
||||
*self = FileNodeFilter::Text(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = FileNodeFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for FileNodeComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"name" => {
|
||||
*self = FileNodeComparator::Name;
|
||||
},
|
||||
b"size" => {
|
||||
*self = FileNodeComparator::Size;
|
||||
},
|
||||
b"created" => {
|
||||
*self = FileNodeComparator::Created;
|
||||
},
|
||||
b"modified" => {
|
||||
*self = FileNodeComparator::Modified;
|
||||
},
|
||||
b"type" => {
|
||||
*self = FileNodeComparator::Type;
|
||||
},
|
||||
b"nodeType" => {
|
||||
*self = FileNodeComparator::NodeType;
|
||||
},
|
||||
b"tree" => {
|
||||
*self = FileNodeComparator::Tree;
|
||||
},
|
||||
_ => {
|
||||
*self = FileNodeComparator::_T(value.into_owned());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileNodeFilter {
|
||||
fn default() -> Self {
|
||||
FileNodeFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileNodeComparator {
|
||||
fn default() -> Self {
|
||||
FileNodeComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for FileNodeValue {
|
||||
fn from(id: Id) -> Self {
|
||||
FileNodeValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for FileNodeValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
FileNodeValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
FileNodeValue::BlobId(blob_id) => Some(AnyId::BlobId(blob_id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let FileNodeValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = FileNodeValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(blob_id) => {
|
||||
*self = FileNodeValue::BlobId(blob_id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeFilter::IsTopLevel(_) => "isTopLevel",
|
||||
FileNodeFilter::ParentId(_) => "parentId",
|
||||
FileNodeFilter::AncestorId(_) => "ancestorId",
|
||||
FileNodeFilter::DescendantId(_) => "descendantId",
|
||||
FileNodeFilter::NodeType(_) => "nodeType",
|
||||
FileNodeFilter::Role(_) => "role",
|
||||
FileNodeFilter::HasAnyRole(_) => "hasAnyRole",
|
||||
FileNodeFilter::BlobId(_) => "blobId",
|
||||
FileNodeFilter::IsExecutable(_) => "isExecutable",
|
||||
FileNodeFilter::CreatedBefore(_) => "createdBefore",
|
||||
FileNodeFilter::CreatedAfter(_) => "createdAfter",
|
||||
FileNodeFilter::ModifiedBefore(_) => "modifiedBefore",
|
||||
FileNodeFilter::ModifiedAfter(_) => "modifiedAfter",
|
||||
FileNodeFilter::AccessedBefore(_) => "accessedBefore",
|
||||
FileNodeFilter::AccessedAfter(_) => "accessedAfter",
|
||||
FileNodeFilter::MinSize(_) => "minSize",
|
||||
FileNodeFilter::MaxSize(_) => "maxSize",
|
||||
FileNodeFilter::Name(_) => "name",
|
||||
FileNodeFilter::NameMatch(_) => "nameMatch",
|
||||
FileNodeFilter::Type(_) => "type",
|
||||
FileNodeFilter::TypeMatch(_) => "typeMatch",
|
||||
FileNodeFilter::Text(_) => "text",
|
||||
FileNodeFilter::Body(_) => "body",
|
||||
FileNodeFilter::_T(s) => return s.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileNodeComparator {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
FileNodeComparator::Name => "name",
|
||||
FileNodeComparator::Size => "size",
|
||||
FileNodeComparator::Created => "created",
|
||||
FileNodeComparator::Modified => "modified",
|
||||
FileNodeComparator::Type => "type",
|
||||
FileNodeComparator::NodeType => "nodeType",
|
||||
FileNodeComparator::Tree => "tree",
|
||||
FileNodeComparator::_T(s) => s.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
FileNodeComparator::Name => "name",
|
||||
FileNodeComparator::Size => "size",
|
||||
FileNodeComparator::Created => "created",
|
||||
FileNodeComparator::Modified => "modified",
|
||||
FileNodeComparator::Type => "type",
|
||||
FileNodeComparator::NodeType => "nodeType",
|
||||
FileNodeComparator::Tree => "tree",
|
||||
FileNodeComparator::_T(s) => return s.into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for FileNodeComparator {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileNodeProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: FileNodeProperty) -> Result<Self, Self::Error> {
|
||||
if let FileNodeProperty::IdValue(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileNodeProperty> for FileNodeRight {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: FileNodeProperty) -> Result<Self, Self::Error> {
|
||||
if let FileNodeProperty::Rights(right) = value {
|
||||
Ok(right)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for FileNodeProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let FileNodeProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let FileNodeProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = FileNodeProperty::IdValue(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileNodeProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_cow())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::object::{AnyId, JmapObject, JmapObjectId};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Identity;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum IdentityProperty {
|
||||
Id,
|
||||
Name,
|
||||
Email,
|
||||
ReplyTo,
|
||||
Bcc,
|
||||
TextSignature,
|
||||
HtmlSignature,
|
||||
MayDelete,
|
||||
|
||||
// Other
|
||||
Pointer(JsonPointer<IdentityProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum IdentityValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for IdentityProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
IdentityProperty::parse(value, key.is_none())
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
IdentityProperty::Bcc => "bcc",
|
||||
IdentityProperty::Email => "email",
|
||||
IdentityProperty::HtmlSignature => "htmlSignature",
|
||||
IdentityProperty::Id => "id",
|
||||
IdentityProperty::MayDelete => "mayDelete",
|
||||
IdentityProperty::Name => "name",
|
||||
IdentityProperty::ReplyTo => "replyTo",
|
||||
IdentityProperty::TextSignature => "textSignature",
|
||||
IdentityProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for IdentityValue {
|
||||
type Property = IdentityProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
IdentityProperty::Id => Id::from_str(value).ok().map(IdentityValue::Id),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
IdentityValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => IdentityProperty::Id,
|
||||
b"name" => IdentityProperty::Name,
|
||||
b"email" => IdentityProperty::Email,
|
||||
b"replyTo" => IdentityProperty::ReplyTo,
|
||||
b"bcc" => IdentityProperty::Bcc,
|
||||
b"textSignature" => IdentityProperty::TextSignature,
|
||||
b"htmlSignature" => IdentityProperty::HtmlSignature,
|
||||
b"mayDelete" => IdentityProperty::MayDelete,
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
IdentityProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &IdentityProperty {
|
||||
if let IdentityProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IdentityProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
IdentityProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Identity {
|
||||
type Property = IdentityProperty;
|
||||
|
||||
type Element = IdentityValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = IdentityProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for IdentityValue {
|
||||
fn from(id: Id) -> Self {
|
||||
IdentityValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for IdentityValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
IdentityValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
IdentityValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = IdentityValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for IdentityProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref,
|
||||
},
|
||||
request::{deserialize::DeserializeArguments, reference::MaybeIdReference},
|
||||
};
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{acl::Acl, id::Id, special_use::SpecialUse};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Mailbox;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MailboxProperty {
|
||||
Id,
|
||||
Name,
|
||||
ParentId,
|
||||
Role,
|
||||
SortOrder,
|
||||
TotalEmails,
|
||||
UnreadEmails,
|
||||
TotalThreads,
|
||||
UnreadThreads,
|
||||
ShareWith,
|
||||
MyRights,
|
||||
IsSubscribed,
|
||||
|
||||
// Other
|
||||
IdValue(Id),
|
||||
Rights(MailboxRight),
|
||||
Pointer(JsonPointer<MailboxProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MailboxRight {
|
||||
MayReadItems,
|
||||
MayAddItems,
|
||||
MayRemoveItems,
|
||||
MaySetSeen,
|
||||
MaySetKeywords,
|
||||
MayCreateChild,
|
||||
MayRename,
|
||||
MaySubmit,
|
||||
MayDelete,
|
||||
MayShare,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MailboxValue {
|
||||
Id(Id),
|
||||
IdReference(String),
|
||||
Role(SpecialUse),
|
||||
}
|
||||
|
||||
impl Property for MailboxProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
let allow_patch = key.is_none();
|
||||
if let Some(Key::Property(key)) = key {
|
||||
match key.patch_or_prop() {
|
||||
MailboxProperty::ShareWith => {
|
||||
Id::from_str(value).ok().map(MailboxProperty::IdValue)
|
||||
}
|
||||
_ => MailboxProperty::parse(value, allow_patch),
|
||||
}
|
||||
} else {
|
||||
MailboxProperty::parse(value, allow_patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
MailboxProperty::Id => "id",
|
||||
MailboxProperty::IsSubscribed => "isSubscribed",
|
||||
MailboxProperty::MyRights => "myRights",
|
||||
MailboxProperty::Name => "name",
|
||||
MailboxProperty::ParentId => "parentId",
|
||||
MailboxProperty::Role => "role",
|
||||
MailboxProperty::SortOrder => "sortOrder",
|
||||
MailboxProperty::TotalEmails => "totalEmails",
|
||||
MailboxProperty::TotalThreads => "totalThreads",
|
||||
MailboxProperty::UnreadEmails => "unreadEmails",
|
||||
MailboxProperty::UnreadThreads => "unreadThreads",
|
||||
MailboxProperty::ShareWith => "shareWith",
|
||||
MailboxProperty::Rights(mailbox_right) => mailbox_right.as_str(),
|
||||
MailboxProperty::Pointer(json_pointer) => return json_pointer.to_string().into(),
|
||||
MailboxProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxRight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
MailboxRight::MayReadItems => "mayReadItems",
|
||||
MailboxRight::MayAddItems => "mayAddItems",
|
||||
MailboxRight::MayRemoveItems => "mayRemoveItems",
|
||||
MailboxRight::MaySetSeen => "maySetSeen",
|
||||
MailboxRight::MaySetKeywords => "maySetKeywords",
|
||||
MailboxRight::MayCreateChild => "mayCreateChild",
|
||||
MailboxRight::MayRename => "mayRename",
|
||||
MailboxRight::MaySubmit => "maySubmit",
|
||||
MailboxRight::MayDelete => "mayDelete",
|
||||
MailboxRight::MayShare => "mayShare",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for MailboxValue {
|
||||
type Property = MailboxProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
MailboxProperty::Id | MailboxProperty::ParentId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(MailboxValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(MailboxValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
MailboxProperty::Role => SpecialUse::parse(value).map(MailboxValue::Role),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
MailboxValue::Id(id) => id.to_string().into(),
|
||||
MailboxValue::IdReference(r) => format!("#{r}").into(),
|
||||
MailboxValue::Role(special_use) => special_use.as_str().unwrap_or_default().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => MailboxProperty::Id,
|
||||
b"name" => MailboxProperty::Name,
|
||||
b"parentId" => MailboxProperty::ParentId,
|
||||
b"role" => MailboxProperty::Role,
|
||||
b"sortOrder" => MailboxProperty::SortOrder,
|
||||
b"totalEmails" => MailboxProperty::TotalEmails,
|
||||
b"unreadEmails" => MailboxProperty::UnreadEmails,
|
||||
b"totalThreads" => MailboxProperty::TotalThreads,
|
||||
b"unreadThreads" => MailboxProperty::UnreadThreads,
|
||||
b"shareWith" => MailboxProperty::ShareWith,
|
||||
b"myRights" => MailboxProperty::MyRights,
|
||||
b"mayReadItems" => MailboxProperty::Rights(MailboxRight::MayReadItems),
|
||||
b"mayAddItems" => MailboxProperty::Rights(MailboxRight::MayAddItems),
|
||||
b"mayRemoveItems" => MailboxProperty::Rights(MailboxRight::MayRemoveItems),
|
||||
b"maySetSeen" => MailboxProperty::Rights(MailboxRight::MaySetSeen),
|
||||
b"maySetKeywords" => MailboxProperty::Rights(MailboxRight::MaySetKeywords),
|
||||
b"mayCreateChild" => MailboxProperty::Rights(MailboxRight::MayCreateChild),
|
||||
b"mayRename" => MailboxProperty::Rights(MailboxRight::MayRename),
|
||||
b"maySubmit" => MailboxProperty::Rights(MailboxRight::MaySubmit),
|
||||
b"mayDelete" => MailboxProperty::Rights(MailboxRight::MayDelete),
|
||||
b"mayShare" => MailboxProperty::Rights(MailboxRight::MayShare),
|
||||
b"isSubscribed" => MailboxProperty::IsSubscribed,
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
MailboxProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &MailboxProperty {
|
||||
if let MailboxProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MailboxSetArguments {
|
||||
pub on_destroy_remove_emails: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MailboxQueryArguments {
|
||||
pub sort_as_tree: Option<bool>,
|
||||
pub filter_as_tree: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for MailboxSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "onDestroyRemoveEmails" {
|
||||
self.on_destroy_remove_emails = map.next_value()?;
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for MailboxQueryArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"sortAsTree" => {
|
||||
self.sort_as_tree = map.next_value()?;
|
||||
},
|
||||
b"filterAsTree" => {
|
||||
self.filter_as_tree = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MailboxProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
MailboxProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Mailbox {
|
||||
type Property = MailboxProperty;
|
||||
|
||||
type Element = MailboxValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = MailboxFilter;
|
||||
|
||||
type Comparator = MailboxComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = MailboxSetArguments;
|
||||
|
||||
type QueryArguments = MailboxQueryArguments;
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = MailboxProperty::Id;
|
||||
}
|
||||
|
||||
impl JmapSharedObject for Mailbox {
|
||||
type Right = MailboxRight;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property = MailboxProperty::ShareWith;
|
||||
}
|
||||
|
||||
impl From<Id> for MailboxProperty {
|
||||
fn from(id: Id) -> Self {
|
||||
MailboxProperty::IdValue(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MailboxProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: MailboxProperty) -> Result<Self, Self::Error> {
|
||||
if let MailboxProperty::IdValue(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MailboxProperty> for MailboxRight {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: MailboxProperty) -> Result<Self, Self::Error> {
|
||||
if let MailboxProperty::Rights(right) = value {
|
||||
Ok(right)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MailboxFilter {
|
||||
Name(String),
|
||||
ParentId(Option<MaybeIdReference<Id>>),
|
||||
Role(Option<SpecialUse>),
|
||||
HasAnyRole(bool),
|
||||
IsSubscribed(bool),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MailboxComparator {
|
||||
SortOrder,
|
||||
Name,
|
||||
ParentId,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for MailboxFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"name" => {
|
||||
*self = MailboxFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"parentId" => {
|
||||
*self = MailboxFilter::ParentId(map.next_value()?);
|
||||
},
|
||||
b"role" => {
|
||||
*self = MailboxFilter::Role(map.next_value::<Option<RoleWrapper>>()?.map(|r| r.0));
|
||||
},
|
||||
b"hasAnyRole" => {
|
||||
*self = MailboxFilter::HasAnyRole(map.next_value()?);
|
||||
},
|
||||
b"isSubscribed" => {
|
||||
*self = MailboxFilter::IsSubscribed(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = MailboxFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for MailboxComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"sortOrder" => {
|
||||
*self = MailboxComparator::SortOrder;
|
||||
},
|
||||
b"name" => {
|
||||
*self = MailboxComparator::Name;
|
||||
},
|
||||
b"parentId" => {
|
||||
*self = MailboxComparator::ParentId;
|
||||
},
|
||||
_ => {
|
||||
*self = MailboxComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MailboxFilter {
|
||||
fn default() -> Self {
|
||||
MailboxFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MailboxComparator {
|
||||
fn default() -> Self {
|
||||
MailboxComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct RoleWrapper(SpecialUse);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for RoleWrapper {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
SpecialUse::parse(<&str>::deserialize(deserializer)?)
|
||||
.map(RoleWrapper)
|
||||
.ok_or_else(|| serde::de::Error::custom("invalid JMAP role"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for MailboxValue {
|
||||
fn from(id: Id) -> Self {
|
||||
MailboxValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for MailboxValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let MailboxValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let MailboxValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let MailboxValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = MailboxValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapRight for MailboxRight {
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
match self {
|
||||
MailboxRight::MayReadItems => &[Acl::Read, Acl::ReadItems],
|
||||
MailboxRight::MayAddItems => &[Acl::AddItems],
|
||||
MailboxRight::MayRemoveItems => &[Acl::RemoveItems],
|
||||
MailboxRight::MaySetSeen => &[Acl::ModifyItems],
|
||||
MailboxRight::MaySetKeywords => &[Acl::ModifyItems],
|
||||
MailboxRight::MayCreateChild => &[Acl::CreateChild],
|
||||
MailboxRight::MayRename => &[Acl::Modify],
|
||||
MailboxRight::MaySubmit => &[Acl::Submit],
|
||||
MailboxRight::MayDelete => &[Acl::Delete],
|
||||
MailboxRight::MayShare => &[Acl::Share],
|
||||
}
|
||||
}
|
||||
|
||||
fn all_rights() -> &'static [Self] {
|
||||
&[
|
||||
MailboxRight::MayReadItems,
|
||||
MailboxRight::MayAddItems,
|
||||
MailboxRight::MayRemoveItems,
|
||||
MailboxRight::MaySetSeen,
|
||||
MailboxRight::MaySetKeywords,
|
||||
MailboxRight::MayCreateChild,
|
||||
MailboxRight::MayRename,
|
||||
MailboxRight::MaySubmit,
|
||||
MailboxRight::MayDelete,
|
||||
MailboxRight::MayShare,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MailboxRight> for MailboxProperty {
|
||||
fn from(right: MailboxRight) -> Self {
|
||||
MailboxProperty::Rights(right)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for MailboxProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let MailboxProperty::IdValue(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let MailboxProperty::IdValue(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = MailboxProperty::IdValue(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::request::deserialize::DeserializeArguments;
|
||||
use jmap_tools::{Element, Null, Property};
|
||||
use serde::Serialize;
|
||||
use std::{fmt::Debug, str::FromStr};
|
||||
use types::{acl::Acl, blob::BlobId, id::Id};
|
||||
|
||||
pub mod addressbook;
|
||||
pub mod blob;
|
||||
pub mod calendar;
|
||||
pub mod calendar_event;
|
||||
pub mod calendar_event_notification;
|
||||
pub mod contact;
|
||||
pub mod email;
|
||||
pub mod email_submission;
|
||||
pub mod file_node;
|
||||
pub mod identity;
|
||||
pub mod mailbox;
|
||||
pub mod participant_identity;
|
||||
pub mod principal;
|
||||
pub mod push_subscription;
|
||||
pub mod quota;
|
||||
pub mod registry;
|
||||
pub mod search_snippet;
|
||||
pub mod share_notification;
|
||||
pub mod sieve;
|
||||
pub mod thread;
|
||||
pub mod vacation_response;
|
||||
|
||||
pub trait JmapObject: std::fmt::Debug {
|
||||
type Property: Property + JmapObjectId + FromStr + Debug + Sync + Send;
|
||||
type Element: Element<Property = Self::Property> + JmapObjectId + Debug + Sync + Send;
|
||||
type Id: FromStr + TryFrom<AnyId> + Into<Self::Element> + Serialize + Debug + Sync + Send;
|
||||
|
||||
type Filter: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
|
||||
type GetArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
type SetArguments<'de>: Default + DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
type ParseArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send;
|
||||
|
||||
const ID_PROPERTY: Self::Property;
|
||||
}
|
||||
|
||||
pub trait JmapSharedObject: JmapObject {
|
||||
type Right: JmapRight + Into<Self::Property> + Debug + Clone + Copy + Sync + Send;
|
||||
|
||||
const SHARE_WITH_PROPERTY: Self::Property;
|
||||
}
|
||||
|
||||
pub trait JmapRight: Clone + Copy + Sized + 'static {
|
||||
fn all_rights() -> &'static [Self];
|
||||
fn to_acl(&self) -> &'static [Acl];
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AnyId {
|
||||
Id(Id),
|
||||
BlobId(BlobId),
|
||||
}
|
||||
|
||||
pub trait JmapObjectId {
|
||||
fn as_id(&self) -> Option<Id>;
|
||||
fn as_any_id(&self) -> Option<AnyId>;
|
||||
fn as_id_ref(&self) -> Option<&str>;
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum MaybeReference<T: FromStr> {
|
||||
Value(T),
|
||||
Reference(String),
|
||||
ParseError,
|
||||
}
|
||||
|
||||
fn parse_ref<T: FromStr>(value: &str) -> MaybeReference<T> {
|
||||
if let Some(reference) = value.strip_prefix('#') {
|
||||
MaybeReference::Reference(reference.to_string())
|
||||
} else {
|
||||
T::from_str(value)
|
||||
.map(MaybeReference::Value)
|
||||
.unwrap_or(MaybeReference::ParseError)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for AnyId {
|
||||
fn from(value: Id) -> Self {
|
||||
AnyId::Id(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobId> for AnyId {
|
||||
fn from(value: BlobId) -> Self {
|
||||
AnyId::BlobId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::Id(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for BlobId {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
|
||||
if let AnyId::BlobId(id) = value {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for AnyId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <&str>::deserialize(deserializer)?;
|
||||
if let Some(blob_id) = BlobId::from_base32(value) {
|
||||
Ok(AnyId::BlobId(blob_id))
|
||||
} else if let Ok(id) = Id::from_str(value) {
|
||||
Ok(AnyId::Id(id))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Invalid AnyId: {}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
|
||||
pub struct NullObject;
|
||||
|
||||
impl JmapObject for NullObject {
|
||||
type Property = Null;
|
||||
type Element = Null;
|
||||
type Id = Null;
|
||||
|
||||
type Filter = ();
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
type SetArguments<'de> = ();
|
||||
type QueryArguments = ();
|
||||
type CopyArguments = ();
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = Null;
|
||||
}
|
||||
|
||||
impl JmapRight for Null {
|
||||
fn all_rights() -> &'static [Self] {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn to_acl(&self) -> &'static [Acl] {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for NullObject {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for Null {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AnyId> for Null {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_: AnyId) -> Result<Self, Self::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::{deserialize::DeserializeArguments, reference::MaybeIdReference},
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParticipantIdentity;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ParticipantIdentityProperty {
|
||||
Id,
|
||||
Name,
|
||||
CalendarAddress,
|
||||
IsDefault,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ParticipantIdentityValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for ParticipantIdentityProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
ParticipantIdentityProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ParticipantIdentityProperty::Id => "id",
|
||||
ParticipantIdentityProperty::Name => "name",
|
||||
ParticipantIdentityProperty::CalendarAddress => "calendarAddress",
|
||||
ParticipantIdentityProperty::IsDefault => "isDefault",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ParticipantIdentityValue {
|
||||
type Property = ParticipantIdentityProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
ParticipantIdentityProperty::Id => {
|
||||
Id::from_str(value).ok().map(ParticipantIdentityValue::Id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ParticipantIdentityValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParticipantIdentityProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => ParticipantIdentityProperty::Id,
|
||||
b"name" => ParticipantIdentityProperty::Name,
|
||||
b"calendarAddress" => ParticipantIdentityProperty::CalendarAddress,
|
||||
b"isDefault" => ParticipantIdentityProperty::IsDefault
|
||||
)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ParticipantIdentityProperty::Id => "id",
|
||||
ParticipantIdentityProperty::Name => "name",
|
||||
ParticipantIdentityProperty::CalendarAddress => "calendarAddress",
|
||||
ParticipantIdentityProperty::IsDefault => "isDefault",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParticipantIdentitySetArguments {
|
||||
pub on_success_set_is_default: Option<MaybeIdReference<Id>>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ParticipantIdentitySetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onSuccessSetIsDefault" => {
|
||||
self.on_success_set_is_default = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ParticipantIdentityProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ParticipantIdentityProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for ParticipantIdentity {
|
||||
type Property = ParticipantIdentityProperty;
|
||||
|
||||
type Element = ParticipantIdentityValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ParticipantIdentitySetArguments;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = ParticipantIdentityProperty::Id;
|
||||
}
|
||||
|
||||
impl TryFrom<ParticipantIdentityProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_: ParticipantIdentityProperty) -> Result<Self, Self::Error> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for ParticipantIdentityValue {
|
||||
fn from(id: Id) -> Self {
|
||||
ParticipantIdentityValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ParticipantIdentityValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
let ParticipantIdentityValue::Id(id) = self;
|
||||
Some(*id)
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
let ParticipantIdentityValue::Id(id) = self;
|
||||
Some(AnyId::Id(*id))
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(new_id) = new_id {
|
||||
*self = ParticipantIdentityValue::Id(new_id);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ParticipantIdentityProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParticipantIdentityProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::{capability::Capability, deserialize::DeserializeArguments},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Principal;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PrincipalProperty {
|
||||
Id,
|
||||
Type,
|
||||
Name,
|
||||
Description,
|
||||
Email,
|
||||
Timezone,
|
||||
Capabilities,
|
||||
Accounts,
|
||||
IdValue(Id),
|
||||
Capability(Capability),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PrincipalValue {
|
||||
Id(Id),
|
||||
Type(PrincipalType),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PrincipalType {
|
||||
Individual,
|
||||
Group,
|
||||
Resource,
|
||||
Location,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Property for PrincipalProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
PrincipalProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
PrincipalProperty::Capabilities => "capabilities",
|
||||
PrincipalProperty::Description => "description",
|
||||
PrincipalProperty::Email => "email",
|
||||
PrincipalProperty::Id => "id",
|
||||
PrincipalProperty::Name => "name",
|
||||
PrincipalProperty::Timezone => "timezone",
|
||||
PrincipalProperty::Type => "type",
|
||||
PrincipalProperty::Accounts => "accounts",
|
||||
PrincipalProperty::Capability(cap) => cap.as_str(),
|
||||
PrincipalProperty::IdValue(id) => return id.to_string().into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for PrincipalValue {
|
||||
type Property = PrincipalProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
PrincipalProperty::Id => Id::from_str(value).ok().map(PrincipalValue::Id),
|
||||
PrincipalProperty::Type => PrincipalType::parse(value).map(PrincipalValue::Type),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
PrincipalValue::Id(id) => id.to_string().into(),
|
||||
PrincipalValue::Type(t) => t.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalProperty {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => PrincipalProperty::Id,
|
||||
b"type" => PrincipalProperty::Type,
|
||||
b"name" => PrincipalProperty::Name,
|
||||
b"description" => PrincipalProperty::Description,
|
||||
b"email" => PrincipalProperty::Email,
|
||||
b"timeZone" => PrincipalProperty::Timezone,
|
||||
b"capabilities" => PrincipalProperty::Capabilities,
|
||||
b"accounts" => PrincipalProperty::Accounts,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PrincipalProperty::Id => "id",
|
||||
PrincipalProperty::Type => "type",
|
||||
PrincipalProperty::Name => "name",
|
||||
PrincipalProperty::Description => "description",
|
||||
PrincipalProperty::Email => "email",
|
||||
PrincipalProperty::Timezone => "timeZone",
|
||||
PrincipalProperty::Capabilities => "capabilities",
|
||||
PrincipalProperty::Accounts => "accounts",
|
||||
PrincipalProperty::Capability(cap) => cap.as_str(),
|
||||
PrincipalProperty::IdValue(_) => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrincipalType {
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(s.as_bytes(),
|
||||
b"individual" => PrincipalType::Individual,
|
||||
b"group" => PrincipalType::Group,
|
||||
b"resource" => PrincipalType::Resource,
|
||||
b"location" => PrincipalType::Location,
|
||||
b"other" => PrincipalType::Other,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PrincipalType::Individual => "individual",
|
||||
PrincipalType::Group => "group",
|
||||
PrincipalType::Resource => "resource",
|
||||
PrincipalType::Location => "location",
|
||||
PrincipalType::Other => "other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrincipalProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
PrincipalProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Principal {
|
||||
type Property = PrincipalProperty;
|
||||
|
||||
type Element = PrincipalValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = PrincipalFilter;
|
||||
|
||||
type Comparator = PrincipalComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = PrincipalProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PrincipalFilter {
|
||||
AccountIds(Vec<Id>),
|
||||
Email(String),
|
||||
Name(String),
|
||||
Text(String),
|
||||
Type(PrincipalType),
|
||||
Timezone(String),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PrincipalComparator {
|
||||
Name,
|
||||
Email,
|
||||
Type,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for PrincipalFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"accountIds" => {
|
||||
*self = PrincipalFilter::AccountIds(map.next_value()?);
|
||||
},
|
||||
b"email" => {
|
||||
*self = PrincipalFilter::Email(map.next_value()?);
|
||||
},
|
||||
b"name" => {
|
||||
*self = PrincipalFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"text" => {
|
||||
*self = PrincipalFilter::Text(map.next_value()?);
|
||||
},
|
||||
b"type" => {
|
||||
*self = PrincipalFilter::Type(map.next_value()?);
|
||||
},
|
||||
b"timeZone" => {
|
||||
*self = PrincipalFilter::Timezone(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = PrincipalFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for PrincipalComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"name" => {
|
||||
*self = PrincipalComparator::Name;
|
||||
},
|
||||
b"email" => {
|
||||
*self = PrincipalComparator::Email;
|
||||
},
|
||||
b"type" => {
|
||||
*self = PrincipalComparator::Type;
|
||||
},
|
||||
_ => {
|
||||
*self = PrincipalComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrincipalFilter {
|
||||
fn default() -> Self {
|
||||
PrincipalFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrincipalComparator {
|
||||
fn default() -> Self {
|
||||
PrincipalComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PrincipalType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
PrincipalType::parse(<&str>::deserialize(deserializer)?)
|
||||
.ok_or_else(|| serde::de::Error::custom("invalid JMAP PrincipalType"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for PrincipalValue {
|
||||
fn from(id: Id) -> Self {
|
||||
PrincipalValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PrincipalValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let PrincipalValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let PrincipalValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = PrincipalValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrincipalFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
PrincipalFilter::AccountIds(_) => "accountIds",
|
||||
PrincipalFilter::Email(_) => "email",
|
||||
PrincipalFilter::Name(_) => "name",
|
||||
PrincipalFilter::Text(_) => "text",
|
||||
PrincipalFilter::Type(_) => "type",
|
||||
PrincipalFilter::Timezone(_) => "timezone",
|
||||
PrincipalFilter::_T(other) => other,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PrincipalProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrincipalProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::object::email::{EmailProperty, HeaderForm, HeaderProperty};
|
||||
use crate::object::{AnyId, JmapObject, JmapObjectId};
|
||||
use crate::types::date::UTCDate;
|
||||
use jmap_tools::{Element, JsonPointer, JsonPointerItem};
|
||||
use jmap_tools::{Key, Property};
|
||||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
use types::{id::Id, type_state::DataType};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PushSubscription;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PushSubscriptionProperty {
|
||||
Id,
|
||||
DeviceClientId,
|
||||
Url,
|
||||
Keys,
|
||||
P256dh,
|
||||
Auth,
|
||||
VerificationCode,
|
||||
Expires,
|
||||
Types,
|
||||
EmailPush,
|
||||
|
||||
// Other
|
||||
Pointer(JsonPointer<PushSubscriptionProperty>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PushSubscriptionValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
Types(DataType),
|
||||
}
|
||||
|
||||
impl Property for PushSubscriptionProperty {
|
||||
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
PushSubscriptionProperty::parse(value, key.is_none())
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
PushSubscriptionProperty::DeviceClientId => "deviceClientId",
|
||||
PushSubscriptionProperty::Expires => "expires",
|
||||
PushSubscriptionProperty::Id => "id",
|
||||
PushSubscriptionProperty::Keys => "keys",
|
||||
PushSubscriptionProperty::Types => "types",
|
||||
PushSubscriptionProperty::Url => "url",
|
||||
PushSubscriptionProperty::EmailPush => "emailPush",
|
||||
PushSubscriptionProperty::VerificationCode => "verificationCode",
|
||||
PushSubscriptionProperty::P256dh => "p256dh",
|
||||
PushSubscriptionProperty::Auth => "auth",
|
||||
PushSubscriptionProperty::Pointer(json_pointer) => {
|
||||
return json_pointer.to_string().into();
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl PushSubscriptionProperty {
|
||||
fn parse(value: &str, allow_patch: bool) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => PushSubscriptionProperty::Id,
|
||||
b"deviceClientId" => PushSubscriptionProperty::DeviceClientId,
|
||||
b"url" => PushSubscriptionProperty::Url,
|
||||
b"keys" => PushSubscriptionProperty::Keys,
|
||||
b"p256dh" => PushSubscriptionProperty::P256dh,
|
||||
b"auth" => PushSubscriptionProperty::Auth,
|
||||
b"verificationCode" => PushSubscriptionProperty::VerificationCode,
|
||||
b"expires" => PushSubscriptionProperty::Expires,
|
||||
b"types" => PushSubscriptionProperty::Types,
|
||||
b"emailPush" => PushSubscriptionProperty::EmailPush,
|
||||
)
|
||||
.or_else(|| {
|
||||
if allow_patch && value.contains('/') {
|
||||
PushSubscriptionProperty::Pointer(JsonPointer::parse(value)).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn patch_or_prop(&self) -> &PushSubscriptionProperty {
|
||||
if let PushSubscriptionProperty::Pointer(ptr) = self
|
||||
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
|
||||
{
|
||||
prop
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for PushSubscriptionValue {
|
||||
type Property = PushSubscriptionProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop.patch_or_prop() {
|
||||
PushSubscriptionProperty::Id => {
|
||||
Id::from_str(value).ok().map(PushSubscriptionValue::Id)
|
||||
}
|
||||
PushSubscriptionProperty::Types => {
|
||||
DataType::parse(value).map(PushSubscriptionValue::Types)
|
||||
}
|
||||
PushSubscriptionProperty::Expires => UTCDate::from_str(value)
|
||||
.ok()
|
||||
.map(PushSubscriptionValue::Date),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
PushSubscriptionValue::Id(id) => id.to_string().into(),
|
||||
PushSubscriptionValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
PushSubscriptionValue::Types(data_type) => data_type.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PushSubscriptionProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
PushSubscriptionProperty::parse(s, false).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EmailPushProperty {
|
||||
Id,
|
||||
BlobId,
|
||||
ThreadId,
|
||||
MailboxIds,
|
||||
Keywords,
|
||||
Size,
|
||||
ReceivedAt,
|
||||
MessageId,
|
||||
InReplyTo,
|
||||
References,
|
||||
Sender,
|
||||
From,
|
||||
To,
|
||||
Cc,
|
||||
Bcc,
|
||||
ReplyTo,
|
||||
Subject,
|
||||
SentAt,
|
||||
Preview,
|
||||
HasAttachment,
|
||||
BodyStructure,
|
||||
BodyValues,
|
||||
TextBody,
|
||||
HtmlBody,
|
||||
Attachments,
|
||||
Headers,
|
||||
Header(EmailPushHeaderProperty),
|
||||
}
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EmailPushHeaderProperty {
|
||||
pub form: EmailPushHeaderForm,
|
||||
pub header: String,
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, Copy, PartialEq, Eq, Default,
|
||||
)]
|
||||
#[rkyv(compare(PartialEq), derive(Debug))]
|
||||
#[repr(u8)]
|
||||
pub enum EmailPushHeaderForm {
|
||||
#[default]
|
||||
Raw = 0,
|
||||
Text = 1,
|
||||
Addresses = 2,
|
||||
GroupedAddresses = 3,
|
||||
MessageIds = 4,
|
||||
Date = 5,
|
||||
Urls = 6,
|
||||
}
|
||||
|
||||
impl TryFrom<&EmailProperty> for EmailPushProperty {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &EmailProperty) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
EmailProperty::Id => EmailPushProperty::Id,
|
||||
EmailProperty::BlobId => EmailPushProperty::BlobId,
|
||||
EmailProperty::ThreadId => EmailPushProperty::ThreadId,
|
||||
EmailProperty::MailboxIds => EmailPushProperty::MailboxIds,
|
||||
EmailProperty::Keywords => EmailPushProperty::Keywords,
|
||||
EmailProperty::Size => EmailPushProperty::Size,
|
||||
EmailProperty::ReceivedAt => EmailPushProperty::ReceivedAt,
|
||||
EmailProperty::MessageId => EmailPushProperty::MessageId,
|
||||
EmailProperty::InReplyTo => EmailPushProperty::InReplyTo,
|
||||
EmailProperty::References => EmailPushProperty::References,
|
||||
EmailProperty::Sender => EmailPushProperty::Sender,
|
||||
EmailProperty::From => EmailPushProperty::From,
|
||||
EmailProperty::To => EmailPushProperty::To,
|
||||
EmailProperty::Cc => EmailPushProperty::Cc,
|
||||
EmailProperty::Bcc => EmailPushProperty::Bcc,
|
||||
EmailProperty::ReplyTo => EmailPushProperty::ReplyTo,
|
||||
EmailProperty::Subject => EmailPushProperty::Subject,
|
||||
EmailProperty::SentAt => EmailPushProperty::SentAt,
|
||||
EmailProperty::Preview => EmailPushProperty::Preview,
|
||||
EmailProperty::HasAttachment => EmailPushProperty::HasAttachment,
|
||||
EmailProperty::BodyStructure => EmailPushProperty::BodyStructure,
|
||||
EmailProperty::BodyValues => EmailPushProperty::BodyValues,
|
||||
EmailProperty::TextBody => EmailPushProperty::TextBody,
|
||||
EmailProperty::HtmlBody => EmailPushProperty::HtmlBody,
|
||||
EmailProperty::Attachments => EmailPushProperty::Attachments,
|
||||
EmailProperty::Headers => EmailPushProperty::Headers,
|
||||
EmailProperty::Header(header) => EmailPushProperty::Header(EmailPushHeaderProperty {
|
||||
form: (&header.form).into(),
|
||||
header: header.header.clone(),
|
||||
all: header.all,
|
||||
}),
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EmailPushProperty> for EmailProperty {
|
||||
fn from(value: &EmailPushProperty) -> Self {
|
||||
match value {
|
||||
EmailPushProperty::Id => EmailProperty::Id,
|
||||
EmailPushProperty::BlobId => EmailProperty::BlobId,
|
||||
EmailPushProperty::ThreadId => EmailProperty::ThreadId,
|
||||
EmailPushProperty::MailboxIds => EmailProperty::MailboxIds,
|
||||
EmailPushProperty::Keywords => EmailProperty::Keywords,
|
||||
EmailPushProperty::Size => EmailProperty::Size,
|
||||
EmailPushProperty::ReceivedAt => EmailProperty::ReceivedAt,
|
||||
EmailPushProperty::MessageId => EmailProperty::MessageId,
|
||||
EmailPushProperty::InReplyTo => EmailProperty::InReplyTo,
|
||||
EmailPushProperty::References => EmailProperty::References,
|
||||
EmailPushProperty::Sender => EmailProperty::Sender,
|
||||
EmailPushProperty::From => EmailProperty::From,
|
||||
EmailPushProperty::To => EmailProperty::To,
|
||||
EmailPushProperty::Cc => EmailProperty::Cc,
|
||||
EmailPushProperty::Bcc => EmailProperty::Bcc,
|
||||
EmailPushProperty::ReplyTo => EmailProperty::ReplyTo,
|
||||
EmailPushProperty::Subject => EmailProperty::Subject,
|
||||
EmailPushProperty::SentAt => EmailProperty::SentAt,
|
||||
EmailPushProperty::Preview => EmailProperty::Preview,
|
||||
EmailPushProperty::HasAttachment => EmailProperty::HasAttachment,
|
||||
EmailPushProperty::BodyStructure => EmailProperty::BodyStructure,
|
||||
EmailPushProperty::BodyValues => EmailProperty::BodyValues,
|
||||
EmailPushProperty::TextBody => EmailProperty::TextBody,
|
||||
EmailPushProperty::HtmlBody => EmailProperty::HtmlBody,
|
||||
EmailPushProperty::Attachments => EmailProperty::Attachments,
|
||||
EmailPushProperty::Headers => EmailProperty::Headers,
|
||||
EmailPushProperty::Header(header) => EmailProperty::Header(HeaderProperty {
|
||||
form: (&header.form).into(),
|
||||
header: header.header.clone(),
|
||||
all: header.all,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArchivedEmailPushProperty> for EmailProperty {
|
||||
fn from(value: &ArchivedEmailPushProperty) -> Self {
|
||||
match value {
|
||||
ArchivedEmailPushProperty::Id => EmailProperty::Id,
|
||||
ArchivedEmailPushProperty::BlobId => EmailProperty::BlobId,
|
||||
ArchivedEmailPushProperty::ThreadId => EmailProperty::ThreadId,
|
||||
ArchivedEmailPushProperty::MailboxIds => EmailProperty::MailboxIds,
|
||||
ArchivedEmailPushProperty::Keywords => EmailProperty::Keywords,
|
||||
ArchivedEmailPushProperty::Size => EmailProperty::Size,
|
||||
ArchivedEmailPushProperty::ReceivedAt => EmailProperty::ReceivedAt,
|
||||
ArchivedEmailPushProperty::MessageId => EmailProperty::MessageId,
|
||||
ArchivedEmailPushProperty::InReplyTo => EmailProperty::InReplyTo,
|
||||
ArchivedEmailPushProperty::References => EmailProperty::References,
|
||||
ArchivedEmailPushProperty::Sender => EmailProperty::Sender,
|
||||
ArchivedEmailPushProperty::From => EmailProperty::From,
|
||||
ArchivedEmailPushProperty::To => EmailProperty::To,
|
||||
ArchivedEmailPushProperty::Cc => EmailProperty::Cc,
|
||||
ArchivedEmailPushProperty::Bcc => EmailProperty::Bcc,
|
||||
ArchivedEmailPushProperty::ReplyTo => EmailProperty::ReplyTo,
|
||||
ArchivedEmailPushProperty::Subject => EmailProperty::Subject,
|
||||
ArchivedEmailPushProperty::SentAt => EmailProperty::SentAt,
|
||||
ArchivedEmailPushProperty::Preview => EmailProperty::Preview,
|
||||
ArchivedEmailPushProperty::HasAttachment => EmailProperty::HasAttachment,
|
||||
ArchivedEmailPushProperty::BodyStructure => EmailProperty::BodyStructure,
|
||||
ArchivedEmailPushProperty::BodyValues => EmailProperty::BodyValues,
|
||||
ArchivedEmailPushProperty::TextBody => EmailProperty::TextBody,
|
||||
ArchivedEmailPushProperty::HtmlBody => EmailProperty::HtmlBody,
|
||||
ArchivedEmailPushProperty::Attachments => EmailProperty::Attachments,
|
||||
ArchivedEmailPushProperty::Headers => EmailProperty::Headers,
|
||||
ArchivedEmailPushProperty::Header(header) => EmailProperty::Header(HeaderProperty {
|
||||
form: (&header.form).into(),
|
||||
header: header.header.as_str().to_string(),
|
||||
all: header.all,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArchivedEmailPushHeaderForm> for HeaderForm {
|
||||
fn from(value: &ArchivedEmailPushHeaderForm) -> Self {
|
||||
match value {
|
||||
ArchivedEmailPushHeaderForm::Raw => HeaderForm::Raw,
|
||||
ArchivedEmailPushHeaderForm::Text => HeaderForm::Text,
|
||||
ArchivedEmailPushHeaderForm::Addresses => HeaderForm::Addresses,
|
||||
ArchivedEmailPushHeaderForm::GroupedAddresses => HeaderForm::GroupedAddresses,
|
||||
ArchivedEmailPushHeaderForm::MessageIds => HeaderForm::MessageIds,
|
||||
ArchivedEmailPushHeaderForm::Date => HeaderForm::Date,
|
||||
ArchivedEmailPushHeaderForm::Urls => HeaderForm::URLs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HeaderForm> for EmailPushHeaderForm {
|
||||
fn from(value: &HeaderForm) -> Self {
|
||||
match value {
|
||||
HeaderForm::Raw => EmailPushHeaderForm::Raw,
|
||||
HeaderForm::Text => EmailPushHeaderForm::Text,
|
||||
HeaderForm::Addresses => EmailPushHeaderForm::Addresses,
|
||||
HeaderForm::GroupedAddresses => EmailPushHeaderForm::GroupedAddresses,
|
||||
HeaderForm::MessageIds => EmailPushHeaderForm::MessageIds,
|
||||
HeaderForm::Date => EmailPushHeaderForm::Date,
|
||||
HeaderForm::URLs => EmailPushHeaderForm::Urls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EmailPushHeaderForm> for HeaderForm {
|
||||
fn from(value: &EmailPushHeaderForm) -> Self {
|
||||
match value {
|
||||
EmailPushHeaderForm::Raw => HeaderForm::Raw,
|
||||
EmailPushHeaderForm::Text => HeaderForm::Text,
|
||||
EmailPushHeaderForm::Addresses => HeaderForm::Addresses,
|
||||
EmailPushHeaderForm::GroupedAddresses => HeaderForm::GroupedAddresses,
|
||||
EmailPushHeaderForm::MessageIds => HeaderForm::MessageIds,
|
||||
EmailPushHeaderForm::Date => HeaderForm::Date,
|
||||
EmailPushHeaderForm::Urls => HeaderForm::URLs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for PushSubscription {
|
||||
type Property = PushSubscriptionProperty;
|
||||
|
||||
type Element = PushSubscriptionValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = PushSubscriptionProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for PushSubscriptionValue {
|
||||
fn from(id: Id) -> Self {
|
||||
PushSubscriptionValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PushSubscriptionValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
PushSubscriptionValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
PushSubscriptionValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = PushSubscriptionValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for PushSubscriptionProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::deserialize::DeserializeArguments,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Quota;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum QuotaProperty {
|
||||
Id,
|
||||
ResourceType,
|
||||
Used,
|
||||
Name,
|
||||
Scope,
|
||||
Types,
|
||||
HardLimit,
|
||||
WarnLimit,
|
||||
SoftLimit,
|
||||
Description,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum QuotaValue {
|
||||
Id(Id),
|
||||
Types(DataType),
|
||||
}
|
||||
|
||||
impl Property for QuotaProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
QuotaProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
QuotaProperty::Description => "description",
|
||||
QuotaProperty::Id => "id",
|
||||
QuotaProperty::Name => "name",
|
||||
QuotaProperty::Types => "types",
|
||||
QuotaProperty::ResourceType => "resourceType",
|
||||
QuotaProperty::Used => "used",
|
||||
QuotaProperty::HardLimit => "hardLimit",
|
||||
QuotaProperty::Scope => "scope",
|
||||
QuotaProperty::WarnLimit => "warnLimit",
|
||||
QuotaProperty::SoftLimit => "softLimit",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl QuotaProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => QuotaProperty::Id,
|
||||
b"resourceType" => QuotaProperty::ResourceType,
|
||||
b"used" => QuotaProperty::Used,
|
||||
b"name" => QuotaProperty::Name,
|
||||
b"scope" => QuotaProperty::Scope,
|
||||
b"types" => QuotaProperty::Types,
|
||||
b"hardLimit" => QuotaProperty::HardLimit,
|
||||
b"warnLimit" => QuotaProperty::WarnLimit,
|
||||
b"softLimit" => QuotaProperty::SoftLimit,
|
||||
b"description" => QuotaProperty::Description,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for QuotaValue {
|
||||
type Property = QuotaProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
QuotaProperty::Id => Id::from_str(value).ok().map(QuotaValue::Id),
|
||||
QuotaProperty::Types => DataType::parse(value).map(QuotaValue::Types),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
QuotaValue::Id(id) => id.to_string().into(),
|
||||
QuotaValue::Types(data_type) => data_type.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for QuotaProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
QuotaProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Quota {
|
||||
type Property = QuotaProperty;
|
||||
|
||||
type Element = QuotaValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = QuotaFilter;
|
||||
|
||||
type Comparator = QuotaComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = QuotaProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum QuotaFilter {
|
||||
Name(String),
|
||||
Type(String),
|
||||
Scope(String),
|
||||
ResourceType(String),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum QuotaComparator {
|
||||
Name,
|
||||
Type,
|
||||
Used,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for QuotaFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"name" => {
|
||||
*self = QuotaFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"type" => {
|
||||
*self = QuotaFilter::Type(map.next_value()?);
|
||||
},
|
||||
b"scope" => {
|
||||
*self = QuotaFilter::Scope(map.next_value()?);
|
||||
},
|
||||
b"resourceType" => {
|
||||
*self = QuotaFilter::ResourceType(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = QuotaFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for QuotaComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"name" => {
|
||||
*self = QuotaComparator::Name;
|
||||
},
|
||||
b"type" => {
|
||||
*self = QuotaComparator::Type;
|
||||
},
|
||||
b"used" => {
|
||||
*self = QuotaComparator::Used;
|
||||
},
|
||||
_ => {
|
||||
*self = QuotaComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QuotaFilter {
|
||||
fn default() -> Self {
|
||||
QuotaFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QuotaComparator {
|
||||
fn default() -> Self {
|
||||
QuotaComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for QuotaValue {
|
||||
fn from(id: Id) -> Self {
|
||||
QuotaValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for QuotaValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let QuotaValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
self.as_id().map(AnyId::Id)
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = QuotaValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for QuotaProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::deserialize::DeserializeArguments,
|
||||
};
|
||||
use registry::{jmap::RegistryValue, schema::prelude::Property, types::EnumImpl};
|
||||
use std::borrow::Cow;
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RegistryFilter {
|
||||
Property {
|
||||
property: Property,
|
||||
operator: RegistryFilterOperator,
|
||||
value: serde_json::Value,
|
||||
},
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegistryFilterOperator {
|
||||
Equal,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RegistryComparator {
|
||||
Property(Property),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl JmapObject for Registry {
|
||||
type Property = Property;
|
||||
|
||||
type Element = RegistryValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = RegistryFilter;
|
||||
|
||||
type Comparator = RegistryComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = Property::Id;
|
||||
}
|
||||
|
||||
impl JmapObjectId for Property {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<super::AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: super::AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for RegistryValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let RegistryValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
RegistryValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
RegistryValue::BlobId(id) => Some(AnyId::BlobId(id.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let RegistryValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = RegistryValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(id) => {
|
||||
*self = RegistryValue::BlobId(id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for RegistryFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if let Some(property) = Property::parse(key) {
|
||||
let value = map.next_value()?;
|
||||
*self = RegistryFilter::Property {
|
||||
property,
|
||||
operator: RegistryFilterOperator::Equal,
|
||||
value,
|
||||
};
|
||||
return Ok(());
|
||||
} else if let Some((property, operator)) = key.rsplit_once("Is")
|
||||
&& let (Some(property), Some(operator)) = (
|
||||
Property::parse(property),
|
||||
RegistryFilterOperator::parse(operator),
|
||||
)
|
||||
{
|
||||
let value = map.next_value()?;
|
||||
*self = RegistryFilter::Property {
|
||||
property,
|
||||
operator,
|
||||
value,
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*self = RegistryFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for RegistryComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
|
||||
if let Some(property) = Property::parse(value.as_ref()) {
|
||||
*self = RegistryComparator::Property(property);
|
||||
} else {
|
||||
*self = RegistryComparator::_T(value.into_owned());
|
||||
}
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryFilterOperator {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"GreaterThan" => RegistryFilterOperator::GreaterThan,
|
||||
b"GreaterThanOrEqual" => RegistryFilterOperator::GreaterThanOrEqual,
|
||||
b"LessThan" => RegistryFilterOperator::LessThan,
|
||||
b"LessThanOrEqual" => RegistryFilterOperator::LessThanOrEqual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegistryFilter {
|
||||
fn default() -> Self {
|
||||
RegistryFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegistryComparator {
|
||||
fn default() -> Self {
|
||||
RegistryComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SearchSnippet;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SearchSnippetProperty {
|
||||
EmailId,
|
||||
Subject,
|
||||
Preview,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SearchSnippetValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for SearchSnippetProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
SearchSnippetProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
SearchSnippetProperty::Preview => "preview",
|
||||
SearchSnippetProperty::Subject => "subject",
|
||||
SearchSnippetProperty::EmailId => "emailId",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SearchSnippetValue {
|
||||
type Property = SearchSnippetProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
SearchSnippetProperty::EmailId => {
|
||||
Id::from_str(value).ok().map(SearchSnippetValue::Id)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
SearchSnippetValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchSnippetProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"emailId" => SearchSnippetProperty::EmailId,
|
||||
b"subject" => SearchSnippetProperty::Subject,
|
||||
b"preview" => SearchSnippetProperty::Preview,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
request::deserialize::DeserializeArguments,
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ShareNotification;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ShareNotificationProperty {
|
||||
Id,
|
||||
Created,
|
||||
ChangedBy,
|
||||
ChangedByName,
|
||||
ChangedByEmail,
|
||||
ChangedByPrincipalId,
|
||||
ObjectType,
|
||||
ObjectAccountId,
|
||||
ObjectId,
|
||||
OldRights,
|
||||
NewRights,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ShareNotificationValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
ObjectType(DataType),
|
||||
}
|
||||
|
||||
impl Property for ShareNotificationProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
ShareNotificationProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ShareNotificationProperty::Id => "id",
|
||||
ShareNotificationProperty::Created => "created",
|
||||
ShareNotificationProperty::ChangedBy => "changedBy",
|
||||
ShareNotificationProperty::ChangedByName => "name",
|
||||
ShareNotificationProperty::ChangedByEmail => "email",
|
||||
ShareNotificationProperty::ChangedByPrincipalId => "principalId",
|
||||
ShareNotificationProperty::ObjectType => "objectType",
|
||||
ShareNotificationProperty::ObjectAccountId => "objectAccountId",
|
||||
ShareNotificationProperty::ObjectId => "objectId",
|
||||
ShareNotificationProperty::OldRights => "oldRights",
|
||||
ShareNotificationProperty::NewRights => "newRights",
|
||||
ShareNotificationProperty::Name => "name",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ShareNotificationValue {
|
||||
type Property = ShareNotificationProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
ShareNotificationProperty::Id
|
||||
| ShareNotificationProperty::ChangedByPrincipalId
|
||||
| ShareNotificationProperty::ObjectAccountId
|
||||
| ShareNotificationProperty::ObjectId => {
|
||||
Id::from_str(value).ok().map(ShareNotificationValue::Id)
|
||||
}
|
||||
ShareNotificationProperty::Created => UTCDate::from_str(value)
|
||||
.ok()
|
||||
.map(ShareNotificationValue::Date),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ShareNotificationValue::Id(id) => id.to_string().into(),
|
||||
ShareNotificationValue::Date(date) => date.to_string().into(),
|
||||
ShareNotificationValue::ObjectType(ty) => ty.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShareNotificationProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => ShareNotificationProperty::Id,
|
||||
b"created" => ShareNotificationProperty::Created,
|
||||
b"changedBy" => ShareNotificationProperty::ChangedBy,
|
||||
b"name" => ShareNotificationProperty::ChangedByName,
|
||||
b"email" => ShareNotificationProperty::ChangedByEmail,
|
||||
b"principalId" => ShareNotificationProperty::ChangedByPrincipalId,
|
||||
b"objectType" => ShareNotificationProperty::ObjectType,
|
||||
b"objectAccountId" => ShareNotificationProperty::ObjectAccountId,
|
||||
b"objectId" => ShareNotificationProperty::ObjectId,
|
||||
b"oldRights" => ShareNotificationProperty::OldRights,
|
||||
b"newRights" => ShareNotificationProperty::NewRights
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ShareNotificationProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ShareNotificationProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for ShareNotification {
|
||||
type Property = ShareNotificationProperty;
|
||||
|
||||
type Element = ShareNotificationValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ShareNotificationFilter;
|
||||
|
||||
type Comparator = ShareNotificationComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = ShareNotificationProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ShareNotificationFilter {
|
||||
After(UTCDate),
|
||||
Before(UTCDate),
|
||||
ObjectType(DataType),
|
||||
ObjectAccountId(Id),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ShareNotificationComparator {
|
||||
Created,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ShareNotificationFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"after" => {
|
||||
*self = ShareNotificationFilter::After(map.next_value()?);
|
||||
},
|
||||
b"before" => {
|
||||
*self = ShareNotificationFilter::Before(map.next_value()?);
|
||||
},
|
||||
b"objectType" => {
|
||||
*self = ShareNotificationFilter::ObjectType(map.next_value()?);
|
||||
},
|
||||
b"objectAccountId" => {
|
||||
*self = ShareNotificationFilter::ObjectAccountId(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = ShareNotificationFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for ShareNotificationComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"created" => {
|
||||
*self = ShareNotificationComparator::Created;
|
||||
},
|
||||
_ => {
|
||||
*self = ShareNotificationComparator::_T(value.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ShareNotificationFilter {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ShareNotificationFilter::After(_) => "after",
|
||||
ShareNotificationFilter::Before(_) => "before",
|
||||
ShareNotificationFilter::ObjectType(_) => "objectType",
|
||||
ShareNotificationFilter::ObjectAccountId(_) => "objectAccountId",
|
||||
ShareNotificationFilter::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShareNotificationComparator {
|
||||
pub fn into_string(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ShareNotificationComparator::Created => "created",
|
||||
ShareNotificationComparator::_T(s) => return Cow::Owned(s),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShareNotificationFilter {
|
||||
fn default() -> Self {
|
||||
ShareNotificationFilter::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShareNotificationComparator {
|
||||
fn default() -> Self {
|
||||
ShareNotificationComparator::_T(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ShareNotificationProperty> for Id {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(_: ShareNotificationProperty) -> Result<Self, Self::Error> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for ShareNotificationValue {
|
||||
fn from(id: Id) -> Self {
|
||||
ShareNotificationValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ShareNotificationValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
if let ShareNotificationValue::Id(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
if let ShareNotificationValue::Id(id) = self {
|
||||
Some(AnyId::Id(*id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ShareNotificationProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ShareNotificationProperty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_cow())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, DeserializeArguments, JmapObject, JmapObjectId, MaybeReference, parse_ref},
|
||||
request::reference::MaybeIdReference,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Sieve;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SieveProperty {
|
||||
Id,
|
||||
Name,
|
||||
BlobId,
|
||||
IsActive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SieveValue {
|
||||
Id(Id),
|
||||
BlobId(BlobId),
|
||||
IdReference(String),
|
||||
}
|
||||
|
||||
impl Property for SieveProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
SieveProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
SieveProperty::BlobId => "blobId",
|
||||
SieveProperty::Id => "id",
|
||||
SieveProperty::Name => "name",
|
||||
SieveProperty::IsActive => "isActive",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SieveValue {
|
||||
type Property = SieveProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
SieveProperty::Id => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(SieveValue::Id(v)),
|
||||
MaybeReference::Reference(v) => Some(SieveValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
SieveProperty::BlobId => match parse_ref(value) {
|
||||
MaybeReference::Value(v) => Some(SieveValue::BlobId(v)),
|
||||
MaybeReference::Reference(v) => Some(SieveValue::IdReference(v)),
|
||||
MaybeReference::ParseError => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
SieveValue::Id(id) => id.to_string().into(),
|
||||
SieveValue::BlobId(blob_id) => blob_id.to_string().into(),
|
||||
SieveValue::IdReference(r) => format!("#{r}").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SieveProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => SieveProperty::Id,
|
||||
b"name" => SieveProperty::Name,
|
||||
b"blobId" => SieveProperty::BlobId,
|
||||
b"isActive" => SieveProperty::IsActive,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SieveSetArguments {
|
||||
pub on_success_activate_script: Option<MaybeIdReference<Id>>,
|
||||
pub on_success_deactivate_script: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for SieveSetArguments {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"onSuccessActivateScript" => {
|
||||
self.on_success_activate_script = map.next_value()?;
|
||||
},
|
||||
b"onSuccessDeactivateScript" => {
|
||||
self.on_success_deactivate_script = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SieveProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
SieveProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Sieve {
|
||||
type Property = SieveProperty;
|
||||
|
||||
type Element = SieveValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = SieveFilter;
|
||||
|
||||
type Comparator = SieveComparator;
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = SieveSetArguments;
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = SieveProperty::Id;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SieveFilter {
|
||||
Name(String),
|
||||
IsActive(bool),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SieveComparator {
|
||||
Name,
|
||||
IsActive,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for SieveFilter {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"name" => {
|
||||
*self = SieveFilter::Name(map.next_value()?);
|
||||
},
|
||||
b"isActive" => {
|
||||
*self = SieveFilter::IsActive(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
*self = SieveFilter::_T(key.to_string());
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for SieveComparator {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
if key == "property" {
|
||||
let value = map.next_value::<Cow<str>>()?;
|
||||
hashify::fnc_map!(value.as_bytes(),
|
||||
b"name" => {
|
||||
*self = SieveComparator::Name;
|
||||
},
|
||||
b"isActive" => {
|
||||
*self = SieveComparator::IsActive;
|
||||
},
|
||||
_ => {
|
||||
*self = SieveComparator::_T(key.to_string());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveFilter {
|
||||
fn default() -> Self {
|
||||
SieveFilter::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveComparator {
|
||||
fn default() -> Self {
|
||||
SieveComparator::_T("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for SieveValue {
|
||||
fn from(id: Id) -> Self {
|
||||
SieveValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for SieveValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
SieveValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
SieveValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
SieveValue::BlobId(id) => Some(AnyId::BlobId(id.clone())),
|
||||
SieveValue::IdReference(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
if let SieveValue::IdReference(r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
match new_id {
|
||||
AnyId::Id(id) => {
|
||||
*self = SieveValue::Id(id);
|
||||
}
|
||||
AnyId::BlobId(id) => {
|
||||
*self = SieveValue::BlobId(id);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for SieveProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
use crate::object::{AnyId, JmapObject, JmapObjectId};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Thread;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ThreadProperty {
|
||||
Id,
|
||||
EmailIds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ThreadValue {
|
||||
Id(Id),
|
||||
}
|
||||
|
||||
impl Property for ThreadProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
ThreadProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ThreadProperty::Id => "id",
|
||||
ThreadProperty::EmailIds => "emailIds",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ThreadValue {
|
||||
type Property = ThreadProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(_) = key {
|
||||
Id::from_str(value).ok().map(ThreadValue::Id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
ThreadValue::Id(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => ThreadProperty::Id,
|
||||
b"emailIds" => ThreadProperty::EmailIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ThreadProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ThreadProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for Thread {
|
||||
type Property = ThreadProperty;
|
||||
|
||||
type Element = ThreadValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = ThreadProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for ThreadValue {
|
||||
fn from(id: Id) -> Self {
|
||||
ThreadValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ThreadValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
ThreadValue::Id(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
self.as_id().map(AnyId::Id)
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = ThreadValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for ThreadProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct VacationResponse;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum VacationResponseProperty {
|
||||
Id,
|
||||
IsEnabled,
|
||||
FromDate,
|
||||
ToDate,
|
||||
Subject,
|
||||
TextBody,
|
||||
HtmlBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum VacationResponseValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
}
|
||||
|
||||
impl Property for VacationResponseProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
VacationResponseProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
VacationResponseProperty::HtmlBody => "htmlBody",
|
||||
VacationResponseProperty::Id => "id",
|
||||
VacationResponseProperty::TextBody => "textBody",
|
||||
VacationResponseProperty::FromDate => "fromDate",
|
||||
VacationResponseProperty::IsEnabled => "isEnabled",
|
||||
VacationResponseProperty::ToDate => "toDate",
|
||||
VacationResponseProperty::Subject => "subject",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for VacationResponseValue {
|
||||
type Property = VacationResponseProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
if let Key::Property(prop) = key {
|
||||
match prop {
|
||||
VacationResponseProperty::Id => {
|
||||
Id::from_str(value).ok().map(VacationResponseValue::Id)
|
||||
}
|
||||
VacationResponseProperty::FromDate | VacationResponseProperty::ToDate => {
|
||||
UTCDate::from_str(value)
|
||||
.ok()
|
||||
.map(VacationResponseValue::Date)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
VacationResponseValue::Id(id) => id.to_string().into(),
|
||||
VacationResponseValue::Date(utcdate) => utcdate.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VacationResponseProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => VacationResponseProperty::Id,
|
||||
b"isEnabled" => VacationResponseProperty::IsEnabled,
|
||||
b"fromDate" => VacationResponseProperty::FromDate,
|
||||
b"toDate" => VacationResponseProperty::ToDate,
|
||||
b"textBody" => VacationResponseProperty::TextBody,
|
||||
b"htmlBody" => VacationResponseProperty::HtmlBody,
|
||||
b"subject" => VacationResponseProperty::Subject,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for VacationResponseProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
VacationResponseProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for VacationResponse {
|
||||
type Property = VacationResponseProperty;
|
||||
|
||||
type Element = VacationResponseValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = VacationResponseProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for VacationResponseValue {
|
||||
fn from(id: Id) -> Self {
|
||||
VacationResponseValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for VacationResponseValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
VacationResponseValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
VacationResponseValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = VacationResponseValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for VacationResponseProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObjectId},
|
||||
references::{
|
||||
Graph,
|
||||
jsptr::{EvalResults, ResponsePtr},
|
||||
},
|
||||
request::reference::ResultReference,
|
||||
response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod},
|
||||
};
|
||||
use compact_str::format_compact;
|
||||
use jmap_tools::{Element, Key, Property, Value};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
impl Response<'_> {
|
||||
pub(crate) fn eval_result_references(&self, rr: &ResultReference) -> trc::Result<EvalResults> {
|
||||
let mut results = EvalResults::default();
|
||||
|
||||
for response in &self.method_responses {
|
||||
if response.id == rr.result_of && response.name == rr.name {
|
||||
let path = rr.path.iter();
|
||||
let success = match &response.method {
|
||||
ResponseMethod::Get(response) => match response {
|
||||
GetResponseMethod::Email(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Mailbox(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Thread(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Identity(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::EmailSubmission(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::PushSubscription(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Sieve(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::VacationResponse(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Principal(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Quota(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Blob(response) => response.eval_jptr(path, &mut results),
|
||||
GetResponseMethod::AddressBook(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::ContactCard(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::FileNode(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Calendar(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::CalendarEvent(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::CalendarEventNotification(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::ParticipantIdentity(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::ShareNotification(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::PrincipalAvailability(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Registry(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
},
|
||||
ResponseMethod::Changes(response) => match response {
|
||||
ChangesResponseMethod::Email(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::Mailbox(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::Thread(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::Identity(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::EmailSubmission(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::Quota(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::AddressBook(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::ContactCard(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::FileNode(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::Calendar(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::CalendarEvent(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::CalendarEventNotification(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
ChangesResponseMethod::ShareNotification(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
},
|
||||
ResponseMethod::Query(response) => response.eval_jptr(path, &mut results),
|
||||
ResponseMethod::QueryChanges(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if success {
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Result reference to {}#{} not found.",
|
||||
rr.result_of,
|
||||
rr.name
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn eval_id_reference(&self, ir: &str) -> trc::Result<Id> {
|
||||
if let Some(AnyId::Id(id)) = self.created_ids.get(ir) {
|
||||
Ok(*id)
|
||||
} else {
|
||||
Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!("Id reference {ir:?} not found.")))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn eval_blob_id_reference(&self, ir: &str) -> trc::Result<BlobId> {
|
||||
if let Some(AnyId::BlobId(id)) = self.created_ids.get(ir) {
|
||||
Ok(id.clone())
|
||||
} else {
|
||||
Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!("blobId reference {ir:?} not found.")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait EvalObjectReferences {
|
||||
fn eval_object_references(
|
||||
&mut self,
|
||||
response: &Response<'_>,
|
||||
graph: &mut Graph<'_>,
|
||||
depth: usize,
|
||||
max_depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> trc::Result<()>;
|
||||
}
|
||||
|
||||
impl<'x, P, E> EvalObjectReferences for Value<'x, P, E>
|
||||
where
|
||||
P: Property + JmapObjectId,
|
||||
E: Element<Property = P> + JmapObjectId,
|
||||
{
|
||||
fn eval_object_references(
|
||||
&mut self,
|
||||
response: &Response<'_>,
|
||||
graph: &mut Graph<'_>,
|
||||
depth: usize,
|
||||
max_depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> trc::Result<()> {
|
||||
match self {
|
||||
Value::Element(element) => {
|
||||
if let Some(id_ref) = element.as_id_ref() {
|
||||
if let Some(id) = response.created_ids.get(id_ref) {
|
||||
if !element.try_set_id(id.clone()) {
|
||||
return Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details("Id reference points to invalid type."));
|
||||
}
|
||||
} else if let Graph::Some { child_id, graph } = graph {
|
||||
graph
|
||||
.entry(child_id.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id_ref.to_string());
|
||||
} else {
|
||||
return Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!("Id reference {id_ref:?} not found.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(items) if depth < max_depth => {
|
||||
// Resolve references in arrays (e.g. emailIds: [#idRef1, #idRef2])
|
||||
for item in items {
|
||||
item.eval_object_references(
|
||||
response,
|
||||
graph,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
eval_strings,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Value::Object(items) if depth < max_depth => {
|
||||
// Resolve references in JMAP sets (e.g. mailboxIds: { "#idRef1": true, "#idRef2": true })
|
||||
for (key, value) in items.as_mut_vec() {
|
||||
if let Key::Property(property) = key
|
||||
&& let Some(id_ref) = property.as_id_ref()
|
||||
{
|
||||
if let Some(id) = response.created_ids.get(id_ref) {
|
||||
if !property.try_set_id(id.clone()) {
|
||||
return Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details("Id reference points to invalid type."));
|
||||
}
|
||||
} else {
|
||||
return Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!("Id reference {id_ref:?} not found.")));
|
||||
}
|
||||
} else if eval_strings
|
||||
&& let Some(id) = key
|
||||
.as_string_key()
|
||||
.and_then(|k| k.strip_prefix('#'))
|
||||
.and_then(|id_ref| response.created_ids.get(id_ref))
|
||||
{
|
||||
*key = Key::Owned(match id {
|
||||
AnyId::Id(id) => id.to_string(),
|
||||
AnyId::BlobId(id) => id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(
|
||||
value,
|
||||
Value::Element(_) | Value::Array(_) | Value::Object(_)
|
||||
) {
|
||||
value.eval_object_references(
|
||||
response,
|
||||
graph,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
eval_strings,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
method::{
|
||||
PropertyWrapper,
|
||||
availability::{BusyPeriod, GetAvailabilityResponse},
|
||||
changes::ChangesResponse,
|
||||
get::GetResponse,
|
||||
query::QueryResponse,
|
||||
query_changes::{AddedItem, QueryChangesResponse},
|
||||
},
|
||||
object::{
|
||||
AnyId, JmapObject, JmapObjectId,
|
||||
calendar_event_notification::{
|
||||
CalendarEventNotificationGetResponse, CalendarEventNotificationObject,
|
||||
},
|
||||
},
|
||||
request::reference::ResultReference,
|
||||
};
|
||||
use compact_str::format_compact;
|
||||
use jmap_tools::{Element, JsonPointerItem, JsonPointerIter, Key, Null, Property, Value};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::{blob::BlobId, id::Id};
|
||||
|
||||
pub(crate) trait ResponsePtr {
|
||||
fn eval_jptr(&self, pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct EvalResults(Vec<EvalResult>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EvalResult {
|
||||
Id(AnyId),
|
||||
Property(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl<T> ResponsePtr for Vec<T>
|
||||
where
|
||||
T: ResponsePtr,
|
||||
{
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next() {
|
||||
Some(JsonPointerItem::Number(n)) => {
|
||||
if let Some(v) = self.get(*n as usize) {
|
||||
v.eval_jptr(pointer, results);
|
||||
}
|
||||
}
|
||||
Some(JsonPointerItem::Wildcard | JsonPointerItem::Root) | None => {
|
||||
for v in self {
|
||||
v.eval_jptr(pointer.clone(), results);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx, P, E> ResponsePtr for Value<'ctx, P, E>
|
||||
where
|
||||
P: Property,
|
||||
E: Element<Property = P> + JmapObjectId,
|
||||
{
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next() {
|
||||
Some(JsonPointerItem::Key(key)) => {
|
||||
if let Some(key) = key.as_string_key()
|
||||
&& let Value::Object(map) = self
|
||||
&& let Some(v) = map.get(&Key::Borrowed(key))
|
||||
{
|
||||
v.eval_jptr(pointer, results);
|
||||
}
|
||||
}
|
||||
Some(JsonPointerItem::Number(n)) => match self {
|
||||
Value::Array(values) => {
|
||||
if let Some(v) = values.get(*n as usize) {
|
||||
v.eval_jptr(pointer, results);
|
||||
}
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let n = Key::Owned(n.to_string());
|
||||
if let Some(v) = map.get(&n) {
|
||||
v.eval_jptr(pointer, results);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(JsonPointerItem::Wildcard) => match self {
|
||||
Value::Array(values) => {
|
||||
for v in values {
|
||||
v.eval_jptr(pointer.clone(), results);
|
||||
}
|
||||
}
|
||||
Value::Object(map) => {
|
||||
for v in map.values() {
|
||||
v.eval_jptr(pointer.clone(), results);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(JsonPointerItem::Root) | None => match self {
|
||||
Value::Element(e) => {
|
||||
if let Some(id) = e.as_any_id() {
|
||||
results.0.push(EvalResult::Id(id));
|
||||
}
|
||||
}
|
||||
Value::Array(list) => {
|
||||
for item in list {
|
||||
if let Value::Element(e) = item
|
||||
&& let Some(id) = e.as_any_id()
|
||||
{
|
||||
results.0.push(EvalResult::Id(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for Id {
|
||||
fn eval_jptr(&self, _pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
results.0.push(EvalResult::Id(AnyId::Id(*self)));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for BlobId {
|
||||
fn eval_jptr(&self, _pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
results.0.push(EvalResult::Id(AnyId::BlobId(self.clone())));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Property> ResponsePtr for PropertyWrapper<T> {
|
||||
fn eval_jptr(&self, _: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
results.0.push(EvalResult::Property(self.0.to_cow()));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ResponsePtr for GetResponse<T> {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("list") => {
|
||||
self.list.eval_jptr(pointer, results);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ResponsePtr for ChangesResponse<T> {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
if let Some(property) = pointer.next().and_then(|item| item.as_string_key()) {
|
||||
hashify::fnc_map!(property.as_bytes(),
|
||||
"created" => {
|
||||
self.created.eval_jptr(pointer, results);
|
||||
},
|
||||
"updated" => {
|
||||
self.updated.eval_jptr(pointer, results);
|
||||
},
|
||||
"updatedProperties" => {
|
||||
if let Some(props) = &self.updated_properties {
|
||||
props.eval_jptr(pointer, results);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for QueryResponse {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("ids") => {
|
||||
self.ids.eval_jptr(pointer, results);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for QueryChangesResponse {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("added") => {
|
||||
self.added.eval_jptr(pointer, results);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for AddedItem {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("id") => {
|
||||
results.0.push(EvalResult::Id(AnyId::Id(self.id)));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for CalendarEventNotificationGetResponse {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("list") => {
|
||||
self.list.eval_jptr(pointer, results);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for CalendarEventNotificationObject {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("id") => {
|
||||
results.0.push(EvalResult::Id(AnyId::Id(self.id)));
|
||||
true
|
||||
}
|
||||
Some("calendarEventId") => {
|
||||
if let Some(id) = &self.calendar_event_id {
|
||||
results.0.push(EvalResult::Id(AnyId::Id(*id)));
|
||||
}
|
||||
true
|
||||
}
|
||||
Some("event") => {
|
||||
if let Some(event) = &self.event {
|
||||
event.0.eval_jptr(pointer, results);
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for GetAvailabilityResponse {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("list") => {
|
||||
self.list.eval_jptr(pointer, results);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsePtr for BusyPeriod {
|
||||
fn eval_jptr(&self, mut pointer: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
|
||||
match pointer.next().and_then(|item| item.as_string_key()) {
|
||||
Some("event") => {
|
||||
if let Some(event) = &self.event {
|
||||
event.0.eval_jptr(pointer, results);
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalResults {
|
||||
pub fn into_ids<T: TryFrom<AnyId>>(
|
||||
self,
|
||||
rr: &ResultReference,
|
||||
) -> impl Iterator<Item = trc::Result<T>> {
|
||||
self.0.into_iter().map(move |id| {
|
||||
if let EvalResult::Id(any_id) = id {
|
||||
T::try_from(any_id).map_err(|_| {
|
||||
trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Failed to evaluate {rr} result reference: Invalid Id type."
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Failed to evaluate {rr} result reference: Invalid Id type."
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_properties<T: Property + FromStr>(
|
||||
self,
|
||||
rr: &ResultReference,
|
||||
) -> impl Iterator<Item = trc::Result<T>> {
|
||||
self.0.into_iter().map(move |prop| {
|
||||
if let EvalResult::Property(prop) = prop {
|
||||
T::from_str(&prop).map_err(|_| {
|
||||
trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Failed to evaluate {rr} result reference: Invalid property."
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Err(trc::JmapEvent::InvalidResultReference
|
||||
.into_err()
|
||||
.details(format_compact!(
|
||||
"Failed to evaluate {rr} result reference: Invalid property."
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,944 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use compact_str::format_compact;
|
||||
use std::collections::HashMap;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub mod eval;
|
||||
pub mod jsptr;
|
||||
pub mod resolve;
|
||||
|
||||
pub(crate) enum Graph<'x> {
|
||||
Some {
|
||||
child_id: &'x str,
|
||||
graph: &'x mut HashMap<String, Vec<String>>,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
fn topological_sort<T>(
|
||||
create: &mut VecMap<String, T>,
|
||||
graph: HashMap<String, Vec<String>>,
|
||||
) -> trc::Result<VecMap<String, T>> {
|
||||
// Make sure all references exist
|
||||
for (from_id, to_ids) in graph.iter() {
|
||||
for to_id in to_ids {
|
||||
if !create.contains_key(to_id) {
|
||||
return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
|
||||
format_compact!(
|
||||
"Invalid reference to non-existing object {to_id:?} from {from_id:?}"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_create = VecMap::with_capacity(create.len());
|
||||
let mut it_stack = Vec::new();
|
||||
let keys = graph.keys().cloned().collect::<Vec<_>>();
|
||||
let mut it = keys.iter();
|
||||
|
||||
'main: loop {
|
||||
while let Some(from_id) = it.next() {
|
||||
if let Some(to_ids) = graph.get(from_id) {
|
||||
it_stack.push((it, from_id));
|
||||
if it_stack.len() > 1000 {
|
||||
return Err(trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details("Cyclical references are not allowed."));
|
||||
}
|
||||
it = to_ids.iter();
|
||||
continue;
|
||||
} else if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((prev_it, from_id)) = it_stack.pop() {
|
||||
it = prev_it;
|
||||
if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
if !create.is_empty() {
|
||||
for (id, value) in std::mem::take(create) {
|
||||
sorted_create.append(id, value);
|
||||
}
|
||||
}
|
||||
Ok(sorted_create)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::references::Graph;
|
||||
use crate::references::eval::EvalObjectReferences;
|
||||
use crate::{
|
||||
method::{changes::ChangesResponse, get::GetResponse, query::QueryResponse},
|
||||
object::{
|
||||
email::{EmailProperty, EmailValue},
|
||||
mailbox::{MailboxProperty, MailboxValue},
|
||||
thread::{ThreadProperty, ThreadValue},
|
||||
},
|
||||
request::{
|
||||
Call, GetRequestMethod, Request, RequestMethod, SetRequestMethod,
|
||||
reference::{MaybeIdReference, MaybeResultReference},
|
||||
},
|
||||
response::{ChangesResponseMethod, GetResponseMethod, Response, ResponseMethod},
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use types::id::Id;
|
||||
|
||||
#[test]
|
||||
fn eval_value_references() {
|
||||
let request = Request::parse(
|
||||
br##"{
|
||||
"using":["urn:ietf:params:jmap:mail"],
|
||||
"methodCalls": [[ "Email/query", {
|
||||
"accountId": "a",
|
||||
"filter": { "inMailbox": "a" },
|
||||
"sort": [{ "property": "receivedAt", "isAscending": false }],
|
||||
"collapseThreads": true,
|
||||
"position": 0,
|
||||
"limit": 10,
|
||||
"calculateTotal": true
|
||||
}, "t0" ],
|
||||
[ "Email/get", {
|
||||
"accountId": "a",
|
||||
"#ids": {
|
||||
"resultOf": "t0",
|
||||
"name": "Email/query",
|
||||
"path": "/ids"
|
||||
},
|
||||
"properties": [ "threadId" ]
|
||||
}, "t1" ],
|
||||
[ "Thread/get", {
|
||||
"accountId": "a",
|
||||
"#ids": {
|
||||
"resultOf": "t1",
|
||||
"name": "Email/get",
|
||||
"path": "/list/*/threadId"
|
||||
}
|
||||
}, "t2" ],
|
||||
[ "Email/get", {
|
||||
"accountId": "a",
|
||||
"#ids": {
|
||||
"resultOf": "t2",
|
||||
"name": "Thread/get",
|
||||
"path": "/list/*/emailIds"
|
||||
},
|
||||
"properties": [ "from", "receivedAt", "subject" ]
|
||||
}, "t3" ]]
|
||||
}"##,
|
||||
100,
|
||||
1024 * 1024,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut response = Response::new(
|
||||
1234,
|
||||
request.created_ids.unwrap_or_default(),
|
||||
request.method_calls.len(),
|
||||
);
|
||||
|
||||
assert_eq!(request.method_calls.len(), 4);
|
||||
|
||||
for (test_num, mut call) in request.method_calls.into_iter().enumerate() {
|
||||
match test_num {
|
||||
0 => {
|
||||
response.method_responses.push(Call {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
method: ResponseMethod::Query(QueryResponse {
|
||||
account_id: Id::new(1),
|
||||
query_state: Default::default(),
|
||||
can_calculate_changes: Default::default(),
|
||||
position: Default::default(),
|
||||
ids: vec![Id::new(4), Id::new(5)],
|
||||
total: Default::default(),
|
||||
limit: Default::default(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
1 => {
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
match call.method {
|
||||
RequestMethod::Get(GetRequestMethod::Email(req)) => {
|
||||
assert_eq!(
|
||||
req.ids,
|
||||
Some(MaybeResultReference::Value(vec![
|
||||
MaybeIdReference::Id(Id::new(4)),
|
||||
MaybeIdReference::Id(Id::new(5))
|
||||
]))
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Email Get Request"),
|
||||
}
|
||||
response.method_responses.push(Call {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
method: ResponseMethod::Get(GetResponseMethod::Email(GetResponse {
|
||||
account_id: Id::new(1).into(),
|
||||
state: Default::default(),
|
||||
list: vec![
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(EmailProperty::ThreadId),
|
||||
Value::Element(EmailValue::Id(Id::new(9))),
|
||||
)])),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(EmailProperty::ThreadId),
|
||||
Value::Element(EmailValue::Id(Id::new(10))),
|
||||
)])),
|
||||
],
|
||||
not_found: Default::default(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
2 => {
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
match call.method {
|
||||
RequestMethod::Get(GetRequestMethod::Thread(req)) => {
|
||||
assert_eq!(
|
||||
req.ids,
|
||||
Some(MaybeResultReference::Value(vec![
|
||||
MaybeIdReference::Id(Id::new(9)),
|
||||
MaybeIdReference::Id(Id::new(10))
|
||||
]))
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Thread Get Request"),
|
||||
}
|
||||
response.method_responses.push(Call {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
method: ResponseMethod::Get(GetResponseMethod::Thread(GetResponse {
|
||||
account_id: Id::new(1).into(),
|
||||
state: Default::default(),
|
||||
list: vec![
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(ThreadProperty::EmailIds),
|
||||
Value::Array(vec![
|
||||
Value::Element(ThreadValue::Id(Id::new(100))),
|
||||
Value::Element(ThreadValue::Id(Id::new(101))),
|
||||
]),
|
||||
)])),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(ThreadProperty::EmailIds),
|
||||
Value::Array(vec![
|
||||
Value::Element(ThreadValue::Id(Id::new(102))),
|
||||
Value::Element(ThreadValue::Id(Id::new(103))),
|
||||
]),
|
||||
)])),
|
||||
],
|
||||
not_found: Default::default(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
3 => {
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
match call.method {
|
||||
RequestMethod::Get(GetRequestMethod::Email(req)) => {
|
||||
assert_eq!(
|
||||
req.ids,
|
||||
Some(MaybeResultReference::Value(vec![
|
||||
MaybeIdReference::Id(Id::new(100)),
|
||||
MaybeIdReference::Id(Id::new(101)),
|
||||
MaybeIdReference::Id(Id::new(102)),
|
||||
MaybeIdReference::Id(Id::new(103)),
|
||||
]))
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Mailbox Get Request"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected invocation {}", test_num),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_property_references() {
|
||||
let request = Request::parse(
|
||||
br##"{
|
||||
"using":["urn:ietf:params:jmap:mail"],
|
||||
"methodCalls": [
|
||||
["Mailbox/changes",{
|
||||
"accountId":"s",
|
||||
"sinceState":"srxqk071myhgkyay"
|
||||
},"0"],
|
||||
["Mailbox/get",{
|
||||
"accountId":"s",
|
||||
"#ids":{"name":"Mailbox/changes","path":"/created","resultOf":"0"}
|
||||
},"1"],
|
||||
["Mailbox/get",{
|
||||
"accountId":"s",
|
||||
"#ids":{"name":"Mailbox/changes","path":"/updated","resultOf":"0"},
|
||||
"#properties":{"name":"Mailbox/changes","path":"/updatedProperties","resultOf":"0"}
|
||||
},"2"]
|
||||
]
|
||||
}"##,
|
||||
100,
|
||||
1024 * 1024,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut response = Response::new(
|
||||
1234,
|
||||
request.created_ids.unwrap_or_default(),
|
||||
request.method_calls.len(),
|
||||
);
|
||||
|
||||
assert_eq!(request.method_calls.len(), 3);
|
||||
|
||||
for (test_num, mut call) in request.method_calls.into_iter().enumerate() {
|
||||
match test_num {
|
||||
0 => {
|
||||
response.method_responses.push(Call {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
method: ResponseMethod::Changes(ChangesResponseMethod::Mailbox(Box::new(
|
||||
ChangesResponse {
|
||||
account_id: Id::new(1),
|
||||
old_state: Default::default(),
|
||||
new_state: Default::default(),
|
||||
has_more_changes: Default::default(),
|
||||
created: Default::default(),
|
||||
updated: vec![Id::new(2), Id::new(3)],
|
||||
destroyed: Default::default(),
|
||||
updated_properties: Some(vec![
|
||||
MailboxProperty::Name.into(),
|
||||
MailboxProperty::ParentId.into(),
|
||||
]),
|
||||
},
|
||||
))),
|
||||
});
|
||||
}
|
||||
1 => {
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
match call.method {
|
||||
RequestMethod::Get(GetRequestMethod::Mailbox(req)) => {
|
||||
assert_eq!(req.ids, Some(MaybeResultReference::Value(vec![])));
|
||||
}
|
||||
_ => panic!("Expected Mailbox Get Request"),
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
match call.method {
|
||||
RequestMethod::Get(GetRequestMethod::Mailbox(req)) => {
|
||||
assert_eq!(
|
||||
req.ids,
|
||||
Some(MaybeResultReference::Value(vec![
|
||||
MaybeIdReference::Id(Id::new(2)),
|
||||
MaybeIdReference::Id(Id::new(3))
|
||||
]))
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected Mailbox Get Request"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected invocation {}", test_num),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_create_references() {
|
||||
let request = Request::parse(
|
||||
br##"{
|
||||
"using": [
|
||||
"urn:ietf:params:jmap:core",
|
||||
"urn:ietf:params:jmap:mail"
|
||||
],
|
||||
"methodCalls": [
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"a": {
|
||||
"name": "Folder a",
|
||||
"parentId": "#b"
|
||||
},
|
||||
"b": {
|
||||
"name": "Folder b",
|
||||
"parentId": "#c"
|
||||
},
|
||||
"c": {
|
||||
"name": "Folder c",
|
||||
"parentId": "#d"
|
||||
},
|
||||
"d": {
|
||||
"name": "Folder d",
|
||||
"parentId": "#e"
|
||||
},
|
||||
"e": {
|
||||
"name": "Folder e",
|
||||
"parentId": "#f"
|
||||
},
|
||||
"f": {
|
||||
"name": "Folder f",
|
||||
"parentId": "#g"
|
||||
},
|
||||
"g": {
|
||||
"name": "Folder g",
|
||||
"parentId": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"fulltree"
|
||||
],
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"a1": {
|
||||
"name": "Folder a1",
|
||||
"parentId": null
|
||||
},
|
||||
"b2": {
|
||||
"name": "Folder b2",
|
||||
"parentId": "#a1"
|
||||
},
|
||||
"c3": {
|
||||
"name": "Folder c3",
|
||||
"parentId": "#a1"
|
||||
},
|
||||
"d4": {
|
||||
"name": "Folder d4",
|
||||
"parentId": "#b2"
|
||||
},
|
||||
"e5": {
|
||||
"name": "Folder e5",
|
||||
"parentId": "#b2"
|
||||
},
|
||||
"f6": {
|
||||
"name": "Folder f6",
|
||||
"parentId": "#d4"
|
||||
},
|
||||
"g7": {
|
||||
"name": "Folder g7",
|
||||
"parentId": "#e5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fulltree2"
|
||||
],
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"z": {
|
||||
"name": "Folder Z",
|
||||
"parentId": "#x"
|
||||
},
|
||||
"y": {
|
||||
"name": null
|
||||
},
|
||||
"x": {
|
||||
"name": "Folder X"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xyz"
|
||||
],
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"a": {
|
||||
"name": "Folder a",
|
||||
"parentId": "#b"
|
||||
},
|
||||
"b": {
|
||||
"name": "Folder b",
|
||||
"parentId": "#c"
|
||||
},
|
||||
"c": {
|
||||
"name": "Folder c",
|
||||
"parentId": "#d"
|
||||
},
|
||||
"d": {
|
||||
"name": "Folder d",
|
||||
"parentId": "#a"
|
||||
}
|
||||
}
|
||||
},
|
||||
"circular"
|
||||
]
|
||||
]
|
||||
}"##,
|
||||
100,
|
||||
1024 * 1024,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response = Response::new(
|
||||
1234,
|
||||
request.created_ids.unwrap_or_default(),
|
||||
request.method_calls.len(),
|
||||
);
|
||||
|
||||
for (test_num, mut call) in request.method_calls.into_iter().enumerate() {
|
||||
match response.resolve_references(&mut call.method) {
|
||||
Ok(_) => assert!(
|
||||
(0..3).contains(&test_num),
|
||||
"Unexpected invocation {}",
|
||||
test_num
|
||||
),
|
||||
Err(err) => {
|
||||
assert_eq!(test_num, 3);
|
||||
assert!(
|
||||
err.matches(trc::EventType::Jmap(trc::JmapEvent::InvalidArguments)),
|
||||
"{:?}",
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method {
|
||||
if test_num == 0 {
|
||||
assert_eq!(
|
||||
request
|
||||
.create
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|b| b.0)
|
||||
.collect::<Vec<_>>(),
|
||||
["g", "f", "e", "d", "c", "b", "a"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
} else if test_num == 1 {
|
||||
let mut pending_ids = vec!["a1", "b2", "d4", "e5", "f6", "c3", "g7"];
|
||||
|
||||
for (id, _) in request.create.as_ref().unwrap() {
|
||||
match id.as_str() {
|
||||
"a1" => (),
|
||||
"b2" | "c3" => assert!(!pending_ids.contains(&"a1")),
|
||||
"d4" | "e5" => assert!(!pending_ids.contains(&"b2")),
|
||||
"f6" => assert!(!pending_ids.contains(&"d4")),
|
||||
"g7" => assert!(!pending_ids.contains(&"e5")),
|
||||
_ => panic!("Unexpected ID"),
|
||||
}
|
||||
pending_ids.retain(|i| i != id);
|
||||
}
|
||||
|
||||
if !pending_ids.is_empty() {
|
||||
panic!(
|
||||
"Unexpected order: {:?}",
|
||||
request
|
||||
.create
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|b| b.0.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} else if test_num == 2 {
|
||||
assert_eq!(
|
||||
request
|
||||
.create
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|b| b.0)
|
||||
.collect::<Vec<_>>(),
|
||||
["x", "z", "y"]
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Set Mailbox Request");
|
||||
}
|
||||
}
|
||||
|
||||
let request = Request::parse(
|
||||
br##"{
|
||||
"using": [
|
||||
"urn:ietf:params:jmap:core",
|
||||
"urn:ietf:params:jmap:mail"
|
||||
],
|
||||
"methodCalls": [
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"a": {
|
||||
"name": "a",
|
||||
"parentId": "#x"
|
||||
},
|
||||
"b": {
|
||||
"name": "b",
|
||||
"parentId": "#y"
|
||||
},
|
||||
"c": {
|
||||
"name": "c",
|
||||
"parentId": "#z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ref1"
|
||||
],
|
||||
[
|
||||
"Mailbox/set",
|
||||
{
|
||||
"accountId": "b",
|
||||
"create": {
|
||||
"a1": {
|
||||
"name": "a1",
|
||||
"parentId": "#a"
|
||||
},
|
||||
"b2": {
|
||||
"name": "b2",
|
||||
"parentId": "#b"
|
||||
},
|
||||
"c3": {
|
||||
"name": "c3",
|
||||
"parentId": "#c"
|
||||
}
|
||||
}
|
||||
},
|
||||
"red2"
|
||||
]
|
||||
],
|
||||
"createdIds": {
|
||||
"x": "b",
|
||||
"y": "c",
|
||||
"z": "d"
|
||||
}
|
||||
}"##,
|
||||
1024,
|
||||
1024 * 1024,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut response = Response::new(
|
||||
1234,
|
||||
request.created_ids.unwrap_or_default(),
|
||||
request.method_calls.len(),
|
||||
);
|
||||
|
||||
let mut invocations = request.method_calls.into_iter();
|
||||
let mut call = invocations.next().unwrap();
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
|
||||
if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method {
|
||||
let create = request
|
||||
.create
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(p, v)| {
|
||||
(
|
||||
p.as_str(),
|
||||
v.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
assert_eq!(
|
||||
*create.get("a").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(1)))
|
||||
);
|
||||
assert_eq!(
|
||||
*create.get("b").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(2)))
|
||||
);
|
||||
assert_eq!(
|
||||
*create.get("c").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(3)))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Mailbox Set Request");
|
||||
}
|
||||
|
||||
response
|
||||
.created_ids
|
||||
.insert("a".to_string(), Id::new(5).into());
|
||||
response
|
||||
.created_ids
|
||||
.insert("b".to_string(), Id::new(6).into());
|
||||
response
|
||||
.created_ids
|
||||
.insert("c".to_string(), Id::new(7).into());
|
||||
|
||||
let mut call = invocations.next().unwrap();
|
||||
response.resolve_references(&mut call.method).unwrap();
|
||||
|
||||
if let RequestMethod::Set(SetRequestMethod::Mailbox(request)) = call.method {
|
||||
let create = request
|
||||
.create
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(p, v)| {
|
||||
(
|
||||
p.as_str(),
|
||||
v.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
assert_eq!(
|
||||
*create.get("a1").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(5)))
|
||||
);
|
||||
assert_eq!(
|
||||
*create.get("b2").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(6)))
|
||||
);
|
||||
assert_eq!(
|
||||
*create.get("c3").unwrap(),
|
||||
&Value::Element(MailboxValue::Id(Id::new(7)))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Mailbox Set Request");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nested_element_ref() {
|
||||
let mut created_ids = HashMap::new();
|
||||
created_ids.insert("server-1".to_string(), Id::new(42).into());
|
||||
let response = Response::new(0, created_ids, 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![
|
||||
(
|
||||
Key::Property(MailboxProperty::Name),
|
||||
Value::Str("inbox".into()),
|
||||
),
|
||||
(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("server-1".into())),
|
||||
)])),
|
||||
),
|
||||
]));
|
||||
|
||||
value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
|
||||
.unwrap();
|
||||
|
||||
let nested = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap();
|
||||
assert_eq!(nested, &Value::Element(MailboxValue::Id(Id::new(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_element_ref() {
|
||||
let mut created_ids = HashMap::new();
|
||||
created_ids.insert("a".to_string(), Id::new(1).into());
|
||||
created_ids.insert("b".to_string(), Id::new(2).into());
|
||||
let response = Response::new(0, created_ids, 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Array(vec![
|
||||
Value::Element(MailboxValue::IdReference("a".into())),
|
||||
Value::Element(MailboxValue::IdReference("b".into())),
|
||||
]),
|
||||
)]));
|
||||
|
||||
value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
|
||||
.unwrap();
|
||||
|
||||
let arr = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap();
|
||||
assert_eq!(arr.len(), 2);
|
||||
assert_eq!(arr[0], Value::Element(MailboxValue::Id(Id::new(1))));
|
||||
assert_eq!(arr[1], Value::Element(MailboxValue::Id(Id::new(2))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nested_array_of_objects_with_element_ref() {
|
||||
let mut created_ids = HashMap::new();
|
||||
created_ids.insert("a".to_string(), Id::new(7).into());
|
||||
let response = Response::new(0, created_ids, 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Array(vec![Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("a".into())),
|
||||
)]))]),
|
||||
)]));
|
||||
|
||||
value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
|
||||
.unwrap();
|
||||
|
||||
let resolved = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()[0]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap();
|
||||
assert_eq!(resolved, &Value::Element(MailboxValue::Id(Id::new(7))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_graph_collects_nested_ref() {
|
||||
let response = Response::new(0, HashMap::new(), 0);
|
||||
let mut graph_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let child_id = "outer".to_string();
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("inner".into())),
|
||||
)])),
|
||||
)]));
|
||||
|
||||
{
|
||||
let mut graph = Graph::Some {
|
||||
child_id: &child_id,
|
||||
graph: &mut graph_map,
|
||||
};
|
||||
value
|
||||
.eval_object_references(&response, &mut graph, 0, 5, true)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(graph_map.get("outer"), Some(&vec!["inner".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_unresolved_nested_ref_errors_without_graph() {
|
||||
let response = Response::new(0, HashMap::new(), 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("missing".into())),
|
||||
)])),
|
||||
)]));
|
||||
|
||||
let err = value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 5, true)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
err.matches(trc::EventType::Jmap(trc::JmapEvent::InvalidResultReference)),
|
||||
"{:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_depth_limit_blocks_walk_into_inner_object() {
|
||||
let mut created_ids = HashMap::new();
|
||||
created_ids.insert("inner".to_string(), Id::new(99).into());
|
||||
let response = Response::new(0, created_ids, 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("inner".into())),
|
||||
)])),
|
||||
)])),
|
||||
)]));
|
||||
|
||||
value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 2, true)
|
||||
.unwrap();
|
||||
|
||||
let deepest = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
deepest,
|
||||
&Value::Element(MailboxValue::IdReference("inner".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_depth_limit_substitutes_element_at_max_depth() {
|
||||
let mut created_ids = HashMap::new();
|
||||
created_ids.insert("inner".to_string(), Id::new(99).into());
|
||||
let response = Response::new(0, created_ids, 0);
|
||||
|
||||
let mut value: Value<'_, MailboxProperty, MailboxValue> = Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Object(Map::from(vec![(
|
||||
Key::Property(MailboxProperty::ParentId),
|
||||
Value::Element(MailboxValue::IdReference("inner".into())),
|
||||
)])),
|
||||
)]));
|
||||
|
||||
value
|
||||
.eval_object_references(&response, &mut Graph::None, 0, 2, true)
|
||||
.unwrap();
|
||||
|
||||
let resolved = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get(&Key::Property(MailboxProperty::ParentId))
|
||||
.unwrap();
|
||||
assert_eq!(resolved, &Value::Element(MailboxValue::Id(Id::new(99))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
method::{
|
||||
copy::CopyRequest,
|
||||
get::GetRequest,
|
||||
import::ImportEmailRequest,
|
||||
parse::ParseRequest,
|
||||
search_snippet::GetSearchSnippetRequest,
|
||||
set::{SetRequest, SetResponse},
|
||||
upload::{BlobUploadRequest, DataSourceObject},
|
||||
},
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
references::{Graph, eval::EvalObjectReferences, topological_sort},
|
||||
request::{
|
||||
CopyRequestMethod, GetRequestMethod, MaybeInvalid, ParseRequestMethod, RequestMethod,
|
||||
SetRequestMethod,
|
||||
reference::{MaybeIdReference, MaybeResultReference},
|
||||
},
|
||||
response::Response,
|
||||
};
|
||||
use compact_str::format_compact;
|
||||
use jmap_tools::{Element, Key, Property, Value};
|
||||
use std::collections::HashMap;
|
||||
use types::id::Id;
|
||||
|
||||
impl Response<'_> {
|
||||
pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> {
|
||||
match request {
|
||||
RequestMethod::Get(request) => match request {
|
||||
GetRequestMethod::Email(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Mailbox(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Thread(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Identity(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::EmailSubmission(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::PushSubscription(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Sieve(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::VacationResponse(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Principal(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Quota(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Blob(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::AddressBook(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::ContactCard(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::FileNode(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::ShareNotification(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Calendar(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::CalendarEventNotification(request) => {
|
||||
request.resolve_references(self)?
|
||||
}
|
||||
GetRequestMethod::ParticipantIdentity(request) => {
|
||||
request.resolve_references(self)?
|
||||
}
|
||||
GetRequestMethod::PrincipalAvailability(_) => (),
|
||||
GetRequestMethod::Registry(request) => request.resolve_references(self)?,
|
||||
},
|
||||
RequestMethod::Set(request) => match request {
|
||||
SetRequestMethod::Email(request) => request.resolve_references(self, 2, false)?,
|
||||
SetRequestMethod::Mailbox(request) => request.resolve_references(self, 1, false)?,
|
||||
SetRequestMethod::Identity(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::EmailSubmission(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::PushSubscription(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::Sieve(request) => request.resolve_references(self, 1, false)?,
|
||||
SetRequestMethod::VacationResponse(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::AddressBook(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::ContactCard(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::FileNode(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::ShareNotification(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::Calendar(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::CalendarEvent(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::CalendarEventNotification(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::ParticipantIdentity(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::Registry(request) => request.resolve_references(self, 5, true)?,
|
||||
},
|
||||
RequestMethod::Copy(request) => match request {
|
||||
CopyRequestMethod::Email(request) => request.resolve_references(self, 1, false)?,
|
||||
CopyRequestMethod::CalendarEvent(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
CopyRequestMethod::ContactCard(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
CopyRequestMethod::FileNode(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
CopyRequestMethod::Blob(_) => (),
|
||||
},
|
||||
RequestMethod::ImportEmail(request) => request.resolve_references(self)?,
|
||||
RequestMethod::SearchSnippet(request) => request.resolve_references(self)?,
|
||||
RequestMethod::UploadBlob(request) => request.resolve_references(self)?,
|
||||
RequestMethod::Parse(request) => match request {
|
||||
ParseRequestMethod::Email(request) => request.resolve_references(self)?,
|
||||
ParseRequestMethod::ContactCard(request) => request.resolve_references(self)?,
|
||||
ParseRequestMethod::CalendarEvent(request) => request.resolve_references(self)?,
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResolveCreatedReference<P, E>
|
||||
where
|
||||
P: Property,
|
||||
E: Element<Property = P> + JmapObjectId,
|
||||
{
|
||||
fn get_created_id(&self, id_ref: &str) -> Option<AnyId>;
|
||||
|
||||
fn resolve_self_references(
|
||||
&self,
|
||||
value: &mut Value<'_, P, E>,
|
||||
depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> Result<(), SetError<P>> {
|
||||
match value {
|
||||
Value::Object(obj) if eval_strings && depth < 5 => {
|
||||
for (key, value) in obj.as_mut_vec() {
|
||||
if let Some(id) = key
|
||||
.as_string_key()
|
||||
.and_then(|k| k.strip_prefix('#'))
|
||||
.and_then(|id_ref| self.get_created_id(id_ref))
|
||||
{
|
||||
*key = Key::Owned(match id {
|
||||
AnyId::Id(id) => id.to_string(),
|
||||
AnyId::BlobId(id) => id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(value, Value::Object(_) | Value::Array(_)) {
|
||||
self.resolve_self_references(value, depth + 1, eval_strings)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Element(element) => {
|
||||
if let Some(id_ref) = element.as_id_ref() {
|
||||
if let Some(id) = self.get_created_id(id_ref) {
|
||||
if !element.try_set_id(id) {
|
||||
return Err(SetError::invalid_properties()
|
||||
.with_description("Id reference points to invalid type."));
|
||||
}
|
||||
} else {
|
||||
return Err(SetError::not_found()
|
||||
.with_description(format!("Id reference {id_ref:?} not found.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(items) if depth < 5 => {
|
||||
for item in items {
|
||||
self.resolve_self_references(item, depth + 1, eval_strings)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ResolveReference {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()>;
|
||||
}
|
||||
|
||||
pub(crate) trait ResolveSetReference {
|
||||
fn resolve_references(
|
||||
&mut self,
|
||||
response: &Response<'_>,
|
||||
max_depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> trc::Result<()>;
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ResolveReference for GetRequest<T> {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
|
||||
// Resolve id references
|
||||
match &mut self.ids {
|
||||
Some(MaybeResultReference::Reference(reference)) => {
|
||||
self.ids = Some(MaybeResultReference::Value(
|
||||
response
|
||||
.eval_result_references(reference)?
|
||||
.into_ids::<T::Id>(reference)
|
||||
.map(|f| f.map(MaybeIdReference::Id))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
Some(MaybeResultReference::Value(ids)) => {
|
||||
for id in ids {
|
||||
if let MaybeIdReference::Reference(reference) = id {
|
||||
if let Some(resolved_id) = response
|
||||
.created_ids
|
||||
.get(reference)
|
||||
.cloned()
|
||||
.and_then(|v| T::Id::try_from(v).ok())
|
||||
{
|
||||
*id = MaybeIdReference::Id(resolved_id);
|
||||
} else {
|
||||
return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
|
||||
format_compact!(
|
||||
"Id reference {reference:?} does not exist or is invalid."
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Resolve properties references
|
||||
if let Some(MaybeResultReference::Reference(reference)) = &self.properties {
|
||||
self.properties = Some(MaybeResultReference::Value(
|
||||
response
|
||||
.eval_result_references(reference)?
|
||||
.into_properties::<T::Property>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: JmapObject> ResolveSetReference for SetRequest<'x, T> {
|
||||
fn resolve_references(
|
||||
&mut self,
|
||||
response: &Response<'_>,
|
||||
max_depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Resolve create references
|
||||
if let Some(create) = &mut self.create {
|
||||
let mut graph = HashMap::with_capacity(create.len());
|
||||
for (id, obj) in create.iter_mut() {
|
||||
obj.eval_object_references(
|
||||
response,
|
||||
&mut Graph::Some {
|
||||
child_id: &*id,
|
||||
graph: &mut graph,
|
||||
},
|
||||
0,
|
||||
max_depth,
|
||||
eval_strings,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Perform topological sort
|
||||
if !graph.is_empty() {
|
||||
self.create = topological_sort(create, graph)?.into();
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve update references
|
||||
if let Some(update) = &mut self.update {
|
||||
for obj in update.values_mut() {
|
||||
obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve destroy references
|
||||
if let Some(MaybeResultReference::Reference(reference)) = &self.destroy {
|
||||
self.destroy = Some(MaybeResultReference::Value(
|
||||
response
|
||||
.eval_result_references(reference)?
|
||||
.into_ids::<Id>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: JmapObject> ResolveSetReference for CopyRequest<'x, T> {
|
||||
fn resolve_references(
|
||||
&mut self,
|
||||
response: &Response<'_>,
|
||||
max_depth: usize,
|
||||
eval_strings: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Resolve create references
|
||||
for (id, obj) in self.create.iter_mut() {
|
||||
obj.eval_object_references(response, &mut Graph::None, 0, max_depth, eval_strings)?;
|
||||
|
||||
if let MaybeIdReference::Reference(ir) = id {
|
||||
*id = MaybeIdReference::Id(response.eval_id_reference(ir)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JmapObject> ResolveReference for ParseRequest<T> {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
|
||||
// Resolve blobId references
|
||||
for id in self.blob_ids.iter_mut() {
|
||||
if let MaybeIdReference::Reference(ir) = id {
|
||||
*id = MaybeIdReference::Id(response.eval_blob_id_reference(ir)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveReference for ImportEmailRequest {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
|
||||
// Resolve email mailbox references
|
||||
for email in self.emails.values_mut() {
|
||||
match &mut email.mailbox_ids {
|
||||
MaybeResultReference::Reference(reference) => {
|
||||
email.mailbox_ids = MaybeResultReference::Value(
|
||||
response
|
||||
.eval_result_references(reference)?
|
||||
.into_ids::<Id>(reference)
|
||||
.map(|f| f.map(MaybeIdReference::Id))
|
||||
.collect::<Result<_, _>>()?,
|
||||
);
|
||||
}
|
||||
MaybeResultReference::Value(values) => {
|
||||
for value in values {
|
||||
if let MaybeIdReference::Reference(ir) = value {
|
||||
*value = MaybeIdReference::Id(response.eval_id_reference(ir)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveReference for GetSearchSnippetRequest {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
|
||||
// Resolve emailIds references
|
||||
if let MaybeResultReference::Reference(reference) = &self.email_ids {
|
||||
self.email_ids = MaybeResultReference::Value(
|
||||
response
|
||||
.eval_result_references(reference)?
|
||||
.into_ids::<Id>(reference)
|
||||
.map(|f| f.map(MaybeInvalid::Value))
|
||||
.collect::<Result<_, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveReference for BlobUploadRequest {
|
||||
fn resolve_references(&mut self, response: &Response<'_>) -> trc::Result<()> {
|
||||
let mut graph = HashMap::with_capacity(self.create.len());
|
||||
for (create_id, object) in self.create.iter_mut() {
|
||||
for data in &mut object.data {
|
||||
if let DataSourceObject::Id { id, .. } = data
|
||||
&& let MaybeIdReference::Reference(parent_id) = id
|
||||
{
|
||||
match response.created_ids.get(parent_id) {
|
||||
Some(AnyId::BlobId(blob_id)) => {
|
||||
*id = MaybeIdReference::Id(blob_id.clone());
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(trc::JmapEvent::InvalidResultReference.into_err().details(
|
||||
format_compact!(
|
||||
"Id reference {parent_id:?} points to invalid type."
|
||||
),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
graph
|
||||
.entry(create_id.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(parent_id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform topological sort
|
||||
if !graph.is_empty() {
|
||||
self.create = topological_sort(&mut self.create, graph)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResolveCreatedReference<T::Property, T::Element> for SetResponse<T>
|
||||
where
|
||||
T: JmapObject,
|
||||
{
|
||||
fn get_created_id(&self, id_ref: &str) -> Option<AnyId> {
|
||||
self.created
|
||||
.get(id_ref)
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|v| v.get(&Key::Property(T::ID_PROPERTY)))
|
||||
.and_then(|v| v.as_element())
|
||||
.and_then(|v| v.as_any_id())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
object::{email::EmailComparator, file_node::FileNodeComparator},
|
||||
response::serialize::serialize_hex,
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Session {
|
||||
#[serde(rename(serialize = "capabilities"))]
|
||||
pub capabilities: VecMap<Capability, Capabilities>,
|
||||
#[serde(rename(serialize = "accounts"))]
|
||||
pub accounts: VecMap<Id, Account>,
|
||||
#[serde(rename(serialize = "primaryAccounts"))]
|
||||
pub primary_accounts: VecMap<Capability, Id>,
|
||||
#[serde(rename(serialize = "username"))]
|
||||
pub username: String,
|
||||
#[serde(rename(serialize = "apiUrl"))]
|
||||
pub api_url: String,
|
||||
#[serde(rename(serialize = "downloadUrl"))]
|
||||
pub download_url: String,
|
||||
#[serde(rename(serialize = "uploadUrl"))]
|
||||
pub upload_url: String,
|
||||
#[serde(rename(serialize = "eventSourceUrl"))]
|
||||
pub event_source_url: String,
|
||||
#[serde(rename(serialize = "state"))]
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
pub state: u32,
|
||||
#[serde(skip)]
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct Account {
|
||||
#[serde(rename(serialize = "name"))]
|
||||
pub name: String,
|
||||
#[serde(rename(serialize = "isPersonal"))]
|
||||
pub is_personal: bool,
|
||||
#[serde(rename(serialize = "isReadOnly"))]
|
||||
pub is_read_only: bool,
|
||||
#[serde(rename(serialize = "accountCapabilities"))]
|
||||
pub account_capabilities: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
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:contacts:parse"))]
|
||||
ContactsParse = 1 << 5,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:calendars"))]
|
||||
Calendars = 1 << 6,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:calendars:parse"))]
|
||||
CalendarsParse = 1 << 7,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:websocket"))]
|
||||
WebSocket = 1 << 8,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))]
|
||||
Sieve = 1 << 9,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:blob"))]
|
||||
Blob = 1 << 10,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:quota"))]
|
||||
Quota = 1 << 11,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:principals"))]
|
||||
Principals = 1 << 12,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:principals:owner"))]
|
||||
PrincipalsOwner = 1 << 13,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:principals:availability"))]
|
||||
PrincipalsAvailability = 1 << 14,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:filenode"))]
|
||||
FileNode = 1 << 15,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:mail:share"))]
|
||||
MailShare = 1 << 16,
|
||||
#[serde(rename(serialize = "urn:stalwart:jmap"))]
|
||||
Stalwart = 1 << 17,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:webpush-vapid"))]
|
||||
WebPushVapid = 1 << 18,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:emailpush"))]
|
||||
EmailPush = 1 << 19,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct CapabilityIds(pub u32);
|
||||
|
||||
impl CapabilityIds {
|
||||
pub fn contains(&self, capability: Capability) -> bool {
|
||||
self.0 & capability as u32 != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Capabilities {
|
||||
Core(CoreCapabilities),
|
||||
Mail(MailCapabilities),
|
||||
Submission(SubmissionCapabilities),
|
||||
WebSocket(WebSocketCapabilities),
|
||||
SieveAccount(SieveAccountCapabilities),
|
||||
SieveSession(SieveSessionCapabilities),
|
||||
Blob(BlobCapabilities),
|
||||
Contacts(ContactsCapabilities),
|
||||
Principals(PrincipalCapabilities),
|
||||
PrincipalsAvailability(PrincipalAvailabilityCapabilities),
|
||||
Calendar(CalendarCapabilities),
|
||||
FileNode(FileNodeCapabilities),
|
||||
WebPush(WebPushCapabilities),
|
||||
Empty(EmptyCapabilities),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct CoreCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeUpload"))]
|
||||
pub max_size_upload: u64,
|
||||
#[serde(rename(serialize = "maxConcurrentUpload"))]
|
||||
pub max_concurrent_upload: u64,
|
||||
#[serde(rename(serialize = "maxSizeRequest"))]
|
||||
pub max_size_request: u64,
|
||||
#[serde(rename(serialize = "maxConcurrentRequests"))]
|
||||
pub max_concurrent_requests: u64,
|
||||
#[serde(rename(serialize = "maxCallsInRequest"))]
|
||||
pub max_calls_in_request: u64,
|
||||
#[serde(rename(serialize = "maxObjectsInGet"))]
|
||||
pub max_objects_in_get: u64,
|
||||
#[serde(rename(serialize = "maxObjectsInSet"))]
|
||||
pub max_objects_in_set: u64,
|
||||
#[serde(rename(serialize = "collationAlgorithms"))]
|
||||
pub collation_algorithms: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct WebSocketCapabilities {
|
||||
#[serde(rename(serialize = "url"))]
|
||||
pub url: String,
|
||||
#[serde(rename(serialize = "supportsPush"))]
|
||||
pub supports_push: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SieveSessionCapabilities {
|
||||
#[serde(rename(serialize = "implementation"))]
|
||||
pub implementation: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SieveAccountCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeScriptName"))]
|
||||
pub max_script_name: u64,
|
||||
#[serde(rename(serialize = "maxSizeScript"))]
|
||||
pub max_script_size: u64,
|
||||
#[serde(rename(serialize = "maxNumberScripts"))]
|
||||
pub max_scripts: u64,
|
||||
#[serde(rename(serialize = "maxNumberRedirects"))]
|
||||
pub max_redirects: u64,
|
||||
#[serde(rename(serialize = "sieveExtensions"))]
|
||||
pub extensions: Vec<String>,
|
||||
#[serde(rename(serialize = "notificationMethods"))]
|
||||
pub notification_methods: Option<Vec<String>>,
|
||||
#[serde(rename(serialize = "externalLists"))]
|
||||
pub ext_lists: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct MailCapabilities {
|
||||
#[serde(rename(serialize = "maxMailboxesPerEmail"))]
|
||||
pub max_mailboxes_per_email: Option<u64>,
|
||||
#[serde(rename(serialize = "maxMailboxDepth"))]
|
||||
pub max_mailbox_depth: u64,
|
||||
#[serde(rename(serialize = "maxSizeMailboxName"))]
|
||||
pub max_size_mailbox_name: u64,
|
||||
#[serde(rename(serialize = "maxSizeAttachmentsPerEmail"))]
|
||||
pub max_size_attachments_per_email: u64,
|
||||
#[serde(rename(serialize = "emailQuerySortOptions"))]
|
||||
pub email_query_sort_options: Vec<EmailComparator>,
|
||||
#[serde(rename(serialize = "mayCreateTopLevelMailbox"))]
|
||||
pub may_create_top_level_mailbox: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SubmissionCapabilities {
|
||||
#[serde(rename(serialize = "maxDelayedSend"))]
|
||||
pub max_delayed_send: u64,
|
||||
#[serde(rename(serialize = "submissionExtensions"))]
|
||||
pub submission_extensions: VecMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct BlobCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeBlobSet"))]
|
||||
pub max_size_blob_set: u64,
|
||||
#[serde(rename(serialize = "maxDataSources"))]
|
||||
pub max_data_sources: u64,
|
||||
#[serde(rename(serialize = "supportedTypeNames"))]
|
||||
pub supported_type_names: Vec<DataType>,
|
||||
#[serde(rename(serialize = "supportedDigestAlgorithms"))]
|
||||
pub supported_digest_algorithms: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct CalendarCapabilities {
|
||||
#[serde(rename(serialize = "maxCalendarsPerEvent"))]
|
||||
pub max_calendars_per_event: Option<u64>,
|
||||
#[serde(rename(serialize = "minDateTime"))]
|
||||
pub min_date_time: UTCDate,
|
||||
#[serde(rename(serialize = "maxDateTime"))]
|
||||
pub max_date_time: UTCDate,
|
||||
#[serde(rename(serialize = "maxExpandedQueryDuration"))]
|
||||
pub max_expanded_query_duration: String,
|
||||
#[serde(rename(serialize = "maxParticipantsPerEvent"))]
|
||||
pub max_participants_per_event: Option<u64>,
|
||||
#[serde(rename(serialize = "mayCreateCalendar"))]
|
||||
pub may_create_calendar: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ContactsCapabilities {
|
||||
#[serde(rename(serialize = "maxAddressBooksPerCard"))]
|
||||
pub max_address_books_per_card: Option<u64>,
|
||||
#[serde(rename(serialize = "mayCreateAddressBook"))]
|
||||
pub may_create_address_book: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct PrincipalAvailabilityCapabilities {
|
||||
#[serde(rename(serialize = "maxAvailabilityDuration"))]
|
||||
pub max_availability_duration: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct PrincipalCapabilities {
|
||||
#[serde(rename(serialize = "currentUserPrincipalId"))]
|
||||
pub current_user_principal_id: Option<Id>,
|
||||
}
|
||||
|
||||
/*#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct PrincipalOwnerCapabilities {
|
||||
#[serde(rename(serialize = "accountIdForPrincipal"))]
|
||||
pub account_id_for_principal: Id,
|
||||
|
||||
#[serde(rename(serialize = "principalId"))]
|
||||
pub principal_id: Id,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct PrincipalCalendarCapabilities {
|
||||
#[serde(rename(serialize = "accountIdForPrincipal"))]
|
||||
pub account_id_for_principal: Option<Id>,
|
||||
#[serde(rename(serialize = "mayGetAvailability"))]
|
||||
pub may_get_availability: bool,
|
||||
#[serde(rename(serialize = "mayShareWith"))]
|
||||
pub may_share_with: bool,
|
||||
#[serde(rename(serialize = "calendarAddress"))]
|
||||
pub calendar_address: String,
|
||||
}*/
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct FileNodeCapabilities {
|
||||
#[serde(rename(serialize = "maxFileNodeDepth"))]
|
||||
pub max_file_node_depth: Option<u64>,
|
||||
#[serde(rename(serialize = "maxSizeFileNodeName"))]
|
||||
pub max_size_file_node_name: u64,
|
||||
#[serde(rename(serialize = "forbiddenNameChars"))]
|
||||
pub forbidden_name_chars: Option<String>,
|
||||
#[serde(rename(serialize = "forbiddenNodeNames"))]
|
||||
pub forbidden_node_names: Option<Vec<String>>,
|
||||
#[serde(rename(serialize = "fileNodeQuerySortOptions"))]
|
||||
pub file_node_query_sort_options: Vec<FileNodeComparator>,
|
||||
#[serde(rename(serialize = "mayCreateTopLevelFileNode"))]
|
||||
pub may_create_top_level_file_node: bool,
|
||||
#[serde(rename(serialize = "caseInsensitiveNames"))]
|
||||
pub case_insensitive_names: bool,
|
||||
#[serde(rename(serialize = "webTrashUrl"))]
|
||||
pub web_trash_url: Option<String>,
|
||||
#[serde(rename(serialize = "webUrlTemplate"))]
|
||||
pub web_url_template: Option<String>,
|
||||
#[serde(rename(serialize = "webWriteUrlTemplate"))]
|
||||
pub web_write_url_template: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct WebPushCapabilities {
|
||||
#[serde(rename(serialize = "applicationServerKey"))]
|
||||
pub application_server_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct EmptyCapabilities {}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct BaseCapabilities {
|
||||
pub session: VecMap<Capability, Capabilities>,
|
||||
pub account: AHashMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Capability::Core => "urn:ietf:params:jmap:core",
|
||||
Capability::Mail => "urn:ietf:params:jmap:mail",
|
||||
Capability::Submission => "urn:ietf:params:jmap:submission",
|
||||
Capability::VacationResponse => "urn:ietf:params:jmap:vacationresponse",
|
||||
Capability::Contacts => "urn:ietf:params:jmap:contacts",
|
||||
Capability::ContactsParse => "urn:ietf:params:jmap:contacts:parse",
|
||||
Capability::Calendars => "urn:ietf:params:jmap:calendars",
|
||||
Capability::CalendarsParse => "urn:ietf:params:jmap:calendars:parse",
|
||||
Capability::WebSocket => "urn:ietf:params:jmap:websocket",
|
||||
Capability::Sieve => "urn:ietf:params:jmap:sieve",
|
||||
Capability::Blob => "urn:ietf:params:jmap:blob",
|
||||
Capability::Quota => "urn:ietf:params:jmap:quota",
|
||||
Capability::Principals => "urn:ietf:params:jmap:principals",
|
||||
Capability::PrincipalsOwner => "urn:ietf:params:jmap:principals:owner",
|
||||
Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability",
|
||||
Capability::FileNode => "urn:ietf:params:jmap:filenode",
|
||||
Capability::MailShare => "urn:ietf:params:jmap:mail:share",
|
||||
Capability::Stalwart => "urn:stalwart:jmap",
|
||||
Capability::WebPushVapid => "urn:ietf:params:jmap:webpush-vapid",
|
||||
Capability::EmailPush => "urn:ietf:params:jmap:emailpush",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_capabilities() -> &'static [Capability] {
|
||||
&[
|
||||
Capability::Core,
|
||||
Capability::Mail,
|
||||
Capability::Submission,
|
||||
Capability::VacationResponse,
|
||||
Capability::Contacts,
|
||||
Capability::ContactsParse,
|
||||
Capability::Calendars,
|
||||
Capability::CalendarsParse,
|
||||
Capability::WebSocket,
|
||||
Capability::Sieve,
|
||||
Capability::Blob,
|
||||
Capability::Quota,
|
||||
Capability::Principals,
|
||||
Capability::PrincipalsAvailability,
|
||||
Capability::FileNode,
|
||||
Capability::MailShare,
|
||||
Capability::Stalwart,
|
||||
Capability::WebPushVapid,
|
||||
Capability::EmailPush,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(base_url: impl Into<String>, base_capabilities: &BaseCapabilities) -> Session {
|
||||
let base_url = base_url.into();
|
||||
let mut capabilities = base_capabilities.session.clone();
|
||||
capabilities.append(
|
||||
Capability::WebSocket,
|
||||
Capabilities::WebSocket(WebSocketCapabilities::new(&base_url)),
|
||||
);
|
||||
|
||||
Session {
|
||||
capabilities,
|
||||
accounts: VecMap::new(),
|
||||
primary_accounts: VecMap::new(),
|
||||
username: "".to_string(),
|
||||
api_url: format!("{}/jmap/", base_url),
|
||||
download_url: format!(
|
||||
"{}/jmap/download/{{accountId}}/{{blobId}}/{{name}}?accept={{type}}",
|
||||
base_url
|
||||
),
|
||||
upload_url: format!("{}/jmap/upload/{{accountId}}/", base_url),
|
||||
event_source_url: format!(
|
||||
"{}/jmap/eventsource/?types={{types}}&closeafter={{closeafter}}&ping={{ping}}",
|
||||
base_url
|
||||
),
|
||||
base_url,
|
||||
state: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: u32) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &str {
|
||||
&self.api_url
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveSessionCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
implementation: "Stalwart v1.0.0",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketCapabilities {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
WebSocketCapabilities {
|
||||
url: format!(
|
||||
"ws{}/jmap/ws",
|
||||
base_url.strip_prefix("http").unwrap_or_default()
|
||||
),
|
||||
supports_push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
pub fn to_account_capabilities(
|
||||
&self,
|
||||
current_user_principal_id: Option<Id>,
|
||||
may_create: bool,
|
||||
) -> Capabilities {
|
||||
match self {
|
||||
Capabilities::Contacts(contacts_capabilities) => {
|
||||
Capabilities::Contacts(ContactsCapabilities {
|
||||
may_create_address_book: may_create,
|
||||
..contacts_capabilities.clone()
|
||||
})
|
||||
}
|
||||
Capabilities::Principals(_) => Capabilities::Principals(PrincipalCapabilities {
|
||||
current_user_principal_id,
|
||||
}),
|
||||
Capabilities::Calendar(calendar_capabilities) => {
|
||||
Capabilities::Calendar(CalendarCapabilities {
|
||||
may_create_calendar: may_create,
|
||||
..calendar_capabilities.clone()
|
||||
})
|
||||
}
|
||||
Capabilities::FileNode(file_node_capabilities) => {
|
||||
Capabilities::FileNode(FileNodeCapabilities {
|
||||
may_create_top_level_file_node: may_create,
|
||||
..file_node_capabilities.clone()
|
||||
})
|
||||
}
|
||||
_ => self.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(s.as_bytes(),
|
||||
"urn:ietf:params:jmap:core" => Capability::Core,
|
||||
"urn:ietf:params:jmap:mail" => Capability::Mail,
|
||||
"urn:ietf:params:jmap:submission" => Capability::Submission,
|
||||
"urn:ietf:params:jmap:vacationresponse" => Capability::VacationResponse,
|
||||
"urn:ietf:params:jmap:contacts" => Capability::Contacts,
|
||||
"urn:ietf:params:jmap:calendars" => Capability::Calendars,
|
||||
"urn:ietf:params:jmap:websocket" => Capability::WebSocket,
|
||||
"urn:ietf:params:jmap:sieve" => Capability::Sieve,
|
||||
"urn:ietf:params:jmap:blob" => Capability::Blob,
|
||||
"urn:ietf:params:jmap:quota" => Capability::Quota,
|
||||
"urn:ietf:params:jmap:principals" => Capability::Principals,
|
||||
"urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner,
|
||||
"urn:ietf:params:jmap:filenode" => Capability::FileNode,
|
||||
"urn:ietf:params:jmap:principals:availability" => Capability::PrincipalsAvailability,
|
||||
"urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse,
|
||||
"urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse,
|
||||
"urn:ietf:params:jmap:mail:share" => Capability::MailShare,
|
||||
"urn:stalwart:jmap" => Capability::Stalwart,
|
||||
"urn:ietf:params:jmap:webpush-vapid" => Capability::WebPushVapid,
|
||||
"urn:ietf:params:jmap:emailpush" => Capability::EmailPush,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityIds {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct CapabilityIdsVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for CapabilityIdsVisitor {
|
||||
type Value = CapabilityIds;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an array of capability strings")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut capability_flags = 0u32;
|
||||
|
||||
while let Some(capability_str) = seq.next_element::<std::borrow::Cow<str>>()? {
|
||||
let capability =
|
||||
Capability::parse(capability_str.as_ref()).ok_or_else(|| {
|
||||
serde::de::Error::custom(format!(
|
||||
"Unknown capability: {capability_str:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
capability_flags |= capability as u32;
|
||||
}
|
||||
|
||||
Ok(CapabilityIds(capability_flags))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(CapabilityIdsVisitor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use serde::{
|
||||
Deserializer,
|
||||
de::{self, MapAccess, Visitor},
|
||||
};
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
pub trait DeserializeArguments<'de> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: MapAccess<'de>;
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for () {
|
||||
fn deserialize_argument<A>(&mut self, _key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let _: de::IgnoredAny = map.next_value()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_request<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: DeserializeArguments<'de> + Default,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct DirectArgumentsVisitor<T> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> DirectArgumentsVisitor<T> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Visitor<'de> for DirectArgumentsVisitor<T>
|
||||
where
|
||||
T: DeserializeArguments<'de> + Default,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a JMAP request object")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut target = T::default();
|
||||
|
||||
while let Some(key) = map.next_key::<&str>()? {
|
||||
target
|
||||
.deserialize_argument(key, &mut map)
|
||||
.map_err(de::Error::custom)?;
|
||||
}
|
||||
|
||||
Ok(target)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(DirectArgumentsVisitor::<T>::new())
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::request::capability::Capability;
|
||||
use registry::{
|
||||
schema::prelude::{OBJ_SINGLETON, ObjectType},
|
||||
types::EnumImpl,
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
#[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,
|
||||
Quota,
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventNotification,
|
||||
AddressBook,
|
||||
ContactCard,
|
||||
FileNode,
|
||||
ParticipantIdentity,
|
||||
ShareNotification,
|
||||
Registry(ObjectType),
|
||||
}
|
||||
|
||||
impl MethodObject {
|
||||
pub fn capability(&self) -> Capability {
|
||||
match self {
|
||||
MethodObject::Email
|
||||
| MethodObject::Mailbox
|
||||
| MethodObject::Thread
|
||||
| MethodObject::SearchSnippet => Capability::Mail,
|
||||
MethodObject::Core | MethodObject::PushSubscription => Capability::Core,
|
||||
MethodObject::Blob => Capability::Blob,
|
||||
MethodObject::Identity | MethodObject::EmailSubmission => Capability::Submission,
|
||||
MethodObject::VacationResponse => Capability::VacationResponse,
|
||||
MethodObject::SieveScript => Capability::Sieve,
|
||||
MethodObject::Principal | MethodObject::ShareNotification => Capability::Principals,
|
||||
MethodObject::Quota => Capability::Quota,
|
||||
MethodObject::Calendar
|
||||
| MethodObject::CalendarEvent
|
||||
| MethodObject::CalendarEventNotification
|
||||
| MethodObject::ParticipantIdentity => Capability::Calendars,
|
||||
MethodObject::AddressBook | MethodObject::ContactCard => Capability::Contacts,
|
||||
MethodObject::FileNode => Capability::FileNode,
|
||||
MethodObject::Registry(_) => Capability::Stalwart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MethodFunction {
|
||||
Get,
|
||||
Set,
|
||||
Changes,
|
||||
Query,
|
||||
QueryChanges,
|
||||
Copy,
|
||||
Import,
|
||||
Parse,
|
||||
Validate,
|
||||
Lookup,
|
||||
Upload,
|
||||
Echo,
|
||||
GetAvailability,
|
||||
}
|
||||
|
||||
impl Display for MethodName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl MethodName {
|
||||
pub fn new(obj: MethodObject, fnc: MethodFunction) -> Self {
|
||||
Self { obj, fnc }
|
||||
}
|
||||
|
||||
pub fn error() -> Self {
|
||||
Self {
|
||||
obj: MethodObject::Thread,
|
||||
fnc: MethodFunction::Echo,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Cow<'static, str> {
|
||||
match (self.fnc, self.obj) {
|
||||
(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",
|
||||
(MethodFunction::Changes, MethodObject::Principal) => "Principal/changes",
|
||||
(MethodFunction::QueryChanges, MethodObject::Principal) => "Principal/queryChanges",
|
||||
(MethodFunction::GetAvailability, MethodObject::Principal) => {
|
||||
"Principal/getAvailability"
|
||||
}
|
||||
|
||||
(MethodFunction::Get, MethodObject::Quota) => "Quota/get",
|
||||
(MethodFunction::Changes, MethodObject::Quota) => "Quota/changes",
|
||||
(MethodFunction::Query, MethodObject::Quota) => "Quota/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::Quota) => "Quota/queryChanges",
|
||||
|
||||
(MethodFunction::Get, MethodObject::Blob) => "Blob/get",
|
||||
(MethodFunction::Copy, MethodObject::Blob) => "Blob/copy",
|
||||
(MethodFunction::Lookup, MethodObject::Blob) => "Blob/lookup",
|
||||
(MethodFunction::Upload, MethodObject::Blob) => "Blob/upload",
|
||||
|
||||
(MethodFunction::Get, MethodObject::AddressBook) => "AddressBook/get",
|
||||
(MethodFunction::Changes, MethodObject::AddressBook) => "AddressBook/changes",
|
||||
(MethodFunction::Set, MethodObject::AddressBook) => "AddressBook/set",
|
||||
(MethodFunction::Query, MethodObject::AddressBook) => "AddressBook/query",
|
||||
|
||||
(MethodFunction::Get, MethodObject::ContactCard) => "ContactCard/get",
|
||||
(MethodFunction::Changes, MethodObject::ContactCard) => "ContactCard/changes",
|
||||
(MethodFunction::Query, MethodObject::ContactCard) => "ContactCard/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::ContactCard) => "ContactCard/queryChanges",
|
||||
(MethodFunction::Set, MethodObject::ContactCard) => "ContactCard/set",
|
||||
(MethodFunction::Copy, MethodObject::ContactCard) => "ContactCard/copy",
|
||||
(MethodFunction::Parse, MethodObject::ContactCard) => "ContactCard/parse",
|
||||
|
||||
(MethodFunction::Get, MethodObject::FileNode) => "FileNode/get",
|
||||
(MethodFunction::Changes, MethodObject::FileNode) => "FileNode/changes",
|
||||
(MethodFunction::Query, MethodObject::FileNode) => "FileNode/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::FileNode) => "FileNode/queryChanges",
|
||||
(MethodFunction::Set, MethodObject::FileNode) => "FileNode/set",
|
||||
(MethodFunction::Copy, MethodObject::FileNode) => "FileNode/copy",
|
||||
|
||||
(MethodFunction::Get, MethodObject::ShareNotification) => "ShareNotification/get",
|
||||
(MethodFunction::Changes, MethodObject::ShareNotification) => {
|
||||
"ShareNotification/changes"
|
||||
}
|
||||
(MethodFunction::Query, MethodObject::ShareNotification) => "ShareNotification/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::ShareNotification) => {
|
||||
"ShareNotification/queryChanges"
|
||||
}
|
||||
(MethodFunction::Set, MethodObject::ShareNotification) => "ShareNotification/set",
|
||||
|
||||
(MethodFunction::Get, MethodObject::Calendar) => "Calendar/get",
|
||||
(MethodFunction::Changes, MethodObject::Calendar) => "Calendar/changes",
|
||||
(MethodFunction::Set, MethodObject::Calendar) => "Calendar/set",
|
||||
(MethodFunction::Query, MethodObject::Calendar) => "Calendar/query",
|
||||
|
||||
(MethodFunction::Get, MethodObject::CalendarEvent) => "CalendarEvent/get",
|
||||
(MethodFunction::Changes, MethodObject::CalendarEvent) => "CalendarEvent/changes",
|
||||
(MethodFunction::Query, MethodObject::CalendarEvent) => "CalendarEvent/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::CalendarEvent) => {
|
||||
"CalendarEvent/queryChanges"
|
||||
}
|
||||
(MethodFunction::Set, MethodObject::CalendarEvent) => "CalendarEvent/set",
|
||||
(MethodFunction::Copy, MethodObject::CalendarEvent) => "CalendarEvent/copy",
|
||||
(MethodFunction::Parse, MethodObject::CalendarEvent) => "CalendarEvent/parse",
|
||||
|
||||
(MethodFunction::Get, MethodObject::CalendarEventNotification) => {
|
||||
"CalendarEventNotification/get"
|
||||
}
|
||||
(MethodFunction::Changes, MethodObject::CalendarEventNotification) => {
|
||||
"CalendarEventNotification/changes"
|
||||
}
|
||||
(MethodFunction::Query, MethodObject::CalendarEventNotification) => {
|
||||
"CalendarEventNotification/query"
|
||||
}
|
||||
(MethodFunction::QueryChanges, MethodObject::CalendarEventNotification) => {
|
||||
"CalendarEventNotification/queryChanges"
|
||||
}
|
||||
(MethodFunction::Set, MethodObject::CalendarEventNotification) => {
|
||||
"CalendarEventNotification/set"
|
||||
}
|
||||
|
||||
(MethodFunction::Get, MethodObject::ParticipantIdentity) => "ParticipantIdentity/get",
|
||||
(MethodFunction::Changes, MethodObject::ParticipantIdentity) => {
|
||||
"ParticipantIdentity/changes"
|
||||
}
|
||||
(MethodFunction::Set, MethodObject::ParticipantIdentity) => "ParticipantIdentity/set",
|
||||
|
||||
(MethodFunction::Echo, MethodObject::Core) => "Core/echo",
|
||||
(method, MethodObject::Registry(obj)) => {
|
||||
return Cow::Owned(format!("x:{}/{}", obj.as_str(), method.as_str()));
|
||||
}
|
||||
_ => "error",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(s.as_bytes(),
|
||||
"PushSubscription/get" => (MethodObject::PushSubscription, MethodFunction::Get),
|
||||
"PushSubscription/set" => (MethodObject::PushSubscription, MethodFunction::Set),
|
||||
|
||||
"Mailbox/get" => (MethodObject::Mailbox, MethodFunction::Get),
|
||||
"Mailbox/changes" => (MethodObject::Mailbox, MethodFunction::Changes),
|
||||
"Mailbox/query" => (MethodObject::Mailbox, MethodFunction::Query),
|
||||
"Mailbox/queryChanges" => (MethodObject::Mailbox, MethodFunction::QueryChanges),
|
||||
"Mailbox/set" => (MethodObject::Mailbox, MethodFunction::Set),
|
||||
|
||||
"Thread/get" => (MethodObject::Thread, MethodFunction::Get),
|
||||
"Thread/changes" => (MethodObject::Thread, MethodFunction::Changes),
|
||||
|
||||
"Email/get" => (MethodObject::Email, MethodFunction::Get),
|
||||
"Email/changes" => (MethodObject::Email, MethodFunction::Changes),
|
||||
"Email/query" => (MethodObject::Email, MethodFunction::Query),
|
||||
"Email/queryChanges" => (MethodObject::Email, MethodFunction::QueryChanges),
|
||||
"Email/set" => (MethodObject::Email, MethodFunction::Set),
|
||||
"Email/copy" => (MethodObject::Email, MethodFunction::Copy),
|
||||
"Email/import" => (MethodObject::Email, MethodFunction::Import),
|
||||
"Email/parse" => (MethodObject::Email, MethodFunction::Parse),
|
||||
|
||||
"SearchSnippet/get" => (MethodObject::SearchSnippet, MethodFunction::Get),
|
||||
|
||||
"Identity/get" => (MethodObject::Identity, MethodFunction::Get),
|
||||
"Identity/changes" => (MethodObject::Identity, MethodFunction::Changes),
|
||||
"Identity/set" => (MethodObject::Identity, MethodFunction::Set),
|
||||
|
||||
"EmailSubmission/get" => (MethodObject::EmailSubmission, MethodFunction::Get),
|
||||
"EmailSubmission/changes" => (MethodObject::EmailSubmission, MethodFunction::Changes),
|
||||
"EmailSubmission/query" => (MethodObject::EmailSubmission, MethodFunction::Query),
|
||||
"EmailSubmission/queryChanges" => (MethodObject::EmailSubmission, MethodFunction::QueryChanges),
|
||||
"EmailSubmission/set" => (MethodObject::EmailSubmission, MethodFunction::Set),
|
||||
|
||||
"VacationResponse/get" => (MethodObject::VacationResponse, MethodFunction::Get),
|
||||
"VacationResponse/set" => (MethodObject::VacationResponse, MethodFunction::Set),
|
||||
|
||||
"SieveScript/get" => (MethodObject::SieveScript, MethodFunction::Get),
|
||||
"SieveScript/set" => (MethodObject::SieveScript, MethodFunction::Set),
|
||||
"SieveScript/query" => (MethodObject::SieveScript, MethodFunction::Query),
|
||||
"SieveScript/validate" => (MethodObject::SieveScript, MethodFunction::Validate),
|
||||
|
||||
"Principal/get" => (MethodObject::Principal, MethodFunction::Get),
|
||||
"Principal/set" => (MethodObject::Principal, MethodFunction::Set),
|
||||
"Principal/query" => (MethodObject::Principal, MethodFunction::Query),
|
||||
"Principal/changes" => (MethodObject::Principal, MethodFunction::Changes),
|
||||
"Principal/queryChanges" => (MethodObject::Principal, MethodFunction::QueryChanges),
|
||||
"Principal/getAvailability" => (MethodObject::Principal, MethodFunction::GetAvailability),
|
||||
|
||||
"Quota/get" => (MethodObject::Quota, MethodFunction::Get),
|
||||
"Quota/changes" => (MethodObject::Quota, MethodFunction::Changes),
|
||||
"Quota/query" => (MethodObject::Quota, MethodFunction::Query),
|
||||
"Quota/queryChanges" => (MethodObject::Quota, MethodFunction::QueryChanges),
|
||||
|
||||
"Blob/get" => (MethodObject::Blob, MethodFunction::Get),
|
||||
"Blob/copy" => (MethodObject::Blob, MethodFunction::Copy),
|
||||
"Blob/lookup" => (MethodObject::Blob, MethodFunction::Lookup),
|
||||
"Blob/upload" => (MethodObject::Blob, MethodFunction::Upload),
|
||||
|
||||
"AddressBook/get" => (MethodObject::AddressBook, MethodFunction::Get),
|
||||
"AddressBook/changes" => (MethodObject::AddressBook, MethodFunction::Changes),
|
||||
"AddressBook/set" => (MethodObject::AddressBook, MethodFunction::Set),
|
||||
"AddressBook/query" => (MethodObject::AddressBook, MethodFunction::Query),
|
||||
|
||||
"ContactCard/get" => (MethodObject::ContactCard, MethodFunction::Get),
|
||||
"ContactCard/changes" => (MethodObject::ContactCard, MethodFunction::Changes),
|
||||
"ContactCard/query" => (MethodObject::ContactCard, MethodFunction::Query),
|
||||
"ContactCard/queryChanges" => (MethodObject::ContactCard, MethodFunction::QueryChanges),
|
||||
"ContactCard/set" => (MethodObject::ContactCard, MethodFunction::Set),
|
||||
"ContactCard/copy" => (MethodObject::ContactCard, MethodFunction::Copy),
|
||||
"ContactCard/parse" => (MethodObject::ContactCard, MethodFunction::Parse),
|
||||
|
||||
"FileNode/get" => (MethodObject::FileNode, MethodFunction::Get),
|
||||
"FileNode/changes" => (MethodObject::FileNode, MethodFunction::Changes),
|
||||
"FileNode/query" => (MethodObject::FileNode, MethodFunction::Query),
|
||||
"FileNode/queryChanges" => (MethodObject::FileNode, MethodFunction::QueryChanges),
|
||||
"FileNode/set" => (MethodObject::FileNode, MethodFunction::Set),
|
||||
"FileNode/copy" => (MethodObject::FileNode, MethodFunction::Copy),
|
||||
|
||||
"ShareNotification/get" => (MethodObject::ShareNotification, MethodFunction::Get),
|
||||
"ShareNotification/changes" => (MethodObject::ShareNotification, MethodFunction::Changes),
|
||||
"ShareNotification/set" => (MethodObject::ShareNotification, MethodFunction::Set),
|
||||
"ShareNotification/query" => (MethodObject::ShareNotification, MethodFunction::Query),
|
||||
"ShareNotification/queryChanges" => (MethodObject::ShareNotification, MethodFunction::QueryChanges),
|
||||
|
||||
"Calendar/get" => (MethodObject::Calendar, MethodFunction::Get),
|
||||
"Calendar/changes" => (MethodObject::Calendar, MethodFunction::Changes),
|
||||
"Calendar/set" => (MethodObject::Calendar, MethodFunction::Set),
|
||||
"Calendar/query" => (MethodObject::Calendar, MethodFunction::Query),
|
||||
|
||||
"CalendarEvent/get" => (MethodObject::CalendarEvent, MethodFunction::Get),
|
||||
"CalendarEvent/changes" => (MethodObject::CalendarEvent, MethodFunction::Changes),
|
||||
"CalendarEvent/query" => (MethodObject::CalendarEvent, MethodFunction::Query),
|
||||
"CalendarEvent/queryChanges" => (MethodObject::CalendarEvent, MethodFunction::QueryChanges),
|
||||
"CalendarEvent/set" => (MethodObject::CalendarEvent, MethodFunction::Set),
|
||||
"CalendarEvent/copy" => (MethodObject::CalendarEvent, MethodFunction::Copy),
|
||||
"CalendarEvent/parse" => (MethodObject::CalendarEvent, MethodFunction::Parse),
|
||||
|
||||
"CalendarEventNotification/get" => (MethodObject::CalendarEventNotification, MethodFunction::Get),
|
||||
"CalendarEventNotification/changes" => (MethodObject::CalendarEventNotification, MethodFunction::Changes),
|
||||
"CalendarEventNotification/set" => (MethodObject::CalendarEventNotification, MethodFunction::Set),
|
||||
"CalendarEventNotification/query" => (MethodObject::CalendarEventNotification, MethodFunction::Query),
|
||||
"CalendarEventNotification/queryChanges" => (MethodObject::CalendarEventNotification, MethodFunction::QueryChanges),
|
||||
|
||||
"ParticipantIdentity/get" => (MethodObject::ParticipantIdentity, MethodFunction::Get),
|
||||
"ParticipantIdentity/changes" => (MethodObject::ParticipantIdentity, MethodFunction::Changes),
|
||||
"ParticipantIdentity/set" => (MethodObject::ParticipantIdentity, MethodFunction::Set),
|
||||
|
||||
"Core/echo" => (MethodObject::Core, MethodFunction::Echo),
|
||||
|
||||
).or_else(|| {
|
||||
let (obj, fnc) = s.strip_prefix("x:")?.split_once('/')?;
|
||||
let obj = ObjectType::parse(obj)?;
|
||||
let fnc = hashify::tiny_map!(fnc.as_bytes(),
|
||||
"get" => MethodFunction::Get,
|
||||
"set" => MethodFunction::Set,
|
||||
"query" => MethodFunction::Query,
|
||||
)?;
|
||||
|
||||
if obj.flags() & OBJ_SINGLETON == 0 || fnc != MethodFunction::Query {
|
||||
(MethodObject::Registry(obj), fnc).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).map(|(obj, fnc)| MethodName { obj, fnc })
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
MethodObject::Quota => "Quota",
|
||||
MethodObject::AddressBook => "AddressBook",
|
||||
MethodObject::ContactCard => "ContactCard",
|
||||
MethodObject::FileNode => "FileNode",
|
||||
MethodObject::ParticipantIdentity => "ParticipantIdentity",
|
||||
MethodObject::Calendar => "Calendar",
|
||||
MethodObject::CalendarEvent => "CalendarEvent",
|
||||
MethodObject::CalendarEventNotification => "CalendarEventNotification",
|
||||
MethodObject::ShareNotification => "ShareNotification",
|
||||
MethodObject::Registry(obj) => {
|
||||
f.write_str("x:")?;
|
||||
return f.write_str(obj.as_str());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MethodFunction {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
MethodFunction::Get => "get",
|
||||
MethodFunction::Set => "set",
|
||||
MethodFunction::Changes => "changes",
|
||||
MethodFunction::Query => "query",
|
||||
MethodFunction::QueryChanges => "queryChanges",
|
||||
MethodFunction::Copy => "copy",
|
||||
MethodFunction::Import => "import",
|
||||
MethodFunction::Parse => "parse",
|
||||
MethodFunction::Validate => "validate",
|
||||
MethodFunction::Lookup => "lookup",
|
||||
MethodFunction::Upload => "upload",
|
||||
MethodFunction::Echo => "echo",
|
||||
MethodFunction::GetAvailability => "getAvailability",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MethodObject {
|
||||
pub fn unwrap_registry(self) -> ObjectType {
|
||||
match self {
|
||||
MethodObject::Registry(obj) => obj,
|
||||
_ => panic!("Not a registry method object"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for MethodName {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <Cow<str>>::deserialize(deserializer)?;
|
||||
|
||||
MethodName::parse(value.as_ref())
|
||||
.ok_or_else(|| serde::de::Error::custom(format!("Invalid method name: {:?}", value)))
|
||||
}
|
||||
}
|
||||
|
||||
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().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod capability;
|
||||
pub mod deserialize;
|
||||
pub mod method;
|
||||
pub mod parser;
|
||||
pub mod reference;
|
||||
pub mod websocket;
|
||||
|
||||
use self::method::MethodName;
|
||||
use crate::{
|
||||
method::{
|
||||
availability::GetAvailabilityRequest,
|
||||
changes::ChangesRequest,
|
||||
copy::{CopyBlobRequest, CopyRequest},
|
||||
get::GetRequest,
|
||||
import::ImportEmailRequest,
|
||||
lookup::BlobLookupRequest,
|
||||
parse::ParseRequest,
|
||||
query::QueryRequest,
|
||||
query_changes::QueryChangesRequest,
|
||||
search_snippet::GetSearchSnippetRequest,
|
||||
set::SetRequest,
|
||||
upload::BlobUploadRequest,
|
||||
validate::ValidateSieveScriptRequest,
|
||||
},
|
||||
object::{
|
||||
AnyId, addressbook::AddressBook, blob::Blob, calendar::Calendar,
|
||||
calendar_event::CalendarEvent, calendar_event_notification::CalendarEventNotification,
|
||||
contact::ContactCard, email::Email, email_submission::EmailSubmission, file_node::FileNode,
|
||||
identity::Identity, mailbox::Mailbox, participant_identity::ParticipantIdentity,
|
||||
principal::Principal, push_subscription::PushSubscription, quota::Quota,
|
||||
registry::Registry, share_notification::ShareNotification, sieve::Sieve, thread::Thread,
|
||||
vacation_response::VacationResponse,
|
||||
},
|
||||
request::{capability::CapabilityIds, reference::MaybeIdReference},
|
||||
};
|
||||
use jmap_tools::{Null, Value};
|
||||
use std::{collections::HashMap, fmt::Debug, str::FromStr};
|
||||
use types::id::Id;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub const INVALID_ACCOUNT_ID: u64 = u64::MAX - 1;
|
||||
|
||||
pub fn deserialize_account_id<'de, A>(map: &mut A) -> Result<Id, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
Ok(map
|
||||
.next_value::<MaybeInvalid<Id>>()?
|
||||
.try_unwrap()
|
||||
.unwrap_or_else(|| Id::from(INVALID_ACCOUNT_ID)))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Request<'x> {
|
||||
pub using: CapabilityIds,
|
||||
pub method_calls: Vec<Call<RequestMethod<'x>>>,
|
||||
pub created_ids: Option<HashMap<String, AnyId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Call<T> {
|
||||
pub id: String,
|
||||
pub name: MethodName,
|
||||
pub method: T,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestMethod<'x> {
|
||||
Get(GetRequestMethod),
|
||||
Set(SetRequestMethod<'x>),
|
||||
Changes(Box<ChangesRequest>),
|
||||
Copy(CopyRequestMethod<'x>),
|
||||
ImportEmail(Box<ImportEmailRequest>),
|
||||
Parse(ParseRequestMethod),
|
||||
Query(QueryRequestMethod),
|
||||
QueryChanges(QueryChangesRequestMethod),
|
||||
SearchSnippet(Box<GetSearchSnippetRequest>),
|
||||
ValidateScript(Box<ValidateSieveScriptRequest>),
|
||||
LookupBlob(Box<BlobLookupRequest>),
|
||||
UploadBlob(Box<BlobUploadRequest>),
|
||||
Echo(Value<'x, Null, Null>),
|
||||
Error(trc::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetRequestMethod {
|
||||
Email(Box<GetRequest<Email>>),
|
||||
Mailbox(Box<GetRequest<Mailbox>>),
|
||||
Thread(Box<GetRequest<Thread>>),
|
||||
Identity(Box<GetRequest<Identity>>),
|
||||
EmailSubmission(Box<GetRequest<EmailSubmission>>),
|
||||
PushSubscription(Box<GetRequest<PushSubscription>>),
|
||||
Sieve(Box<GetRequest<Sieve>>),
|
||||
VacationResponse(Box<GetRequest<VacationResponse>>),
|
||||
Principal(Box<GetRequest<Principal>>),
|
||||
PrincipalAvailability(Box<GetAvailabilityRequest>),
|
||||
Quota(Box<GetRequest<Quota>>),
|
||||
Blob(Box<GetRequest<Blob>>),
|
||||
AddressBook(Box<GetRequest<AddressBook>>),
|
||||
ContactCard(Box<GetRequest<ContactCard>>),
|
||||
FileNode(Box<GetRequest<FileNode>>),
|
||||
Calendar(Box<GetRequest<Calendar>>),
|
||||
CalendarEvent(Box<GetRequest<CalendarEvent>>),
|
||||
CalendarEventNotification(Box<GetRequest<CalendarEventNotification>>),
|
||||
ParticipantIdentity(Box<GetRequest<ParticipantIdentity>>),
|
||||
ShareNotification(Box<GetRequest<ShareNotification>>),
|
||||
Registry(Box<GetRequest<Registry>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SetRequestMethod<'x> {
|
||||
Email(Box<SetRequest<'x, Email>>),
|
||||
Mailbox(Box<SetRequest<'x, Mailbox>>),
|
||||
Identity(Box<SetRequest<'x, Identity>>),
|
||||
EmailSubmission(Box<SetRequest<'x, EmailSubmission>>),
|
||||
PushSubscription(Box<SetRequest<'x, PushSubscription>>),
|
||||
Sieve(Box<SetRequest<'x, Sieve>>),
|
||||
VacationResponse(Box<SetRequest<'x, VacationResponse>>),
|
||||
AddressBook(Box<SetRequest<'x, AddressBook>>),
|
||||
ContactCard(Box<SetRequest<'x, ContactCard>>),
|
||||
FileNode(Box<SetRequest<'x, FileNode>>),
|
||||
ShareNotification(Box<SetRequest<'x, ShareNotification>>),
|
||||
Calendar(Box<SetRequest<'x, Calendar>>),
|
||||
CalendarEvent(Box<SetRequest<'x, CalendarEvent>>),
|
||||
CalendarEventNotification(Box<SetRequest<'x, CalendarEventNotification>>),
|
||||
ParticipantIdentity(Box<SetRequest<'x, ParticipantIdentity>>),
|
||||
Registry(Box<SetRequest<'x, Registry>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CopyRequestMethod<'x> {
|
||||
Email(Box<CopyRequest<'x, Email>>),
|
||||
ContactCard(Box<CopyRequest<'x, ContactCard>>),
|
||||
CalendarEvent(Box<CopyRequest<'x, CalendarEvent>>),
|
||||
FileNode(Box<CopyRequest<'x, FileNode>>),
|
||||
Blob(Box<CopyBlobRequest>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum QueryRequestMethod {
|
||||
Email(Box<QueryRequest<Email>>),
|
||||
Mailbox(Box<QueryRequest<Mailbox>>),
|
||||
EmailSubmission(Box<QueryRequest<EmailSubmission>>),
|
||||
Sieve(Box<QueryRequest<Sieve>>),
|
||||
Principal(Box<QueryRequest<Principal>>),
|
||||
Quota(Box<QueryRequest<Quota>>),
|
||||
AddressBook(Box<QueryRequest<AddressBook>>),
|
||||
ContactCard(Box<QueryRequest<ContactCard>>),
|
||||
FileNode(Box<QueryRequest<FileNode>>),
|
||||
Calendar(Box<QueryRequest<Calendar>>),
|
||||
CalendarEvent(Box<QueryRequest<CalendarEvent>>),
|
||||
CalendarEventNotification(Box<QueryRequest<CalendarEventNotification>>),
|
||||
ShareNotification(Box<QueryRequest<ShareNotification>>),
|
||||
Registry(Box<QueryRequest<Registry>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum QueryChangesRequestMethod {
|
||||
Email(Box<QueryChangesRequest<Email>>),
|
||||
Mailbox(Box<QueryChangesRequest<Mailbox>>),
|
||||
EmailSubmission(Box<QueryChangesRequest<EmailSubmission>>),
|
||||
Principal(Box<QueryChangesRequest<Principal>>),
|
||||
Quota(Box<QueryChangesRequest<Quota>>),
|
||||
ContactCard(Box<QueryChangesRequest<ContactCard>>),
|
||||
FileNode(Box<QueryChangesRequest<FileNode>>),
|
||||
CalendarEvent(Box<QueryChangesRequest<CalendarEvent>>),
|
||||
CalendarEventNotification(Box<QueryChangesRequest<CalendarEventNotification>>),
|
||||
ShareNotification(Box<QueryChangesRequest<ShareNotification>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParseRequestMethod {
|
||||
Email(Box<ParseRequest<Email>>),
|
||||
ContactCard(Box<ParseRequest<ContactCard>>),
|
||||
CalendarEvent(Box<ParseRequest<CalendarEvent>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MaybeInvalid<V: FromStr> {
|
||||
Value(V),
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl<'de, V: FromStr> serde::Deserialize<'de> for MaybeInvalid<V> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <&str>::deserialize(deserializer)?;
|
||||
|
||||
if let Ok(id) = V::from_str(value) {
|
||||
Ok(MaybeInvalid::Value(id))
|
||||
} else {
|
||||
Ok(MaybeInvalid::Invalid(value.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: FromStr + serde::Serialize> serde::Serialize for MaybeInvalid<V> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
MaybeInvalid::Value(v) => v.serialize(serializer),
|
||||
MaybeInvalid::Invalid(s) => serializer.serialize_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: FromStr> From<V> for MaybeInvalid<V> {
|
||||
fn from(value: V) -> Self {
|
||||
MaybeInvalid::Value(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: FromStr> Default for MaybeInvalid<V> {
|
||||
fn default() -> Self {
|
||||
MaybeInvalid::Invalid("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Request<'_> {
|
||||
fn default() -> Self {
|
||||
Request {
|
||||
using: CapabilityIds::default(),
|
||||
method_calls: Vec::new(),
|
||||
created_ids: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeInvalid<T>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
pub fn try_unwrap(self) -> Option<T> {
|
||||
match self {
|
||||
MaybeInvalid::Value(id) => Some(id),
|
||||
MaybeInvalid::Invalid(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoValid {
|
||||
type Item;
|
||||
|
||||
fn into_valid(self) -> impl Iterator<Item = Self::Item>;
|
||||
}
|
||||
|
||||
impl<T: FromStr> IntoValid for Vec<MaybeInvalid<T>> {
|
||||
type Item = T;
|
||||
|
||||
fn into_valid(self) -> impl Iterator<Item = Self::Item> {
|
||||
self.into_iter().filter_map(|v| v.try_unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> IntoValid for Vec<MaybeIdReference<T>> {
|
||||
type Item = T;
|
||||
|
||||
fn into_valid(self) -> impl Iterator<Item = Self::Item> {
|
||||
self.into_iter().filter_map(|v| v.try_unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr + Eq, V> IntoValid for VecMap<MaybeInvalid<T>, V> {
|
||||
type Item = (T, V);
|
||||
|
||||
fn into_valid(self) -> impl Iterator<Item = Self::Item> {
|
||||
self.into_iter()
|
||||
.filter_map(|(k, v)| k.try_unwrap().map(|k| (k, v)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr + Eq, V> IntoValid for VecMap<MaybeIdReference<T>, V> {
|
||||
type Item = (T, V);
|
||||
|
||||
fn into_valid(self) -> impl Iterator<Item = Self::Item> {
|
||||
self.into_iter()
|
||||
.filter_map(|(k, v)| k.try_unwrap().map(|k| (k, v)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,948 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Call, Request, RequestMethod,
|
||||
method::{MethodFunction, MethodName, MethodObject},
|
||||
};
|
||||
use crate::request::{
|
||||
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
|
||||
QueryRequestMethod, SetRequestMethod, deserialize::DeserializeArguments,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{self, SeqAccess, Visitor},
|
||||
};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
impl<'x> Request<'x> {
|
||||
pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
|
||||
if json.len() <= max_size {
|
||||
match serde_json::from_slice::<Request>(json) {
|
||||
Ok(request) => {
|
||||
if request.method_calls.len() <= max_calls {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::LimitEvent::CallsIn.into_err())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(trc::JmapEvent::NotRequest
|
||||
.into_err()
|
||||
.reason(err.to_string())
|
||||
.details(String::from_utf8_lossy(json).into_owned())),
|
||||
}
|
||||
} else {
|
||||
Err(trc::LimitEvent::SizeRequest.into_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeArguments<'de> for Request<'de> {
|
||||
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"using" => {
|
||||
self.using = map.next_value()?;
|
||||
},
|
||||
b"methodCalls" => {
|
||||
self.method_calls = map.next_value()?;
|
||||
},
|
||||
b"createdIds" => {
|
||||
self.created_ids = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
let _ = map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct CallVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for CallVisitor {
|
||||
type Value = Call<RequestMethod<'de>>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an array with 3 elements")
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<Call<RequestMethod<'de>>, V::Error>
|
||||
where
|
||||
V: SeqAccess<'de>,
|
||||
{
|
||||
let method_name = seq
|
||||
.next_element::<std::borrow::Cow<str>>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
||||
let name = match MethodName::parse(method_name.as_ref()) {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
// Ignore the rest of the call
|
||||
let _ = seq
|
||||
.next_element::<serde::de::IgnoredAny>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
||||
let id = seq
|
||||
.next_element::<String>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
||||
|
||||
return Ok(Call {
|
||||
id,
|
||||
method: RequestMethod::Error(
|
||||
trc::JmapEvent::UnknownMethod
|
||||
.into_err()
|
||||
.details(method_name.to_string()),
|
||||
),
|
||||
name: MethodName::error(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let method = match (&name.fnc, &name.obj) {
|
||||
(MethodFunction::Get, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Mailbox) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Mailbox(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Thread) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Thread(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Identity) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Identity(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::EmailSubmission) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::EmailSubmission(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::PushSubscription) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::PushSubscription(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::VacationResponse) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::VacationResponse(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::SieveScript) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Sieve(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Principal) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Principal(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Quota) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Quota(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Blob) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Blob(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Calendar) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Calendar(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::CalendarEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::CalendarEvent(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::CalendarEventNotification) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::Get(GetRequestMethod::CalendarEventNotification(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::Get, MethodObject::ParticipantIdentity) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ParticipantIdentity(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::AddressBook) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AddressBook(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ContactCard(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::ShareNotification) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ShareNotification(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::SearchSnippet) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::SearchSnippet(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::Registry(_)) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::Registry(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Mailbox) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Mailbox(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Identity) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Identity(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::EmailSubmission) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::EmailSubmission(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::PushSubscription) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::PushSubscription(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::VacationResponse) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::VacationResponse(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::SieveScript) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Sieve(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Calendar) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Calendar(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::CalendarEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::CalendarEvent(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::CalendarEventNotification) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::Set(SetRequestMethod::CalendarEventNotification(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::Set, MethodObject::ParticipantIdentity) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ParticipantIdentity(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::AddressBook) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::AddressBook(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ContactCard(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::ShareNotification) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ShareNotification(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::Registry(_)) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::Registry(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Mailbox) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Mailbox(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::EmailSubmission) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::EmailSubmission(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::SieveScript) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Sieve(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Principal) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Principal(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Quota) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Quota(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Calendar) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Calendar(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::CalendarEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::CalendarEvent(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::CalendarEventNotification) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::Query(QueryRequestMethod::CalendarEventNotification(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::Query, MethodObject::AddressBook) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::AddressBook(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::ContactCard(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::ShareNotification) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::Query(QueryRequestMethod::ShareNotification(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Query, MethodObject::Registry(_)) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Registry(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::Email(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::Mailbox) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::Mailbox(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::EmailSubmission) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::QueryChanges(
|
||||
QueryChangesRequestMethod::EmailSubmission(value),
|
||||
),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::QueryChanges, MethodObject::Principal) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::Principal(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::Quota) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::Quota(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::CalendarEvent) => match seq.next_element()
|
||||
{
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::CalendarEvent(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::CalendarEventNotification) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::QueryChanges(
|
||||
QueryChangesRequestMethod::CalendarEventNotification(value),
|
||||
),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::QueryChanges, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::ContactCard(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::QueryChanges(QueryChangesRequestMethod::FileNode(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::QueryChanges, MethodObject::ShareNotification) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::QueryChanges(
|
||||
QueryChangesRequestMethod::ShareNotification(value),
|
||||
),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::Changes, _) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Changes(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Copy, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Copy, MethodObject::Blob) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::Blob(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Copy, MethodObject::CalendarEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::CalendarEvent(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Copy, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::ContactCard(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Copy, MethodObject::FileNode) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::FileNode(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Lookup, MethodObject::Blob) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::LookupBlob(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Upload, MethodObject::Blob) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::UploadBlob(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Import, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::ImportEmail(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Parse, MethodObject::Email) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::Email(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Parse, MethodObject::CalendarEvent) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::CalendarEvent(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Parse, MethodObject::ContactCard) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::ContactCard(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::GetAvailability, MethodObject::Principal) => {
|
||||
match seq.next_element() {
|
||||
Ok(Some(value)) => {
|
||||
RequestMethod::Get(GetRequestMethod::PrincipalAvailability(value))
|
||||
}
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
}
|
||||
}
|
||||
(MethodFunction::Validate, MethodObject::SieveScript) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::ValidateScript(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Echo, MethodObject::Core) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Echo(value),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(de::Error::custom(format!(
|
||||
"Invalid method function/object combination: {}",
|
||||
method_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let id = seq
|
||||
.next_element::<String>()?
|
||||
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
||||
|
||||
Ok(Call { id, method, name })
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestMethod<'_> {
|
||||
fn invalid(err: impl Display) -> Self {
|
||||
RequestMethod::Error(
|
||||
trc::JmapEvent::InvalidArguments
|
||||
.into_err()
|
||||
.details(err.to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Request<'de> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct RequestVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for RequestVisitor {
|
||||
type Value = Request<'de>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a JMAP request object")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'de>,
|
||||
{
|
||||
let mut target = Request::default();
|
||||
let mut has_using = false;
|
||||
let mut has_method_calls = false;
|
||||
|
||||
while let Some(key) = map.next_key::<&str>()? {
|
||||
match key {
|
||||
"using" => has_using = true,
|
||||
"methodCalls" => has_method_calls = true,
|
||||
_ => {}
|
||||
}
|
||||
target
|
||||
.deserialize_argument(key, &mut map)
|
||||
.map_err(de::Error::custom)?;
|
||||
}
|
||||
|
||||
if !has_using || !has_method_calls {
|
||||
return Err(de::Error::custom(
|
||||
"Request is missing the \"using\" or \"methodCalls\" property.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(target)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(RequestVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Call<RequestMethod<'de>> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(CallVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[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"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const TEST1: &str = r#"
|
||||
{
|
||||
"using": [
|
||||
"urn:ietf:params:jmap:core",
|
||||
"urn:ietf:params:jmap:mail"
|
||||
],
|
||||
"methodCalls": [
|
||||
[
|
||||
"Email/query",
|
||||
{
|
||||
"accountId": "0",
|
||||
"filter": { "conditions": [ { "hasKeyword": "music", "maxSize": 455 }, { "hasKeyword": "video" }, { "operator": "AND", "conditions": [ { "subject": "test" }, { "minSize": 100 } ] } ], "operator": "OR" },
|
||||
"sort": [
|
||||
{
|
||||
"property": "subject",
|
||||
"isAscending": true
|
||||
},
|
||||
{
|
||||
"property": "allInThreadHaveKeyword",
|
||||
"isAscending": false,
|
||||
"keyword": "$seen"
|
||||
},
|
||||
{
|
||||
"keyword": "$junk",
|
||||
"property": "someInThreadHaveKeyword",
|
||||
"collation": "i;octet",
|
||||
"isAscending": false
|
||||
}
|
||||
],
|
||||
"position": 0,
|
||||
"limit": 10
|
||||
},
|
||||
"c1"
|
||||
]
|
||||
],
|
||||
"createdIds": {}
|
||||
}
|
||||
"#;
|
||||
|
||||
const TEST2: &str = r##"
|
||||
{
|
||||
"using": [
|
||||
"urn:ietf:params:jmap:submission",
|
||||
"urn:ietf:params:jmap:mail",
|
||||
"urn:ietf:params:jmap:core"
|
||||
],
|
||||
"methodCalls": [
|
||||
[
|
||||
"Email/set",
|
||||
{
|
||||
"accountId": "c",
|
||||
"create": {
|
||||
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
|
||||
"mailboxIds": {
|
||||
"9": true
|
||||
},
|
||||
"subject": "test",
|
||||
"from": [
|
||||
{
|
||||
"name": "Foo",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
],
|
||||
"to": [
|
||||
{
|
||||
"name": null,
|
||||
"email": "[email protected]"
|
||||
}
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"replyTo": [
|
||||
{
|
||||
"name": null,
|
||||
"email": "[email protected]"
|
||||
}
|
||||
],
|
||||
"htmlBody": [
|
||||
{
|
||||
"partId": "c37ee58b-e224-4799-88e6-1d7484e3b782",
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"bodyValues": {
|
||||
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
|
||||
"value": "<p>test email<br></p>",
|
||||
"isEncodingProblem": false,
|
||||
"isTruncated": false
|
||||
}
|
||||
},
|
||||
"header:User-Agent:asText": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"c0"
|
||||
],
|
||||
[
|
||||
"EmailSubmission/set",
|
||||
{
|
||||
"accountId": "c",
|
||||
"create": {
|
||||
"c37ee58b-e224-4799-88e6-1d7484e3b782": {
|
||||
"identityId": "a",
|
||||
"emailId": "#c37ee58b-e224-4799-88e6-1d7484e3b782",
|
||||
"envelope": {
|
||||
"mailFrom": {
|
||||
"email": "[email protected]"
|
||||
},
|
||||
"rcptTo": [
|
||||
{
|
||||
"email": "[email protected]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"onSuccessUpdateEmail": {
|
||||
"#c37ee58b-e224-4799-88e6-1d7484e3b782": {
|
||||
"mailboxIds/d": true,
|
||||
"mailboxIds/9": null,
|
||||
"keywords/$seen": true,
|
||||
"keywords/$draft": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"c1"
|
||||
]
|
||||
]
|
||||
}
|
||||
"##;
|
||||
|
||||
const TEST_ESCAPED_SOLIDUS: &str = r#"
|
||||
{
|
||||
"using": [ "urn:ietf:params:jmap:core" ],
|
||||
"methodCalls": [
|
||||
[ "Core\/echo", { "hello": true }, "c1" ]
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parse_request() {
|
||||
println!("{:#?}", Request::parse(TEST.as_bytes(), 10, 10240));
|
||||
println!("{:#?}", Request::parse(TEST1.as_bytes(), 10, 10240));
|
||||
println!("{:#?}", Request::parse(TEST2.as_bytes(), 10, 10240));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_method_name_with_escaped_solidus() {
|
||||
let request = Request::parse(TEST_ESCAPED_SOLIDUS.as_bytes(), 10, 10240)
|
||||
.expect("escaped solidus in method name must parse");
|
||||
assert_eq!(request.method_calls.len(), 1);
|
||||
assert!(matches!(
|
||||
request.method_calls[0].method,
|
||||
super::RequestMethod::Echo(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::method::MethodName;
|
||||
use jmap_tools::{JsonPointer, Null};
|
||||
use std::{borrow::Cow, fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ResultReference {
|
||||
#[serde(rename = "resultOf")]
|
||||
pub result_of: String,
|
||||
pub name: MethodName,
|
||||
pub path: JsonPointer<Null>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MaybeIdReference<V: FromStr> {
|
||||
Id(V),
|
||||
Reference(String),
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MaybeResultReference<V> {
|
||||
Value(V),
|
||||
Reference(ResultReference),
|
||||
}
|
||||
|
||||
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: FromStr + Display> Display for MaybeIdReference<V> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MaybeIdReference::Id(id) => write!(f, "{}", id),
|
||||
MaybeIdReference::Reference(str) => write!(f, "#{}", str),
|
||||
MaybeIdReference::Invalid(str) => write!(f, "{}", str),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, V: FromStr> serde::Deserialize<'de> for MaybeIdReference<V> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <Cow<'de, str>>::deserialize(deserializer)?;
|
||||
|
||||
if let Some(reference) = value.strip_prefix('#') {
|
||||
if reference.is_empty() {
|
||||
return Ok(MaybeIdReference::Invalid(value.into_owned()));
|
||||
}
|
||||
Ok(MaybeIdReference::Reference(reference.to_string()))
|
||||
} else if let Ok(id) = V::from_str(value.as_ref()) {
|
||||
Ok(MaybeIdReference::Id(id))
|
||||
} else {
|
||||
Ok(MaybeIdReference::Invalid(value.into_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: FromStr> FromStr for MaybeIdReference<V> {
|
||||
type Err = V::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(reference) = s.strip_prefix('#') {
|
||||
if reference.is_empty() {
|
||||
return Ok(MaybeIdReference::Invalid(s.to_string()));
|
||||
}
|
||||
Ok(MaybeIdReference::Reference(reference.to_string()))
|
||||
} else if let Ok(id) = V::from_str(s) {
|
||||
Ok(MaybeIdReference::Id(id))
|
||||
} else {
|
||||
Ok(MaybeIdReference::Invalid(s.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Display + FromStr> serde::Serialize for MaybeIdReference<V> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
MaybeIdReference::Id(id) => serializer.serialize_str(&id.to_string()),
|
||||
MaybeIdReference::Reference(str) => serializer.serialize_str(&format!("#{}", str)),
|
||||
MaybeIdReference::Invalid(str) => serializer.serialize_str(str),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Default> Default for MaybeResultReference<V> {
|
||||
fn default() -> Self {
|
||||
MaybeResultReference::Value(V::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> MaybeResultReference<T> {
|
||||
pub fn unwrap(self) -> T {
|
||||
match self {
|
||||
MaybeResultReference::Value(v) => v,
|
||||
MaybeResultReference::Reference(_) => T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> MaybeIdReference<T> {
|
||||
pub fn try_unwrap(self) -> Option<T> {
|
||||
match self {
|
||||
MaybeIdReference::Id(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::Request;
|
||||
use crate::{
|
||||
error::request::{RequestError, RequestErrorType, RequestLimitError},
|
||||
object::AnyId,
|
||||
request::{Call, deserialize::DeserializeArguments},
|
||||
response::{Response, ResponseMethod, serialize::serialize_hex, status::PushObject},
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{self, MapAccess, Visitor},
|
||||
};
|
||||
use std::{borrow::Cow, collections::HashMap, fmt};
|
||||
use types::type_state::DataType;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebSocketRequest<'x> {
|
||||
pub id: Option<String>,
|
||||
pub request: Request<'x>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WebSocketResponse<'x> {
|
||||
#[serde(rename = "@type")]
|
||||
_type: WebSocketResponseType,
|
||||
|
||||
#[serde(rename = "methodResponses")]
|
||||
method_responses: Vec<Call<ResponseMethod<'x>>>,
|
||||
|
||||
#[serde(rename = "sessionState")]
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
session_state: u32,
|
||||
|
||||
#[serde(rename(deserialize = "createdIds"))]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
created_ids: HashMap<String, AnyId>,
|
||||
|
||||
#[serde(rename = "requestId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum WebSocketResponseType {
|
||||
Response,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct WebSocketPushEnable {
|
||||
pub data_types: Vec<DataType>,
|
||||
pub push_state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WebSocketMessage<'x> {
|
||||
Request(WebSocketRequest<'x>),
|
||||
PushEnable(WebSocketPushEnable),
|
||||
PushDisable,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct WebSocketPushObject {
|
||||
#[serde(flatten)]
|
||||
pub push: PushObject,
|
||||
|
||||
#[serde(rename = "pushState")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub push_state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WebSocketRequestError<'x> {
|
||||
#[serde(rename = "@type")]
|
||||
pub type_: WebSocketRequestErrorType,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
p_type: RequestErrorType,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
limit: Option<RequestLimitError>,
|
||||
status: u16,
|
||||
detail: Cow<'x, str>,
|
||||
|
||||
#[serde(rename = "requestId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub enum WebSocketRequestErrorType {
|
||||
RequestError,
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
Request,
|
||||
PushEnable,
|
||||
PushDisable,
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'x> WebSocketMessage<'x> {
|
||||
pub fn parse(json: &'x [u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
|
||||
if json.len() <= max_size {
|
||||
match serde_json::from_slice::<Self>(json) {
|
||||
Ok(WebSocketMessage::Request(req))
|
||||
if req.request.method_calls.len() > max_calls =>
|
||||
{
|
||||
Err(trc::LimitEvent::CallsIn.into_err())
|
||||
}
|
||||
Ok(msg) => Ok(msg),
|
||||
Err(err) => Err(trc::JmapEvent::NotRequest
|
||||
.into_err()
|
||||
.details(format!("Invalid WebSocket JMAP request {err}"))),
|
||||
}
|
||||
} else {
|
||||
Err(trc::LimitEvent::SizeRequest.into_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'x, 'x: 'de> Deserialize<'de> for WebSocketMessage<'x> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_map(WebSocketMessageVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct WebSocketMessageVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for WebSocketMessageVisitor {
|
||||
type Value = WebSocketMessage<'de>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a WebSocketMessage as a map")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<WebSocketMessage<'de>, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut message_type = MessageType::None;
|
||||
let mut request = WebSocketRequest {
|
||||
id: None,
|
||||
request: Request::default(),
|
||||
};
|
||||
let mut push_enable = WebSocketPushEnable::default();
|
||||
|
||||
let mut found_request_keys = false;
|
||||
let mut found_push_keys = false;
|
||||
|
||||
while let Some(key) = map.next_key::<&str>()? {
|
||||
hashify::fnc_map!(key.as_bytes(),
|
||||
b"@type" => {
|
||||
message_type = MessageType::parse(map.next_value()?);
|
||||
},
|
||||
b"dataTypes" => {
|
||||
push_enable.data_types = map.next_value::<Option<Vec<DataType>>>()?.unwrap_or_default();
|
||||
found_push_keys = true;
|
||||
},
|
||||
b"pushState" => {
|
||||
push_enable.push_state = map.next_value()?;
|
||||
found_push_keys = true;
|
||||
},
|
||||
b"id" => {
|
||||
request.id = map.next_value()?;
|
||||
},
|
||||
_ => {
|
||||
request.request.deserialize_argument(key, &mut map)?;
|
||||
found_request_keys = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
match message_type {
|
||||
MessageType::Request if found_request_keys => Ok(WebSocketMessage::Request(request)),
|
||||
MessageType::PushEnable if found_push_keys => {
|
||||
Ok(WebSocketMessage::PushEnable(push_enable))
|
||||
}
|
||||
MessageType::PushDisable if !found_request_keys && !found_push_keys => {
|
||||
Ok(WebSocketMessage::PushDisable)
|
||||
}
|
||||
_ => Err(de::Error::custom("Invalid WebSocket JMAP request")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
fn parse(s: &str) -> Self {
|
||||
hashify::tiny_map!(s.as_bytes(),
|
||||
b"Request" => MessageType::Request,
|
||||
b"WebSocketPushEnable" => MessageType::PushEnable,
|
||||
b"WebSocketPushDisable" => MessageType::PushDisable,
|
||||
)
|
||||
.unwrap_or(MessageType::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> WebSocketRequestError<'x> {
|
||||
pub fn from_error(error: RequestError<'x>, request_id: Option<String>) -> Self {
|
||||
Self {
|
||||
type_: WebSocketRequestErrorType::RequestError,
|
||||
p_type: error.p_type,
|
||||
limit: error.limit,
|
||||
status: error.status,
|
||||
detail: error.detail,
|
||||
request_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<RequestError<'x>> for WebSocketRequestError<'x> {
|
||||
fn from(value: RequestError<'x>) -> Self {
|
||||
Self::from_error(value, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> WebSocketResponse<'x> {
|
||||
pub fn from_response(response: Response<'x>, request_id: Option<String>) -> Self {
|
||||
Self {
|
||||
_type: WebSocketResponseType::Response,
|
||||
method_responses: response.method_responses,
|
||||
session_state: response.session_state,
|
||||
created_ids: response.created_ids,
|
||||
request_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketPushObject {
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod serialize;
|
||||
pub mod status;
|
||||
|
||||
use self::serialize::serialize_hex;
|
||||
use crate::{
|
||||
error::method::MethodErrorWrapper,
|
||||
method::{
|
||||
availability::GetAvailabilityResponse,
|
||||
changes::ChangesResponse,
|
||||
copy::{CopyBlobResponse, CopyResponse},
|
||||
get::GetResponse,
|
||||
import::ImportEmailResponse,
|
||||
lookup::BlobLookupResponse,
|
||||
parse::ParseResponse,
|
||||
query::QueryResponse,
|
||||
query_changes::QueryChangesResponse,
|
||||
search_snippet::GetSearchSnippetResponse,
|
||||
set::SetResponse,
|
||||
upload::BlobUploadResponse,
|
||||
validate::ValidateSieveScriptResponse,
|
||||
},
|
||||
object::{
|
||||
AnyId,
|
||||
addressbook::AddressBook,
|
||||
blob::Blob,
|
||||
calendar::Calendar,
|
||||
calendar_event::CalendarEvent,
|
||||
calendar_event_notification::{
|
||||
CalendarEventNotification, CalendarEventNotificationGetResponse,
|
||||
},
|
||||
contact::ContactCard,
|
||||
email::Email,
|
||||
email_submission::EmailSubmission,
|
||||
file_node::FileNode,
|
||||
identity::Identity,
|
||||
mailbox::Mailbox,
|
||||
participant_identity::ParticipantIdentity,
|
||||
principal::Principal,
|
||||
push_subscription::PushSubscription,
|
||||
quota::Quota,
|
||||
registry::Registry,
|
||||
share_notification::ShareNotification,
|
||||
sieve::Sieve,
|
||||
thread::Thread,
|
||||
vacation_response::VacationResponse,
|
||||
},
|
||||
request::{Call, method::MethodName},
|
||||
};
|
||||
use jmap_tools::{Null, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ResponseMethod<'x> {
|
||||
Get(GetResponseMethod),
|
||||
Set(SetResponseMethod),
|
||||
Changes(ChangesResponseMethod),
|
||||
Copy(CopyResponseMethod),
|
||||
ImportEmail(ImportEmailResponse),
|
||||
Parse(ParseResponseMethod),
|
||||
QueryChanges(QueryChangesResponse),
|
||||
Query(QueryResponse),
|
||||
SearchSnippet(GetSearchSnippetResponse),
|
||||
ValidateScript(ValidateSieveScriptResponse),
|
||||
LookupBlob(BlobLookupResponse),
|
||||
UploadBlob(BlobUploadResponse),
|
||||
Echo(Value<'x, Null, Null>),
|
||||
Error(MethodErrorWrapper),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum GetResponseMethod {
|
||||
Email(GetResponse<Email>),
|
||||
Mailbox(GetResponse<Mailbox>),
|
||||
Thread(GetResponse<Thread>),
|
||||
Identity(GetResponse<Identity>),
|
||||
EmailSubmission(GetResponse<EmailSubmission>),
|
||||
PushSubscription(GetResponse<PushSubscription>),
|
||||
Sieve(GetResponse<Sieve>),
|
||||
VacationResponse(GetResponse<VacationResponse>),
|
||||
Principal(GetResponse<Principal>),
|
||||
PrincipalAvailability(GetAvailabilityResponse),
|
||||
Quota(GetResponse<Quota>),
|
||||
Blob(GetResponse<Blob>),
|
||||
AddressBook(GetResponse<AddressBook>),
|
||||
ContactCard(GetResponse<ContactCard>),
|
||||
FileNode(GetResponse<FileNode>),
|
||||
Calendar(GetResponse<Calendar>),
|
||||
CalendarEvent(GetResponse<CalendarEvent>),
|
||||
CalendarEventNotification(CalendarEventNotificationGetResponse),
|
||||
ParticipantIdentity(GetResponse<ParticipantIdentity>),
|
||||
ShareNotification(GetResponse<ShareNotification>),
|
||||
Registry(GetResponse<Registry>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SetResponseMethod {
|
||||
Email(Box<SetResponse<Email>>),
|
||||
Mailbox(Box<SetResponse<Mailbox>>),
|
||||
Identity(Box<SetResponse<Identity>>),
|
||||
EmailSubmission(Box<SetResponse<EmailSubmission>>),
|
||||
PushSubscription(Box<SetResponse<PushSubscription>>),
|
||||
Sieve(Box<SetResponse<Sieve>>),
|
||||
VacationResponse(Box<SetResponse<VacationResponse>>),
|
||||
AddressBook(Box<SetResponse<AddressBook>>),
|
||||
ContactCard(Box<SetResponse<ContactCard>>),
|
||||
FileNode(Box<SetResponse<FileNode>>),
|
||||
ShareNotification(Box<SetResponse<ShareNotification>>),
|
||||
Calendar(Box<SetResponse<Calendar>>),
|
||||
CalendarEvent(Box<SetResponse<CalendarEvent>>),
|
||||
CalendarEventNotification(Box<SetResponse<CalendarEventNotification>>),
|
||||
ParticipantIdentity(Box<SetResponse<ParticipantIdentity>>),
|
||||
Registry(Box<SetResponse<Registry>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ChangesResponseMethod {
|
||||
Email(Box<ChangesResponse<Email>>),
|
||||
Mailbox(Box<ChangesResponse<Mailbox>>),
|
||||
Thread(Box<ChangesResponse<Thread>>),
|
||||
Identity(Box<ChangesResponse<Identity>>),
|
||||
EmailSubmission(Box<ChangesResponse<EmailSubmission>>),
|
||||
Quota(Box<ChangesResponse<Quota>>),
|
||||
AddressBook(Box<ChangesResponse<AddressBook>>),
|
||||
ContactCard(Box<ChangesResponse<ContactCard>>),
|
||||
FileNode(Box<ChangesResponse<FileNode>>),
|
||||
Calendar(Box<ChangesResponse<Calendar>>),
|
||||
CalendarEvent(Box<ChangesResponse<CalendarEvent>>),
|
||||
CalendarEventNotification(Box<ChangesResponse<CalendarEventNotification>>),
|
||||
ShareNotification(Box<ChangesResponse<ShareNotification>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CopyResponseMethod {
|
||||
Email(CopyResponse<Email>),
|
||||
ContactCard(CopyResponse<ContactCard>),
|
||||
CalendarEvent(CopyResponse<CalendarEvent>),
|
||||
FileNode(CopyResponse<FileNode>),
|
||||
Blob(CopyBlobResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ParseResponseMethod {
|
||||
Email(ParseResponse<Email>),
|
||||
ContactCard(ParseResponse<ContactCard>),
|
||||
CalendarEvent(ParseResponse<CalendarEvent>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct Response<'x> {
|
||||
#[serde(rename = "methodResponses")]
|
||||
pub method_responses: Vec<Call<ResponseMethod<'x>>>,
|
||||
|
||||
#[serde(rename = "sessionState")]
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
pub session_state: u32,
|
||||
|
||||
#[serde(rename = "createdIds")]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub created_ids: HashMap<String, AnyId>,
|
||||
}
|
||||
|
||||
impl<'x> Response<'x> {
|
||||
pub fn new(session_state: u32, created_ids: HashMap<String, AnyId>, capacity: usize) -> Self {
|
||||
Response {
|
||||
session_state,
|
||||
created_ids,
|
||||
method_responses: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_response(
|
||||
&mut self,
|
||||
id: String,
|
||||
name: MethodName,
|
||||
method: impl Into<ResponseMethod<'x>>,
|
||||
) {
|
||||
self.method_responses.push(Call {
|
||||
id,
|
||||
method: method.into(),
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_error(&mut self, id: String, err: impl Into<MethodErrorWrapper>) {
|
||||
self.method_responses.push(Call {
|
||||
id,
|
||||
method: ResponseMethod::Error(err.into()),
|
||||
name: MethodName::error(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_created_id(&mut self, create_id: String, id: impl Into<AnyId>) {
|
||||
self.created_ids.insert(create_id, id.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl From<trc::Error> for ResponseMethod<'_> {
|
||||
fn from(error: trc::Error) -> Self {
|
||||
ResponseMethod::Error(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x, T: Into<ResponseMethod<'x>>> From<trc::Result<T>> for ResponseMethod<'x> {
|
||||
fn from(result: trc::Result<T>) -> Self {
|
||||
match result {
|
||||
Ok(value) => value.into(),
|
||||
Err(error) => error.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Email>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Email>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Email(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Mailbox>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Mailbox>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Mailbox(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Thread>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Thread>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Thread(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Identity>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Identity>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Identity(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<EmailSubmission>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<EmailSubmission>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::EmailSubmission(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<PushSubscription>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<PushSubscription>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::PushSubscription(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Sieve>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Sieve>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Sieve(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<VacationResponse>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<VacationResponse>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::VacationResponse(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Principal>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Principal>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Principal(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Quota>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Quota>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Quota(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Blob>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Blob>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Blob(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<ContactCard>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<ContactCard>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::ContactCard(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<AddressBook>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<AddressBook>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::AddressBook(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<Registry>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<Registry>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Registry(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<Email>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<Email>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Email(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<Mailbox>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<Mailbox>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Mailbox(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<Identity>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<Identity>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Identity(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<EmailSubmission>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<EmailSubmission>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::EmailSubmission(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<PushSubscription>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<PushSubscription>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::PushSubscription(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<Sieve>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<Sieve>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Sieve(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<VacationResponse>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<VacationResponse>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::VacationResponse(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<AddressBook>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<AddressBook>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::AddressBook(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<ContactCard>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<ContactCard>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::ContactCard(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<Registry>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<Registry>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Registry(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<Email>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<Email>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::Email(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<Mailbox>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<Mailbox>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::Mailbox(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<Thread>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<Thread>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::Thread(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<Identity>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<Identity>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::Identity(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<EmailSubmission>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<EmailSubmission>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::EmailSubmission(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<Quota>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<Quota>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::Quota(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ChangesResponse<AddressBook>> for ResponseMethod<'x> {
|
||||
fn from(value: ChangesResponse<AddressBook>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::AddressBook(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<CopyResponse<Email>> for ResponseMethod<'x> {
|
||||
fn from(value: CopyResponse<Email>) -> Self {
|
||||
ResponseMethod::Copy(CopyResponseMethod::Email(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<CopyBlobResponse> for ResponseMethod<'x> {
|
||||
fn from(value: CopyBlobResponse) -> Self {
|
||||
ResponseMethod::Copy(CopyResponseMethod::Blob(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<CopyResponse<ContactCard>> for ResponseMethod<'x> {
|
||||
fn from(value: CopyResponse<ContactCard>) -> Self {
|
||||
ResponseMethod::Copy(CopyResponseMethod::ContactCard(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ImportEmailResponse> for ResponseMethod<'x> {
|
||||
fn from(value: ImportEmailResponse) -> Self {
|
||||
ResponseMethod::ImportEmail(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ParseResponse<Email>> for ResponseMethod<'x> {
|
||||
fn from(value: ParseResponse<Email>) -> Self {
|
||||
ResponseMethod::Parse(ParseResponseMethod::Email(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ParseResponse<ContactCard>> for ResponseMethod<'x> {
|
||||
fn from(value: ParseResponse<ContactCard>) -> Self {
|
||||
ResponseMethod::Parse(ParseResponseMethod::ContactCard(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<QueryChangesResponse> for ResponseMethod<'x> {
|
||||
fn from(value: QueryChangesResponse) -> Self {
|
||||
ResponseMethod::QueryChanges(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<QueryResponse> for ResponseMethod<'x> {
|
||||
fn from(value: QueryResponse) -> Self {
|
||||
ResponseMethod::Query(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetSearchSnippetResponse> for ResponseMethod<'x> {
|
||||
fn from(value: GetSearchSnippetResponse) -> Self {
|
||||
ResponseMethod::SearchSnippet(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<ValidateSieveScriptResponse> for ResponseMethod<'x> {
|
||||
fn from(value: ValidateSieveScriptResponse) -> Self {
|
||||
ResponseMethod::ValidateScript(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<BlobLookupResponse> for ResponseMethod<'x> {
|
||||
fn from(value: BlobLookupResponse) -> Self {
|
||||
ResponseMethod::LookupBlob(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<BlobUploadResponse> for ResponseMethod<'x> {
|
||||
fn from(value: BlobUploadResponse) -> Self {
|
||||
ResponseMethod::UploadBlob(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<Value<'x, Null, Null>> for ResponseMethod<'x> {
|
||||
fn from(value: Value<'x, Null, Null>) -> Self {
|
||||
ResponseMethod::Echo(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<MethodErrorWrapper> for ResponseMethod<'x> {
|
||||
fn from(value: MethodErrorWrapper) -> Self {
|
||||
ResponseMethod::Error(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::FileNode(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::FileNode(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangesResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(response: ChangesResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::FileNode(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetAvailabilityResponse> for ResponseMethod<'_> {
|
||||
fn from(response: GetAvailabilityResponse) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::PrincipalAvailability(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<Calendar>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<Calendar>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::Calendar(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<Calendar>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<Calendar>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::Calendar(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangesResponse<CalendarEvent>> for ResponseMethod<'_> {
|
||||
fn from(response: ChangesResponse<CalendarEvent>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::CalendarEvent(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangesResponse<CalendarEventNotification>> for ResponseMethod<'_> {
|
||||
fn from(response: ChangesResponse<CalendarEventNotification>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::CalendarEventNotification(Box::new(
|
||||
response,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<CalendarEvent>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<CalendarEvent>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::CalendarEvent(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<ParticipantIdentity>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<ParticipantIdentity>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::ParticipantIdentity(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<ParticipantIdentity>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<ParticipantIdentity>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::ParticipantIdentity(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangesResponse<ShareNotification>> for ResponseMethod<'_> {
|
||||
fn from(response: ChangesResponse<ShareNotification>) -> Self {
|
||||
ResponseMethod::Changes(ChangesResponseMethod::ShareNotification(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<ShareNotification>> for ResponseMethod<'_> {
|
||||
fn from(response: SetResponse<ShareNotification>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::ShareNotification(Box::new(response)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<ShareNotification>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<ShareNotification>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::ShareNotification(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetResponse<CalendarEvent>> for ResponseMethod<'_> {
|
||||
fn from(response: GetResponse<CalendarEvent>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::CalendarEvent(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseResponse<CalendarEvent>> for ResponseMethod<'_> {
|
||||
fn from(value: ParseResponse<CalendarEvent>) -> Self {
|
||||
ResponseMethod::Parse(ParseResponseMethod::CalendarEvent(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CopyResponse<CalendarEvent>> for ResponseMethod<'_> {
|
||||
fn from(value: CopyResponse<CalendarEvent>) -> Self {
|
||||
ResponseMethod::Copy(CopyResponseMethod::CalendarEvent(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CopyResponse<FileNode>> for ResponseMethod<'_> {
|
||||
fn from(value: CopyResponse<FileNode>) -> Self {
|
||||
ResponseMethod::Copy(CopyResponseMethod::FileNode(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarEventNotificationGetResponse> for ResponseMethod<'_> {
|
||||
fn from(value: CalendarEventNotificationGetResponse) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::CalendarEventNotification(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SetResponse<CalendarEventNotification>> for ResponseMethod<'_> {
|
||||
fn from(value: SetResponse<CalendarEventNotification>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::CalendarEventNotification(Box::new(
|
||||
value,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ResponseMethod;
|
||||
use crate::request::Call;
|
||||
use serde::{Serialize, ser::SerializeSeq};
|
||||
|
||||
impl Serialize for Call<ResponseMethod<'_>> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(3.into())?;
|
||||
seq.serialize_element(&self.name.to_string())?;
|
||||
seq.serialize_element(&self.method)?;
|
||||
seq.serialize_element(&self.id)?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_hex<S>(value: &u32, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
format!("{:x}", value).serialize(serializer)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::object::email::{EmailProperty, EmailValue};
|
||||
use crate::types::state::State;
|
||||
use jmap_tools::Value;
|
||||
use types::{id::Id, type_state::DataType};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
#[serde(tag = "@type")]
|
||||
pub enum PushObject {
|
||||
StateChange {
|
||||
changed: VecMap<Id, VecMap<DataType, State>>,
|
||||
},
|
||||
EmailPush {
|
||||
#[serde(rename = "accountId")]
|
||||
account_id: Id,
|
||||
emails: Vec<Value<'static, EmailProperty, EmailValue>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
state: Option<State>,
|
||||
},
|
||||
CalendarAlert {
|
||||
#[serde(rename = "accountId")]
|
||||
account_id: Id,
|
||||
#[serde(rename = "calendarEventId")]
|
||||
calendar_event_id: Id,
|
||||
uid: String,
|
||||
#[serde(rename = "recurrenceId")]
|
||||
recurrence_id: Option<String>,
|
||||
#[serde(rename = "alertId")]
|
||||
alert_id: String,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[rkyv(derive(Debug), compare(PartialEq))]
|
||||
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 FromStr for UTCDate {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// 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;
|
||||
|
||||
for ch in s.as_bytes() {
|
||||
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;
|
||||
}
|
||||
b':' if [3, 4, 6].contains(&pos) => {
|
||||
pos += 1;
|
||||
}
|
||||
b'+' if pos == 5 => {
|
||||
pos += 1;
|
||||
skip_digits = false;
|
||||
}
|
||||
b'.' if pos == 5 => {
|
||||
skip_digits = true;
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 From<&ArchivedUTCDate> for UTCDate {
|
||||
fn from(value: &ArchivedUTCDate) -> Self {
|
||||
UTCDate {
|
||||
year: value.year.to_native(),
|
||||
month: value.month,
|
||||
day: value.day,
|
||||
hour: value.hour,
|
||||
minute: value.minute,
|
||||
second: value.second,
|
||||
tz_before_gmt: value.tz_before_gmt,
|
||||
tz_hour: value.tz_hour,
|
||||
tz_minute: value.tz_minute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UTCDate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.tz_hour != 0 || self.tz_minute != 0 {
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
|
||||
self.year,
|
||||
self.month,
|
||||
self.day,
|
||||
self.hour,
|
||||
self.minute,
|
||||
self.second,
|
||||
if self.tz_before_gmt && (self.tz_hour > 0 || self.tz_minute > 0) {
|
||||
"-"
|
||||
} else {
|
||||
"+"
|
||||
},
|
||||
self.tz_hour,
|
||||
self.tz_minute,
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
|
||||
self.year, self.month, self.day, self.hour, self.minute, self.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for UTCDate {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for UTCDate {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
UTCDate::from_str(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid JMAP UTCDate"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UTCDate> for u64 {
|
||||
fn from(value: UTCDate) -> Self {
|
||||
value.timestamp() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for UTCDate {
|
||||
fn from(value: u64) -> Self {
|
||||
UTCDate::from_timestamp(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::types::date::UTCDate;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[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 = UTCDate::from_str(input).unwrap();
|
||||
assert_eq!(date.to_string(), expected_result);
|
||||
|
||||
let timestamp = date.timestamp();
|
||||
assert_eq!(UTCDate::from_timestamp(timestamp).timestamp(), timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod date;
|
||||
pub mod state;
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use types::ChangeId;
|
||||
use utils::codec::{
|
||||
base32_custom::{Base32Reader, Base32Writer},
|
||||
leb128::{Leb128Iterator, Leb128Writer},
|
||||
};
|
||||
|
||||
#[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<Option<ChangeId>> for State {
|
||||
fn from(change_id: Option<ChangeId>) -> Self {
|
||||
match change_id {
|
||||
Some(change_id) => State::Exact(change_id),
|
||||
None => State::Initial,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
let mut it = value.as_bytes().iter();
|
||||
|
||||
match it.next()? {
|
||||
b'n' => Some(State::Initial),
|
||||
b's' => {
|
||||
let mut reader = Base32Reader::from_iter(it);
|
||||
reader
|
||||
.next_leb128::<ChangeId>()
|
||||
.map(|change_id| (change_id != 0).then_some(change_id).into())
|
||||
}
|
||||
b'r' => {
|
||||
let mut it = Base32Reader::from_iter(it);
|
||||
|
||||
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 {
|
||||
Some(State::Intermediate(JMAPIntermediateState {
|
||||
from_id,
|
||||
to_id: from_id.saturating_add(to_id),
|
||||
items_sent,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
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<'de> serde::Deserialize<'de> for State {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
State::parse(<&str>::deserialize(deserializer)?)
|
||||
.ok_or_else(|| serde::de::Error::custom("invalid JMAP State"))
|
||||
}
|
||||
}
|
||||
|
||||
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 super::State;
|
||||
use types::ChangeId;
|
||||
|
||||
#[test]
|
||||
fn test_state_id() {
|
||||
for id in [
|
||||
State::new_initial(),
|
||||
State::new_exact(1),
|
||||
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!(State::parse(&id.to_string()).unwrap(), id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_zero_change_id_is_initial() {
|
||||
assert_eq!(
|
||||
State::parse(&State::new_exact(0).to_string()).unwrap(),
|
||||
State::Initial
|
||||
);
|
||||
assert_eq!(State::from(None), State::Initial);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user