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,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(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user