mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-11-02 00:12:45 +00:00
v0.4.2
This commit is contained in:
@@ -2,6 +2,19 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.4.2] - 2023-11-01
|
||||
|
||||
## Added
|
||||
- JMAP for Quotas support ([RFC9425](https://www.rfc-editor.org/rfc/rfc9425.html))
|
||||
- JMAP Blob Management Extension support ([RFC9404](https://www.rfc-editor.org/rfc/rfc9404.html))
|
||||
- Spam Filter - Empty header rules.
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
- Daylight savings time support for crontabs.
|
||||
- JMAP `oldState` doesn’t reflect in `*/changes` (#56)
|
||||
|
||||
## [0.4.1] - 2023-10-26
|
||||
|
||||
## Added
|
||||
|
||||
Generated
+10
-9
@@ -2276,7 +2276,7 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
||||
|
||||
[[package]]
|
||||
name = "imap"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"dashmap",
|
||||
@@ -2460,7 +2460,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jmap"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
@@ -2497,6 +2497,7 @@ dependencies = [
|
||||
"sequoia-openpgp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"sieve-rs",
|
||||
"smtp",
|
||||
@@ -2824,7 +2825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mail-server"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"directory",
|
||||
"imap",
|
||||
@@ -2841,7 +2842,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "managesieve"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"bincode",
|
||||
@@ -3031,7 +3032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nlp"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"bincode",
|
||||
@@ -4739,7 +4740,7 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
|
||||
|
||||
[[package]]
|
||||
name = "smtp"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"blake3",
|
||||
@@ -5070,7 +5071,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stalwart-cli"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"console",
|
||||
@@ -5092,7 +5093,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stalwart-install"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"clap",
|
||||
@@ -5900,7 +5901,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "utils"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"chrono",
|
||||
|
||||
@@ -13,8 +13,8 @@ Key features:
|
||||
- **JMAP** server:
|
||||
- JMAP Core ([RFC 8620](https://datatracker.ietf.org/doc/html/rfc8620))
|
||||
- JMAP Mail ([RFC 8621](https://datatracker.ietf.org/doc/html/rfc8621))
|
||||
- JMAP over WebSocket ([RFC 8887](https://datatracker.ietf.org/doc/html/rfc8887))
|
||||
- JMAP for Sieve Scripts ([DRAFT-SIEVE-13](https://www.ietf.org/archive/id/draft-ietf-jmap-sieve-13.html))
|
||||
- JMAP for Sieve Scripts ([DRAFT-SIEVE-15](https://www.ietf.org/archive/id/draft-ietf-jmap-sieve-15.html))
|
||||
- JMAP over WebSocket ([RFC 8887](https://datatracker.ietf.org/doc/html/rfc8887)), JMAP Blob Management ([RFC9404](https://www.rfc-editor.org/rfc/rfc9404.html)) and JMAP for Quotas ([RFC9425](https://www.rfc-editor.org/rfc/rfc9425.html)) extensions.
|
||||
- **IMAP4** server:
|
||||
- IMAP4rev2 ([RFC 9051](https://datatracker.ietf.org/doc/html/rfc9051)) full compliance.
|
||||
- IMAP4rev1 ([RFC 3501](https://datatracker.ietf.org/doc/html/rfc3501)) backwards compatible.
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
Upgrading from `v0.4.0` to `v0.4.2`
|
||||
-----------------------------------
|
||||
|
||||
- Replace the binary with the new version.
|
||||
- Restart the service.
|
||||
|
||||
|
||||
Upgrading from `v0.3.x` to `v0.4.0`
|
||||
-----------------------------------
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/stalwartlabs/cli"
|
||||
homepage = "https://github.com/stalwartlabs/cli"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
resolver = "2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imap"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ use jmap_proto::{
|
||||
object::{index::ObjectIndexBuilder, Object},
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange,
|
||||
type_state::TypeState, value::Value,
|
||||
type_state::DataType, value::Value,
|
||||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder};
|
||||
@@ -434,7 +434,7 @@ impl<T: AsyncRead> Session<T> {
|
||||
data.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(mailbox.account_id)
|
||||
.with_change(TypeState::Mailbox, change_id),
|
||||
.with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use imap_proto::{
|
||||
};
|
||||
|
||||
use jmap::email::ingest::IngestEmail;
|
||||
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::TypeState};
|
||||
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType};
|
||||
use mail_parser::MessageParser;
|
||||
use tokio::io::AsyncRead;
|
||||
|
||||
@@ -173,9 +173,9 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Mailbox, change_id)
|
||||
.with_change(TypeState::Thread, change_id),
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Mailbox, change_id)
|
||||
.with_change(DataType::Thread, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use jmap_proto::{
|
||||
error::{method::MethodError, set::SetErrorType},
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange,
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE};
|
||||
@@ -400,9 +400,9 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(dest_account_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Thread, change_id)
|
||||
.with_change(TypeState::Mailbox, change_id),
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Thread, change_id)
|
||||
.with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -420,8 +420,8 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(src_mailbox.id.account_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Mailbox, change_id),
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use jmap_proto::{
|
||||
object::{index::ObjectIndexBuilder, Object},
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange,
|
||||
type_state::TypeState, value::Value,
|
||||
type_state::DataType, value::Value,
|
||||
},
|
||||
};
|
||||
use store::{query::Filter, write::BatchBuilder};
|
||||
@@ -132,7 +132,7 @@ impl SessionData {
|
||||
// Broadcast changes
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(params.account_id).with_change(TypeState::Mailbox, change_id),
|
||||
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
use imap_proto::{
|
||||
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
use jmap_proto::types::{state::StateChange, type_state::TypeState};
|
||||
use jmap_proto::types::{state::StateChange, type_state::DataType};
|
||||
use store::write::log::ChangeLogBuilder;
|
||||
use tokio::io::AsyncRead;
|
||||
|
||||
@@ -109,11 +109,11 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(if did_remove_emails {
|
||||
StateChange::new(account_id)
|
||||
.with_change(TypeState::Mailbox, change_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Thread, change_id)
|
||||
.with_change(DataType::Mailbox, change_id)
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Thread, change_id)
|
||||
} else {
|
||||
StateChange::new(account_id).with_change(TypeState::Mailbox, change_id)
|
||||
StateChange::new(account_id).with_change(DataType::Mailbox, change_id)
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
|
||||
state::StateChange, type_state::TypeState,
|
||||
state::StateChange, type_state::DataType,
|
||||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, F_VALUE};
|
||||
@@ -247,9 +247,9 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Mailbox, change_id)
|
||||
.with_change(TypeState::Thread, change_id),
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Mailbox, change_id)
|
||||
.with_change(DataType::Thread, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ use jmap_proto::{
|
||||
object::Object,
|
||||
types::{
|
||||
acl::Acl, blob::BlobId, collection::Collection, id::Id, keyword::Keyword,
|
||||
property::Property, state::StateChange, type_state::TypeState, value::Value,
|
||||
property::Property, state::StateChange, type_state::DataType, value::Value,
|
||||
},
|
||||
};
|
||||
use mail_parser::{Address, GetHeader, HeaderName, Message, MessageParser, PartType};
|
||||
@@ -569,7 +569,7 @@ impl SessionData {
|
||||
modseq = change_id.into();
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(account_id).with_change(TypeState::Email, change_id),
|
||||
StateChange::new(account_id).with_change(DataType::Email, change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
};
|
||||
|
||||
use jmap_proto::types::{collection::Collection, type_state::TypeState};
|
||||
use jmap_proto::types::{collection::Collection, type_state::DataType};
|
||||
use store::query::log::Query;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
@@ -46,16 +46,12 @@ impl<T: AsyncRead> Session<T> {
|
||||
pub async fn handle_idle(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||
let (data, mailbox, types) = match &self.state {
|
||||
State::Authenticated { data, .. } => {
|
||||
(data.clone(), None, Bitmap::from_iter([TypeState::Mailbox]))
|
||||
(data.clone(), None, Bitmap::from_iter([DataType::Mailbox]))
|
||||
}
|
||||
State::Selected { data, mailbox, .. } => (
|
||||
data.clone(),
|
||||
mailbox.clone().into(),
|
||||
Bitmap::from_iter([
|
||||
TypeState::Email,
|
||||
TypeState::Mailbox,
|
||||
TypeState::EmailDelivery,
|
||||
]),
|
||||
Bitmap::from_iter([DataType::Email, DataType::Mailbox, DataType::EmailDelivery]),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
@@ -120,10 +116,10 @@ impl<T: AsyncRead> Session<T> {
|
||||
|
||||
for (type_state, _) in state_change.types {
|
||||
match type_state {
|
||||
TypeState::Email | TypeState::EmailDelivery => {
|
||||
DataType::Email | DataType::EmailDelivery => {
|
||||
has_email_changes = true;
|
||||
}
|
||||
TypeState::Mailbox => {
|
||||
DataType::Mailbox => {
|
||||
has_mailbox_changes = true;
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -32,7 +32,7 @@ use jmap_proto::{
|
||||
object::{index::ObjectIndexBuilder, Object},
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange,
|
||||
type_state::TypeState, value::Value,
|
||||
type_state::DataType, value::Value,
|
||||
},
|
||||
};
|
||||
use store::write::{assert::HashedValue, BatchBuilder};
|
||||
@@ -203,7 +203,7 @@ impl SessionData {
|
||||
// Broadcast changes
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(params.account_id).with_change(TypeState::Mailbox, change_id),
|
||||
StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
types::{
|
||||
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
|
||||
state::StateChange, type_state::TypeState,
|
||||
state::StateChange, type_state::DataType,
|
||||
},
|
||||
};
|
||||
use store::{
|
||||
@@ -353,10 +353,10 @@ impl SessionData {
|
||||
self.jmap
|
||||
.broadcast_state_change(if !changed_mailboxes.is_empty() {
|
||||
StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, change_id)
|
||||
.with_change(TypeState::Mailbox, change_id)
|
||||
.with_change(DataType::Email, change_id)
|
||||
.with_change(DataType::Mailbox, change_id)
|
||||
} else {
|
||||
StateChange::new(account_id).with_change(TypeState::Email, change_id)
|
||||
StateChange::new(account_id).with_change(DataType::Email, change_id)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
object::{index::ObjectIndexBuilder, Object},
|
||||
types::{
|
||||
collection::Collection, property::Property, state::StateChange, type_state::TypeState,
|
||||
collection::Collection, property::Property, state::StateChange, type_state::DataType,
|
||||
value::Value,
|
||||
},
|
||||
};
|
||||
@@ -164,7 +164,7 @@ impl SessionData {
|
||||
// Broadcast changes
|
||||
self.jmap
|
||||
.broadcast_state_change(
|
||||
StateChange::new(account_id).with_change(TypeState::Mailbox, change_id),
|
||||
StateChange::new(account_id).with_change(DataType::Mailbox, change_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/stalwartlabs/mail-server"
|
||||
homepage = "https://github.com/stalwartlabs/mail-server"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
resolver = "2"
|
||||
|
||||
@@ -44,6 +44,8 @@ pub enum MethodError {
|
||||
AccountNotSupportedByMethod,
|
||||
AccountReadOnly,
|
||||
NotFound,
|
||||
CannotCalculateChanges,
|
||||
UnknownDataType,
|
||||
}
|
||||
|
||||
impl Display for MethodError {
|
||||
@@ -69,6 +71,8 @@ impl Display for MethodError {
|
||||
}
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +159,21 @@ impl Serialize for MethodError {
|
||||
"accountReadOnly",
|
||||
"This method modifies state, but the account is read-only.",
|
||||
),
|
||||
MethodError::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."
|
||||
),
|
||||
),
|
||||
MethodError::CannotCalculateChanges => (
|
||||
"cannotCalculateChanges",
|
||||
concat!(
|
||||
"The server cannot calculate the changes ",
|
||||
"between the old and new states."
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
map.serialize_entry("type", error_type)?;
|
||||
|
||||
@@ -182,6 +182,10 @@ impl SetError {
|
||||
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.")
|
||||
}
|
||||
@@ -190,6 +194,10 @@ impl SetError {
|
||||
Self::new(SetErrorType::AlreadyExists)
|
||||
}
|
||||
|
||||
pub fn too_large() -> Self {
|
||||
Self::new(SetErrorType::TooLarge)
|
||||
}
|
||||
|
||||
pub fn will_destroy() -> Self {
|
||||
Self::new(SetErrorType::WillDestroy).with_description("ID will be destroyed.")
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ pub enum RequestArguments {
|
||||
Thread,
|
||||
Identity,
|
||||
EmailSubmission,
|
||||
Quota,
|
||||
}
|
||||
|
||||
impl JsonObjectParser for ChangesRequest {
|
||||
@@ -82,6 +83,7 @@ impl JsonObjectParser for ChangesRequest {
|
||||
MethodObject::Thread => RequestArguments::Thread,
|
||||
MethodObject::Identity => RequestArguments::Identity,
|
||||
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
|
||||
MethodObject::Quota => RequestArguments::Quota,
|
||||
_ => {
|
||||
return Err(Error::Method(MethodError::UnknownMethod(format!(
|
||||
"{}/changes",
|
||||
|
||||
@@ -23,20 +23,20 @@
|
||||
|
||||
use crate::{
|
||||
error::method::MethodError,
|
||||
object::{email, Object},
|
||||
object::{blob, email, Object},
|
||||
parser::{json::Parser, Error, JsonObjectParser, Token},
|
||||
request::{
|
||||
method::MethodObject,
|
||||
reference::{MaybeReference, ResultReference},
|
||||
RequestProperty, RequestPropertyParser,
|
||||
},
|
||||
types::{id::Id, property::Property, state::State, value::Value},
|
||||
types::{any_id::AnyId, blob::BlobId, id::Id, property::Property, state::State, value::Value},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetRequest<T> {
|
||||
pub account_id: Id,
|
||||
pub ids: Option<MaybeReference<Vec<Id>, ResultReference>>,
|
||||
pub ids: Option<MaybeReference<Vec<MaybeReference<AnyId, String>>, ResultReference>>,
|
||||
pub properties: Option<MaybeReference<Vec<Property>, ResultReference>>,
|
||||
pub arguments: T,
|
||||
}
|
||||
@@ -52,6 +52,8 @@ pub enum RequestArguments {
|
||||
SieveScript,
|
||||
VacationResponse,
|
||||
Principal,
|
||||
Quota,
|
||||
Blob(blob::GetArguments),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -66,7 +68,7 @@ pub struct GetResponse {
|
||||
pub list: Vec<Object<Value>>,
|
||||
|
||||
#[serde(rename = "notFound")]
|
||||
pub not_found: Vec<Id>,
|
||||
pub not_found: Vec<AnyId>,
|
||||
}
|
||||
|
||||
impl JsonObjectParser for GetRequest<RequestArguments> {
|
||||
@@ -85,6 +87,8 @@ impl JsonObjectParser for GetRequest<RequestArguments> {
|
||||
MethodObject::SieveScript => RequestArguments::SieveScript,
|
||||
MethodObject::VacationResponse => RequestArguments::VacationResponse,
|
||||
MethodObject::Principal => RequestArguments::Principal,
|
||||
MethodObject::Blob => RequestArguments::Blob(Default::default()),
|
||||
MethodObject::Quota => RequestArguments::Quota,
|
||||
_ => {
|
||||
return Err(Error::Method(MethodError::UnknownMethod(format!(
|
||||
"{}/get",
|
||||
@@ -108,7 +112,17 @@ impl JsonObjectParser for GetRequest<RequestArguments> {
|
||||
}
|
||||
0x0073_6469 => {
|
||||
request.ids = if !key.is_ref {
|
||||
<Option<Vec<Id>>>::parse(parser)?.map(MaybeReference::Value)
|
||||
if parser.ctx != MethodObject::Blob {
|
||||
<Option<Vec<MaybeReference<Id, String>>>>::parse(parser)?.map(|ids| {
|
||||
MaybeReference::Value(ids.into_iter().map(Into::into).collect())
|
||||
})
|
||||
} else {
|
||||
<Option<Vec<MaybeReference<BlobId, String>>>>::parse(parser)?.map(
|
||||
|ids| {
|
||||
MaybeReference::Value(ids.into_iter().map(Into::into).collect())
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Some(MaybeReference::Reference(ResultReference::parse(parser)?))
|
||||
};
|
||||
@@ -138,10 +152,10 @@ impl RequestPropertyParser for RequestArguments {
|
||||
parser: &mut Parser,
|
||||
property: RequestProperty,
|
||||
) -> crate::parser::Result<bool> {
|
||||
if let RequestArguments::Email(arguments) = self {
|
||||
arguments.parse(parser, property)
|
||||
} else {
|
||||
Ok(false)
|
||||
match self {
|
||||
RequestArguments::Email(arguments) => arguments.parse(parser, property),
|
||||
RequestArguments::Blob(arguments) => arguments.parse(parser, property),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +195,31 @@ impl<T> GetRequest<T> {
|
||||
if let Some(ids) = self.ids.take() {
|
||||
let ids = ids.unwrap();
|
||||
if ids.len() <= max_objects_in_get {
|
||||
Ok(Some(ids))
|
||||
Ok(Some(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| id.try_unwrap().and_then(|id| id.into_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Err(MethodError::RequestTooLarge)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_blob_ids(
|
||||
&mut self,
|
||||
max_objects_in_get: usize,
|
||||
) -> Result<Option<Vec<BlobId>>, MethodError> {
|
||||
if let Some(ids) = self.ids.take() {
|
||||
let ids = ids.unwrap();
|
||||
if ids.len() <= max_objects_in_get {
|
||||
Ok(Some(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| id.try_unwrap().and_then(|id| id.into_blob_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
Err(MethodError::RequestTooLarge)
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ impl ImportEmailResponse {
|
||||
pub fn update_created_ids(&self, response: &mut Response) {
|
||||
for (user_id, obj) in &self.created {
|
||||
if let Some(id) = obj.get(&Property::Id).as_id() {
|
||||
response.created_ids.insert(user_id.clone(), *id);
|
||||
response.created_ids.insert(user_id.clone(), (*id).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{
|
||||
parser::{json::Parser, JsonObjectParser, Token},
|
||||
request::RequestProperty,
|
||||
types::{blob::BlobId, id::Id, type_state::DataType, MaybeUnparsable},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlobLookupRequest {
|
||||
pub account_id: Id,
|
||||
pub type_names: Vec<MaybeUnparsable<DataType>>,
|
||||
pub ids: Vec<MaybeUnparsable<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<MaybeUnparsable<BlobId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct BlobInfo {
|
||||
pub id: BlobId,
|
||||
#[serde(rename = "matchedIds")]
|
||||
pub matched_ids: VecMap<DataType, Vec<Id>>,
|
||||
}
|
||||
|
||||
impl JsonObjectParser for BlobLookupRequest {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut request = BlobLookupRequest {
|
||||
account_id: Id::default(),
|
||||
type_names: Vec::new(),
|
||||
ids: Vec::new(),
|
||||
};
|
||||
|
||||
parser
|
||||
.next_token::<String>()?
|
||||
.assert_jmap(Token::DictStart)?;
|
||||
|
||||
while let Some(key) = parser.next_dict_key::<RequestProperty>()? {
|
||||
match &key.hash[0] {
|
||||
0x0064_4974_6e75_6f63_6361 if !key.is_ref => {
|
||||
request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?;
|
||||
}
|
||||
0x0073_656d_614e_6570_7974 if !key.is_ref => {
|
||||
request.type_names = <Vec<MaybeUnparsable<DataType>>>::parse(parser)?;
|
||||
}
|
||||
0x0073_6469 if !key.is_ref => {
|
||||
request.ids = <Vec<MaybeUnparsable<BlobId>>>::parse(parser)?;
|
||||
}
|
||||
_ => {
|
||||
parser.skip_token(parser.depth_array, parser.depth_dict)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,13 @@ 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)]
|
||||
|
||||
@@ -113,6 +113,8 @@ pub enum Filter {
|
||||
HasAnyRole(bool),
|
||||
IsSubscribed(bool),
|
||||
IsActive(bool),
|
||||
Scope(String),
|
||||
ResourceType(String),
|
||||
_T(String),
|
||||
|
||||
And,
|
||||
@@ -149,6 +151,7 @@ pub enum SortProperty {
|
||||
HasKeyword,
|
||||
AllInThreadHaveKeyword,
|
||||
SomeInThreadHaveKeyword,
|
||||
Used,
|
||||
_T(String),
|
||||
}
|
||||
|
||||
@@ -159,6 +162,7 @@ pub enum RequestArguments {
|
||||
EmailSubmission,
|
||||
SieveScript,
|
||||
Principal,
|
||||
Quota,
|
||||
}
|
||||
|
||||
impl JsonObjectParser for QueryRequest<RequestArguments> {
|
||||
@@ -173,6 +177,7 @@ impl JsonObjectParser for QueryRequest<RequestArguments> {
|
||||
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
|
||||
MethodObject::SieveScript => RequestArguments::SieveScript,
|
||||
MethodObject::Principal => RequestArguments::Principal,
|
||||
MethodObject::Quota => RequestArguments::Quota,
|
||||
_ => {
|
||||
return Err(Error::Method(MethodError::UnknownMethod(format!(
|
||||
"{}/query",
|
||||
@@ -428,6 +433,14 @@ pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> {
|
||||
(0x6576_6974_6341_7369, _) => Filter::IsActive(
|
||||
parser.next_token::<String>()?.unwrap_bool("isActive")?,
|
||||
),
|
||||
(0x0065_706f_6373, _) => {
|
||||
Filter::Scope(parser.next_token::<String>()?.unwrap_string("scope")?)
|
||||
}
|
||||
(0x6570_7954_6563_7275_6f73_6572, _) => Filter::ResourceType(
|
||||
parser
|
||||
.next_token::<String>()?
|
||||
.unwrap_string("resourceType")?,
|
||||
),
|
||||
_ => {
|
||||
if parser.is_eof || parser.skip_string() {
|
||||
let filter = Filter::_T(
|
||||
@@ -569,6 +582,7 @@ impl JsonObjectParser for SortProperty {
|
||||
0x6472_6f77_7965_4b73_6168 => Ok(SortProperty::HasKeyword),
|
||||
0x4b65_7661_4864_6165_7268_546e_496c_6c61 => Ok(SortProperty::AllInThreadHaveKeyword),
|
||||
0x6576_6148_6461_6572_6854_6e49_656d_6f73 => Ok(SortProperty::SomeInThreadHaveKeyword),
|
||||
0x6465_7375 => Ok(SortProperty::Used),
|
||||
_ => {
|
||||
if parser.is_eof || parser.skip_string() {
|
||||
Ok(SortProperty::_T(
|
||||
@@ -629,6 +643,8 @@ impl Display for Filter {
|
||||
Filter::HasAnyRole(_) => "hasAnyRole",
|
||||
Filter::IsSubscribed(_) => "isSubscribed",
|
||||
Filter::IsActive(_) => "isActive",
|
||||
Filter::ResourceType(_) => "resourceType",
|
||||
Filter::Scope(_) => "scope",
|
||||
Filter::_T(v) => v.as_str(),
|
||||
Filter::And => "and",
|
||||
Filter::Or => "or",
|
||||
@@ -659,6 +675,7 @@ impl Display for SortProperty {
|
||||
SortProperty::HasKeyword => "hasKeyword",
|
||||
SortProperty::AllInThreadHaveKeyword => "allInThreadHaveKeyword",
|
||||
SortProperty::SomeInThreadHaveKeyword => "someInThreadHaveKeyword",
|
||||
SortProperty::Used => "used",
|
||||
SortProperty::_T(s) => s,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ impl JsonObjectParser for QueryChangesRequest {
|
||||
MethodObject::Email => RequestArguments::Email(Default::default()),
|
||||
MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()),
|
||||
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
|
||||
MethodObject::Quota => RequestArguments::Quota,
|
||||
_ => {
|
||||
return Err(Error::Method(MethodError::UnknownMethod(format!(
|
||||
"{}/queryChanges",
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::{
|
||||
response::Response,
|
||||
types::{
|
||||
acl::Acl,
|
||||
any_id::AnyId,
|
||||
blob::BlobId,
|
||||
date::UTCDate,
|
||||
id::Id,
|
||||
@@ -205,9 +206,9 @@ impl JsonObjectParser for Object<SetValue> {
|
||||
.map(|id| SetValue::Value(Value::Id(id)))
|
||||
.unwrap_or(SetValue::Value(Value::Null)),
|
||||
Property::BlobId | Property::Picture => parser
|
||||
.next_token::<BlobId>()?
|
||||
.next_token::<MaybeReference<BlobId, String>>()?
|
||||
.unwrap_string_or_null("")?
|
||||
.map(|id| SetValue::Value(Value::BlobId(id)))
|
||||
.map(SetValue::from)
|
||||
.unwrap_or(SetValue::Value(Value::Null)),
|
||||
Property::SentAt
|
||||
| Property::ReceivedAt
|
||||
@@ -272,11 +273,11 @@ impl JsonObjectParser for Object<SetValue> {
|
||||
Property::ParentId | Property::EmailId | Property::IdentityId => parser
|
||||
.next_token::<MaybeReference<Id, String>>()?
|
||||
.unwrap_string_or_null("")?
|
||||
.map(SetValue::IdReference)
|
||||
.map(SetValue::from)
|
||||
.unwrap_or(SetValue::Value(Value::Null)),
|
||||
Property::MailboxIds => {
|
||||
if key.patch.is_empty() {
|
||||
SetValue::IdReferences(
|
||||
SetValue::from(
|
||||
<SetValueMap<MaybeReference<Id, String>>>::parse(parser)?.values,
|
||||
)
|
||||
} else {
|
||||
@@ -375,6 +376,31 @@ impl JsonObjectParser for Object<SetValue> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<AnyId>> From<MaybeReference<T, String>> for SetValue {
|
||||
fn from(reference: MaybeReference<T, String>) -> Self {
|
||||
match reference {
|
||||
MaybeReference::Value(id) => SetValue::IdReference(MaybeReference::Value(id.into())),
|
||||
MaybeReference::Reference(reference) => {
|
||||
SetValue::IdReference(MaybeReference::Reference(reference))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<AnyId>> From<Vec<MaybeReference<T, String>>> for SetValue {
|
||||
fn from(value: Vec<MaybeReference<T, String>>) -> Self {
|
||||
SetValue::IdReferences(
|
||||
value
|
||||
.into_iter()
|
||||
.map(|reference| match reference {
|
||||
MaybeReference::Value(id) => MaybeReference::Value(id.into()),
|
||||
MaybeReference::Reference(reference) => MaybeReference::Reference(reference),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestPropertyParser for RequestArguments {
|
||||
fn parse(
|
||||
&mut self,
|
||||
@@ -520,7 +546,7 @@ impl SetResponse {
|
||||
pub fn update_created_ids(&self, response: &mut Response) {
|
||||
for (user_id, obj) in &self.created {
|
||||
if let Some(id) = obj.get(&Property::Id).as_id() {
|
||||
response.created_ids.insert(user_id.clone(), *id);
|
||||
response.created_ids.insert(user_id.clone(), (*id).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{
|
||||
error::set::SetError,
|
||||
parser::{json::Parser, Ignore, JsonObjectParser, Token},
|
||||
request::{reference::MaybeReference, RequestProperty},
|
||||
response::Response,
|
||||
types::{blob::BlobId, id::Id},
|
||||
};
|
||||
|
||||
use super::ahash_is_empty;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlobUploadRequest {
|
||||
pub account_id: Id,
|
||||
pub create: VecMap<String, UploadObject>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UploadObject {
|
||||
pub type_: Option<String>,
|
||||
pub data: Vec<DataSourceObject>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DataSourceObject {
|
||||
Id {
|
||||
id: MaybeReference<BlobId, String>,
|
||||
length: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
},
|
||||
Value(Vec<u8>),
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[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 JsonObjectParser for BlobUploadRequest {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut request = BlobUploadRequest {
|
||||
account_id: Id::default(),
|
||||
create: VecMap::new(),
|
||||
};
|
||||
|
||||
parser
|
||||
.next_token::<String>()?
|
||||
.assert_jmap(Token::DictStart)?;
|
||||
|
||||
while let Some(key) = parser.next_dict_key::<RequestProperty>()? {
|
||||
match &key.hash[0] {
|
||||
0x0064_4974_6e75_6f63_6361 if !key.is_ref => {
|
||||
request.account_id = parser.next_token::<Id>()?.unwrap_string("accountId")?;
|
||||
}
|
||||
0x6574_6165_7263 if !key.is_ref => {
|
||||
request.create = <VecMap<String, UploadObject>>::parse(parser)?;
|
||||
}
|
||||
_ => {
|
||||
parser.skip_token(parser.depth_array, parser.depth_dict)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for UploadObject {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut request = UploadObject {
|
||||
type_: None,
|
||||
data: Vec::new(),
|
||||
};
|
||||
|
||||
parser
|
||||
.next_token::<String>()?
|
||||
.assert_jmap(Token::DictStart)?;
|
||||
|
||||
while let Some(key) = parser.next_dict_key::<RequestProperty>()? {
|
||||
match &key.hash[0] {
|
||||
0x6570_7974 if !key.is_ref => {
|
||||
request.type_ = parser
|
||||
.next_token::<String>()?
|
||||
.unwrap_string_or_null("type")?;
|
||||
}
|
||||
0x6174_6164 if !key.is_ref => {
|
||||
parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?;
|
||||
loop {
|
||||
match parser.next_token::<Ignore>()? {
|
||||
Token::Comma => (),
|
||||
Token::ArrayEnd => break,
|
||||
Token::DictStart => {
|
||||
request.data.push(DataSourceObject::parse(parser)?);
|
||||
}
|
||||
token => return Err(token.error("", "DataSourceObject")),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
parser.skip_token(parser.depth_array, parser.depth_dict)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for DataSourceObject {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut data: Option<Vec<u8>> = None;
|
||||
let mut blob_id: Option<MaybeReference<BlobId, String>> = None;
|
||||
let mut offset: Option<usize> = None;
|
||||
let mut length: Option<usize> = None;
|
||||
|
||||
while let Some(key) = parser.next_dict_key::<RequestProperty>()? {
|
||||
match &key.hash[0] {
|
||||
0x0074_7865_5473_613a_6174_6164 if !key.is_ref => {
|
||||
data = parser
|
||||
.next_token::<String>()?
|
||||
.unwrap_string("data:asText")?
|
||||
.into_bytes()
|
||||
.into();
|
||||
}
|
||||
0x0034_3665_7361_4273_613a_6174_6164 if !key.is_ref => {
|
||||
data = base64_decode(
|
||||
parser
|
||||
.next_token::<String>()?
|
||||
.unwrap_string("data:asBase64")?
|
||||
.as_bytes(),
|
||||
)
|
||||
.ok_or_else(|| parser.error("Failed to decode data:asBase64"))?
|
||||
.into();
|
||||
}
|
||||
0x6449_626f_6c62 if !key.is_ref => {
|
||||
blob_id = parser
|
||||
.next_token::<MaybeReference<BlobId, String>>()?
|
||||
.unwrap_string("blobId")?
|
||||
.into();
|
||||
}
|
||||
0x6874_676e_656c if !key.is_ref => {
|
||||
length = parser
|
||||
.next_token::<Ignore>()?
|
||||
.unwrap_usize_or_null("length")?;
|
||||
}
|
||||
0x7465_7366_666f if !key.is_ref => {
|
||||
offset = parser
|
||||
.next_token::<Ignore>()?
|
||||
.unwrap_usize_or_null("offset")?;
|
||||
}
|
||||
_ => {
|
||||
parser.skip_token(parser.depth_array, parser.depth_dict)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(data) = data {
|
||||
Ok(DataSourceObject::Value(data))
|
||||
} else if let Some(blob_id) = blob_id {
|
||||
Ok(DataSourceObject::Id {
|
||||
id: blob_id,
|
||||
length,
|
||||
offset,
|
||||
})
|
||||
} else {
|
||||
Err(parser.error("Missing data or blobId in DataSourceObject"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(), obj.id.clone().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{json::Parser, Ignore},
|
||||
request::{RequestProperty, RequestPropertyParser},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GetArguments {
|
||||
pub offset: Option<usize>,
|
||||
pub length: Option<usize>,
|
||||
}
|
||||
|
||||
impl RequestPropertyParser for GetArguments {
|
||||
fn parse(
|
||||
&mut self,
|
||||
parser: &mut Parser,
|
||||
property: RequestProperty,
|
||||
) -> crate::parser::Result<bool> {
|
||||
match &property.hash[0] {
|
||||
0x7465_7366_666f => {
|
||||
self.offset = parser
|
||||
.next_token::<Ignore>()?
|
||||
.unwrap_usize_or_null("offset")?;
|
||||
}
|
||||
0x6874_676e_656c => {
|
||||
self.length = parser
|
||||
.next_token::<Ignore>()?
|
||||
.unwrap_usize_or_null("length")?;
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn gen_ids() {
|
||||
for label in ["sha-256", "sha-512"] {
|
||||
let mut iter = label.chars();
|
||||
let mut hash = [0; 2];
|
||||
let mut shift = 0;
|
||||
|
||||
'outer: for hash in hash.iter_mut() {
|
||||
for ch in iter.by_ref() {
|
||||
*hash |= (ch as u128) << shift;
|
||||
shift += 8;
|
||||
if shift == 128 {
|
||||
shift = 0;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
print!(
|
||||
"0x{}",
|
||||
format!("{:032x}", hash[0])
|
||||
.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
.replace("0000_", "")
|
||||
);
|
||||
if hash[1] != 0 {
|
||||
print!(
|
||||
", 0x{}",
|
||||
format!("{:032x}", hash[1])
|
||||
.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
.replace("0000_", "")
|
||||
);
|
||||
}
|
||||
println!(" => Property::{},", label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
pub mod blob;
|
||||
pub mod email;
|
||||
pub mod email_submission;
|
||||
pub mod index;
|
||||
|
||||
@@ -206,7 +206,7 @@ impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> {
|
||||
Token::String(item) => vec.push(item),
|
||||
Token::Comma => (),
|
||||
Token::ArrayEnd => break,
|
||||
token => return Err(token.error("", &token.to_string())),
|
||||
token => return Err(token.error("", "[ or string")),
|
||||
}
|
||||
}
|
||||
Ok(vec)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{fmt::Display, slice::Iter};
|
||||
use std::{fmt::Display, iter::Peekable, slice::Iter};
|
||||
|
||||
use crate::{error::method::MethodError, request::method::MethodObject};
|
||||
|
||||
@@ -32,7 +32,7 @@ const MAX_NESTED_LEVELS: u32 = 16;
|
||||
#[derive(Debug)]
|
||||
pub struct Parser<'x> {
|
||||
pub bytes: &'x [u8],
|
||||
pub iter: Iter<'x, u8>,
|
||||
pub iter: Peekable<Iter<'x, u8>>,
|
||||
pub next_ch: Option<u8>,
|
||||
pub pos: usize,
|
||||
pub pos_marker: usize,
|
||||
@@ -46,7 +46,7 @@ impl<'x> Parser<'x> {
|
||||
pub fn new(bytes: &'x [u8]) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
iter: bytes.iter(),
|
||||
iter: bytes.iter().peekable(),
|
||||
next_ch: None,
|
||||
pos: 0,
|
||||
pos_marker: 0,
|
||||
@@ -81,6 +81,11 @@ impl<'x> Parser<'x> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn peek_char(&mut self) -> Option<u8> {
|
||||
self.iter.peek().map(|&&ch| ch)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn next_char(&mut self) -> Option<u8> {
|
||||
self.pos += 1;
|
||||
|
||||
@@ -44,6 +44,10 @@ pub enum Capability {
|
||||
WebSocket = 1 << 6,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:sieve"))]
|
||||
Sieve = 1 << 7,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:blob"))]
|
||||
Blob = 1 << 8,
|
||||
#[serde(rename(serialize = "urn:ietf:params:jmap:quota"))]
|
||||
Quota = 1 << 9,
|
||||
}
|
||||
|
||||
impl JsonObjectParser for Capability {
|
||||
@@ -71,6 +75,8 @@ impl JsonObjectParser for Capability {
|
||||
0x0073_7261_646e_656c_6163 => Ok(Capability::Calendars),
|
||||
0x0074_656b_636f_7362_6577 => Ok(Capability::WebSocket),
|
||||
0x0065_7665_6973 => Ok(Capability::Sieve),
|
||||
0x626f_6c62 => Ok(Capability::Blob),
|
||||
0x0061_746f_7571 => Ok(Capability::Quota),
|
||||
_ => Err(parser.error_capability()),
|
||||
},
|
||||
Err(Error::Method(_)) => Err(parser.error_capability()),
|
||||
|
||||
@@ -45,6 +45,7 @@ pub enum MethodObject {
|
||||
VacationResponse,
|
||||
SieveScript,
|
||||
Principal,
|
||||
Quota,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -58,6 +59,8 @@ pub enum MethodFunction {
|
||||
Import,
|
||||
Parse,
|
||||
Validate,
|
||||
Lookup,
|
||||
Upload,
|
||||
Echo,
|
||||
}
|
||||
|
||||
@@ -109,6 +112,7 @@ impl JsonObjectParser for MethodName {
|
||||
0x6e6f_6974_7069_7263_7362_7553_6873_7550 => MethodObject::PushSubscription,
|
||||
0x0074_7069_7263_5365_7665_6953 => MethodObject::SieveScript,
|
||||
0x006c_6170_6963_6e69_7250 => MethodObject::Principal,
|
||||
0x0061_746f_7551 => MethodObject::Quota,
|
||||
0x6572_6f43 => MethodObject::Core,
|
||||
_ => return Err(parser.error_value()),
|
||||
},
|
||||
@@ -122,6 +126,8 @@ impl JsonObjectParser for MethodName {
|
||||
0x7472_6f70_6d69 => MethodFunction::Import,
|
||||
0x0065_7372_6170 => MethodFunction::Parse,
|
||||
0x6574_6164_696c_6176 => MethodFunction::Validate,
|
||||
0x7075_6b6f_6f6c => MethodFunction::Lookup,
|
||||
0x6461_6f6c_7075 => MethodFunction::Upload,
|
||||
0x6f68_6365 => MethodFunction::Echo,
|
||||
_ => return Err(parser.error_value()),
|
||||
},
|
||||
@@ -149,17 +155,18 @@ impl MethodName {
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match (self.fnc, self.obj) {
|
||||
(MethodFunction::Echo, MethodObject::Core) => "Core/echo",
|
||||
(MethodFunction::Copy, MethodObject::Blob) => "Blob/copy",
|
||||
(MethodFunction::Get, MethodObject::PushSubscription) => "PushSubscription/get",
|
||||
(MethodFunction::Set, MethodObject::PushSubscription) => "PushSubscription/set",
|
||||
|
||||
(MethodFunction::Get, MethodObject::Mailbox) => "Mailbox/get",
|
||||
(MethodFunction::Changes, MethodObject::Mailbox) => "Mailbox/changes",
|
||||
(MethodFunction::Query, MethodObject::Mailbox) => "Mailbox/query",
|
||||
(MethodFunction::QueryChanges, MethodObject::Mailbox) => "Mailbox/queryChanges",
|
||||
(MethodFunction::Set, MethodObject::Mailbox) => "Mailbox/set",
|
||||
|
||||
(MethodFunction::Get, MethodObject::Thread) => "Thread/get",
|
||||
(MethodFunction::Changes, MethodObject::Thread) => "Thread/changes",
|
||||
|
||||
(MethodFunction::Get, MethodObject::Email) => "Email/get",
|
||||
(MethodFunction::Changes, MethodObject::Email) => "Email/changes",
|
||||
(MethodFunction::Query, MethodObject::Email) => "Email/query",
|
||||
@@ -168,10 +175,13 @@ impl MethodName {
|
||||
(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",
|
||||
@@ -179,15 +189,30 @@ impl MethodName {
|
||||
"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::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::Echo, MethodObject::Core) => "Core/echo",
|
||||
_ => "error",
|
||||
}
|
||||
}
|
||||
@@ -208,6 +233,7 @@ impl Display for MethodObject {
|
||||
MethodObject::Mailbox => "Mailbox",
|
||||
MethodObject::Thread => "Thread",
|
||||
MethodObject::Email => "Email",
|
||||
MethodObject::Quota => "Quota",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,17 @@ use crate::{
|
||||
copy::{self, CopyBlobRequest, CopyRequest},
|
||||
get::{self, GetRequest},
|
||||
import::ImportEmailRequest,
|
||||
lookup::BlobLookupRequest,
|
||||
parse::ParseEmailRequest,
|
||||
query::{self, QueryRequest},
|
||||
query_changes::QueryChangesRequest,
|
||||
search_snippet::GetSearchSnippetRequest,
|
||||
set::{self, SetRequest},
|
||||
upload::BlobUploadRequest,
|
||||
validate::ValidateSieveScriptRequest,
|
||||
},
|
||||
parser::{json::Parser, JsonObjectParser},
|
||||
types::id::Id,
|
||||
types::any_id::AnyId,
|
||||
};
|
||||
|
||||
use self::{echo::Echo, method::MethodName};
|
||||
@@ -57,7 +59,7 @@ use self::{echo::Echo, method::MethodName};
|
||||
pub struct Request {
|
||||
pub using: u32,
|
||||
pub method_calls: Vec<Call<RequestMethod>>,
|
||||
pub created_ids: Option<HashMap<String, Id>>,
|
||||
pub created_ids: Option<HashMap<String, AnyId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -86,6 +88,8 @@ pub enum RequestMethod {
|
||||
Query(QueryRequest<query::RequestArguments>),
|
||||
SearchSnippet(GetSearchSnippetRequest),
|
||||
ValidateScript(ValidateSieveScriptRequest),
|
||||
LookupBlob(BlobLookupRequest),
|
||||
UploadBlob(BlobUploadRequest),
|
||||
Echo(Echo),
|
||||
Error(MethodError),
|
||||
}
|
||||
|
||||
@@ -33,15 +33,17 @@ use crate::{
|
||||
copy::{CopyBlobRequest, CopyRequest},
|
||||
get::GetRequest,
|
||||
import::ImportEmailRequest,
|
||||
lookup::BlobLookupRequest,
|
||||
parse::ParseEmailRequest,
|
||||
query::QueryRequest,
|
||||
query_changes::QueryChangesRequest,
|
||||
search_snippet::GetSearchSnippetRequest,
|
||||
set::SetRequest,
|
||||
upload::BlobUploadRequest,
|
||||
validate::ValidateSieveScriptRequest,
|
||||
},
|
||||
parser::{json::Parser, Error, Ignore, JsonObjectParser, Token},
|
||||
types::id::Id,
|
||||
types::any_id::AnyId,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -129,13 +131,23 @@ impl Request {
|
||||
let start_depth_dict = parser.depth_dict;
|
||||
|
||||
let method = match (&method_name.fnc, &method_name.obj) {
|
||||
(MethodFunction::Get, _) => {
|
||||
if method_name.obj != MethodObject::SearchSnippet {
|
||||
GetRequest::parse(parser).map(RequestMethod::Get)
|
||||
} else {
|
||||
GetSearchSnippetRequest::parse(parser)
|
||||
.map(RequestMethod::SearchSnippet)
|
||||
}
|
||||
(
|
||||
MethodFunction::Get,
|
||||
MethodObject::Email
|
||||
| MethodObject::Mailbox
|
||||
| MethodObject::Thread
|
||||
| MethodObject::Identity
|
||||
| MethodObject::EmailSubmission
|
||||
| MethodObject::PushSubscription
|
||||
| MethodObject::VacationResponse
|
||||
| MethodObject::SieveScript
|
||||
| MethodObject::Principal
|
||||
| MethodObject::Quota
|
||||
| MethodObject::Blob,
|
||||
) => GetRequest::parse(parser).map(RequestMethod::Get),
|
||||
(MethodFunction::Get, MethodObject::SearchSnippet) => {
|
||||
GetSearchSnippetRequest::parse(parser)
|
||||
.map(RequestMethod::SearchSnippet)
|
||||
}
|
||||
(MethodFunction::Query, _) => {
|
||||
QueryRequest::parse(parser).map(RequestMethod::Query)
|
||||
@@ -155,6 +167,12 @@ impl Request {
|
||||
(MethodFunction::Copy, MethodObject::Blob) => {
|
||||
CopyBlobRequest::parse(parser).map(RequestMethod::CopyBlob)
|
||||
}
|
||||
(MethodFunction::Lookup, MethodObject::Blob) => {
|
||||
BlobLookupRequest::parse(parser).map(RequestMethod::LookupBlob)
|
||||
}
|
||||
(MethodFunction::Upload, MethodObject::Blob) => {
|
||||
BlobUploadRequest::parse(parser).map(RequestMethod::UploadBlob)
|
||||
}
|
||||
(MethodFunction::Import, MethodObject::Email) => {
|
||||
ImportEmailRequest::parse(parser).map(RequestMethod::ImportEmail)
|
||||
}
|
||||
@@ -204,8 +222,10 @@ impl Request {
|
||||
let mut created_ids = HashMap::new();
|
||||
parser.next_token::<Ignore>()?.assert(Token::DictStart)?;
|
||||
while let Some(key) = parser.next_dict_key::<String>()? {
|
||||
created_ids
|
||||
.insert(key, parser.next_token::<Id>()?.unwrap_string("createdIds")?);
|
||||
created_ids.insert(
|
||||
key,
|
||||
parser.next_token::<AnyId>()?.unwrap_string("createdIds")?,
|
||||
);
|
||||
}
|
||||
self.created_ids = Some(created_ids);
|
||||
Ok(true)
|
||||
|
||||
@@ -52,6 +52,13 @@ impl<V, R> MaybeReference<V, R> {
|
||||
MaybeReference::Reference(_) => panic!("unwrap() called on MaybeReference::Reference"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap(self) -> Option<V> {
|
||||
match self {
|
||||
MaybeReference::Value(v) => Some(v),
|
||||
MaybeReference::Reference(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for ResultReference {
|
||||
@@ -98,6 +105,20 @@ impl JsonObjectParser for ResultReference {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JsonObjectParser> JsonObjectParser for MaybeReference<T, String> {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(b'#') = parser.peek_char() {
|
||||
parser.next_unescaped()?;
|
||||
String::parse(parser).map(MaybeReference::Reference)
|
||||
} else {
|
||||
T::parse(parser).map(MaybeReference::Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ResultReference {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::{
|
||||
parser::{json::Parser, Error, JsonObjectParser, Token},
|
||||
request::Call,
|
||||
response::{serialize::serialize_hex, Response, ResponseMethod},
|
||||
types::{id::Id, state::State, type_state::TypeState},
|
||||
types::{any_id::AnyId, id::Id, state::State, type_state::DataType},
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct WebSocketResponse {
|
||||
|
||||
#[serde(rename(deserialize = "createdIds"))]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
created_ids: HashMap<String, Id>,
|
||||
created_ids: HashMap<String, AnyId>,
|
||||
|
||||
#[serde(rename = "requestId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -68,7 +68,7 @@ pub enum WebSocketResponseType {
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct WebSocketPushEnable {
|
||||
pub data_types: Vec<TypeState>,
|
||||
pub data_types: Vec<DataType>,
|
||||
pub push_state: Option<String>,
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub enum WebSocketStateChangeType {
|
||||
pub struct WebSocketStateChange {
|
||||
#[serde(rename = "@type")]
|
||||
pub type_: WebSocketStateChangeType,
|
||||
pub changed: VecMap<Id, VecMap<TypeState, State>>,
|
||||
pub changed: VecMap<Id, VecMap<DataType, State>>,
|
||||
#[serde(rename = "pushState")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
push_state: Option<String>,
|
||||
@@ -162,7 +162,7 @@ impl WebSocketMessage {
|
||||
}
|
||||
0x0073_6570_7954_6174_6164 => {
|
||||
push_enable.data_types =
|
||||
<Option<Vec<TypeState>>>::parse(&mut parser)?.unwrap_or_default();
|
||||
<Option<Vec<DataType>>>::parse(&mut parser)?.unwrap_or_default();
|
||||
found_push_keys = true;
|
||||
}
|
||||
0x0065_7461_7453_6873_7570 => {
|
||||
|
||||
@@ -33,15 +33,17 @@ use crate::{
|
||||
copy::{CopyBlobResponse, CopyResponse},
|
||||
get::GetResponse,
|
||||
import::ImportEmailResponse,
|
||||
lookup::BlobLookupResponse,
|
||||
parse::ParseEmailResponse,
|
||||
query::QueryResponse,
|
||||
query_changes::QueryChangesResponse,
|
||||
search_snippet::GetSearchSnippetResponse,
|
||||
set::SetResponse,
|
||||
upload::BlobUploadResponse,
|
||||
validate::ValidateSieveScriptResponse,
|
||||
},
|
||||
request::{echo::Echo, method::MethodName, Call},
|
||||
types::id::Id,
|
||||
types::any_id::AnyId,
|
||||
};
|
||||
|
||||
use self::serialize::serialize_hex;
|
||||
@@ -60,6 +62,8 @@ pub enum ResponseMethod {
|
||||
Query(QueryResponse),
|
||||
SearchSnippet(GetSearchSnippetResponse),
|
||||
ValidateScript(ValidateSieveScriptResponse),
|
||||
LookupBlob(BlobLookupResponse),
|
||||
UploadBlob(BlobUploadResponse),
|
||||
Echo(Echo),
|
||||
Error(MethodError),
|
||||
}
|
||||
@@ -75,11 +79,11 @@ pub struct Response {
|
||||
|
||||
#[serde(rename = "createdIds")]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub created_ids: HashMap<String, Id>,
|
||||
pub created_ids: HashMap<String, AnyId>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(session_state: u32, created_ids: HashMap<String, Id>, capacity: usize) -> Self {
|
||||
pub fn new(session_state: u32, created_ids: HashMap<String, AnyId>, capacity: usize) -> Self {
|
||||
Response {
|
||||
session_state,
|
||||
created_ids,
|
||||
@@ -108,8 +112,8 @@ impl Response {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_created_id(&mut self, create_id: String, id: Id) {
|
||||
self.created_ids.insert(create_id, id);
|
||||
pub fn push_created_id(&mut self, create_id: String, id: impl Into<AnyId>) {
|
||||
self.created_ids.insert(create_id, id.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +195,18 @@ impl From<ValidateSieveScriptResponse> for ResponseMethod {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobUploadResponse> for ResponseMethod {
|
||||
fn from(upload_blob: BlobUploadResponse) -> Self {
|
||||
ResponseMethod::UploadBlob(upload_blob)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobLookupResponse> for ResponseMethod {
|
||||
fn from(lookup_blob: BlobLookupResponse) -> Self {
|
||||
ResponseMethod::LookupBlob(lookup_blob)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<ResponseMethod>> From<Result<T, MethodError>> for ResponseMethod {
|
||||
fn from(result: Result<T, MethodError>) -> Self {
|
||||
match result {
|
||||
|
||||
@@ -27,13 +27,14 @@ use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{
|
||||
error::{method::MethodError, set::SetError},
|
||||
method::{copy::CopyResponse, set::SetResponse},
|
||||
method::{copy::CopyResponse, set::SetResponse, upload::DataSourceObject},
|
||||
object::Object,
|
||||
request::{
|
||||
reference::{MaybeReference, ResultReference},
|
||||
RequestMethod,
|
||||
},
|
||||
types::{
|
||||
any_id::AnyId,
|
||||
id::Id,
|
||||
property::Property,
|
||||
value::{MaybePatchValue, SetValue, Value},
|
||||
@@ -53,11 +54,27 @@ impl Response {
|
||||
match request {
|
||||
RequestMethod::Get(request) => {
|
||||
// Resolve id references
|
||||
if let Some(MaybeReference::Reference(reference)) = &request.ids {
|
||||
request.ids = Some(MaybeReference::Value(
|
||||
self.eval_result_references(reference)
|
||||
.unwrap_ids(reference)?,
|
||||
));
|
||||
match &mut request.ids {
|
||||
Some(MaybeReference::Reference(reference)) => {
|
||||
request.ids = Some(MaybeReference::Value(
|
||||
self.eval_result_references(reference)
|
||||
.unwrap_any_ids(reference)?,
|
||||
));
|
||||
}
|
||||
Some(MaybeReference::Value(ids)) => {
|
||||
for id in ids {
|
||||
if let MaybeReference::Reference(reference) = id {
|
||||
if let Some(resolved_id) = self.created_ids.get(reference) {
|
||||
*id = MaybeReference::Value(resolved_id.clone());
|
||||
} else {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Id reference {reference:?} does not exist."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Resolve properties references
|
||||
@@ -78,61 +95,7 @@ impl Response {
|
||||
|
||||
// Perform topological sort
|
||||
if !graph.is_empty() {
|
||||
// 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(MethodError::InvalidResultReference(format!(
|
||||
"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(MethodError::InvalidArguments(
|
||||
"Cyclical references are not allowed.".to_string(),
|
||||
));
|
||||
}
|
||||
it = to_ids.iter();
|
||||
continue;
|
||||
} else if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((prev_it, from_id)) = it_stack.pop() {
|
||||
it = prev_it;
|
||||
if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
if !create.is_empty() {
|
||||
for (id, value) in std::mem::take(create) {
|
||||
sorted_create.append(id, value);
|
||||
}
|
||||
}
|
||||
request.create = sorted_create.into();
|
||||
request.create = topological_sort(create, graph)?.into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +155,38 @@ impl Response {
|
||||
);
|
||||
}
|
||||
}
|
||||
RequestMethod::UploadBlob(request) => {
|
||||
let mut graph = HashMap::with_capacity(request.create.len());
|
||||
for (create_id, object) in request.create.iter_mut() {
|
||||
for data in &mut object.data {
|
||||
if let DataSourceObject::Id { id, .. } = data {
|
||||
if let MaybeReference::Reference(parent_id) = id {
|
||||
match self.created_ids.get(parent_id) {
|
||||
Some(AnyId::Blob(blob_id)) => {
|
||||
*id = MaybeReference::Value(blob_id.clone());
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"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() {
|
||||
request.create = topological_sort(&mut request.create, graph)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -269,7 +264,7 @@ impl Response {
|
||||
}
|
||||
|
||||
fn eval_id_reference(&self, ir: &str) -> Result<Id, MethodError> {
|
||||
if let Some(id) = self.created_ids.get(ir) {
|
||||
if let Some(AnyId::Id(id)) = self.created_ids.get(ir) {
|
||||
Ok(*id)
|
||||
} else {
|
||||
Err(MethodError::InvalidResultReference(format!(
|
||||
@@ -287,7 +282,7 @@ impl Response {
|
||||
match set_value {
|
||||
SetValue::IdReference(MaybeReference::Reference(parent_id)) => {
|
||||
if let Some(id) = self.created_ids.get(parent_id) {
|
||||
*set_value = SetValue::Value(Value::Id(*id));
|
||||
*set_value = SetValue::Value(id.into());
|
||||
} else if let Some((child_id, graph)) = &mut graph {
|
||||
graph
|
||||
.entry(child_id.to_string())
|
||||
@@ -303,7 +298,7 @@ impl Response {
|
||||
for id_ref in id_refs {
|
||||
if let MaybeReference::Reference(parent_id) = id_ref {
|
||||
if let Some(id) = self.created_ids.get(parent_id) {
|
||||
*id_ref = MaybeReference::Value(*id);
|
||||
*id_ref = MaybeReference::Value(id.clone());
|
||||
} else if let Some((child_id, graph)) = &mut graph {
|
||||
graph
|
||||
.entry(child_id.to_string())
|
||||
@@ -329,8 +324,69 @@ impl Response {
|
||||
}
|
||||
}
|
||||
|
||||
fn topological_sort<T>(
|
||||
create: &mut VecMap<String, T>,
|
||||
graph: HashMap<String, Vec<String>>,
|
||||
) -> Result<VecMap<String, T>, MethodError> {
|
||||
// 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(MethodError::InvalidResultReference(format!(
|
||||
"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(MethodError::InvalidArguments(
|
||||
"Cyclical references are not allowed.".to_string(),
|
||||
));
|
||||
}
|
||||
it = to_ids.iter();
|
||||
continue;
|
||||
} else if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((prev_it, from_id)) = it_stack.pop() {
|
||||
it = prev_it;
|
||||
if let Some((id, value)) = create.remove_entry(from_id) {
|
||||
sorted_create.append(id, value);
|
||||
if create.is_empty() {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
if !create.is_empty() {
|
||||
for (id, value) in std::mem::take(create) {
|
||||
sorted_create.append(id, value);
|
||||
}
|
||||
}
|
||||
Ok(sorted_create)
|
||||
}
|
||||
|
||||
pub trait EvalObjectReferences {
|
||||
fn get_id(&self, id_ref: &str) -> Option<&Id>;
|
||||
fn get_id(&self, id_ref: &str) -> Option<Value>;
|
||||
|
||||
fn eval_object_references(&self, set_value: SetValue) -> Result<MaybePatchValue, SetError> {
|
||||
match set_value {
|
||||
@@ -338,25 +394,31 @@ pub trait EvalObjectReferences {
|
||||
SetValue::Patch(patch) => Ok(MaybePatchValue::Patch(patch)),
|
||||
SetValue::IdReference(MaybeReference::Reference(id_ref)) => {
|
||||
if let Some(id) = self.get_id(&id_ref) {
|
||||
Ok(MaybePatchValue::Value(Value::Id(*id)))
|
||||
Ok(MaybePatchValue::Value(id))
|
||||
} else {
|
||||
Err(SetError::not_found()
|
||||
.with_description(format!("Id reference {id_ref:?} not found.")))
|
||||
}
|
||||
}
|
||||
SetValue::IdReference(MaybeReference::Value(id)) => {
|
||||
SetValue::IdReference(MaybeReference::Value(AnyId::Id(id))) => {
|
||||
Ok(MaybePatchValue::Value(Value::Id(id)))
|
||||
}
|
||||
SetValue::IdReference(MaybeReference::Value(AnyId::Blob(blob_id))) => {
|
||||
Ok(MaybePatchValue::Value(Value::BlobId(blob_id)))
|
||||
}
|
||||
SetValue::IdReferences(id_refs) => {
|
||||
let mut ids = Vec::with_capacity(id_refs.len());
|
||||
for id_ref in id_refs {
|
||||
match id_ref {
|
||||
MaybeReference::Value(id) => {
|
||||
MaybeReference::Value(AnyId::Id(id)) => {
|
||||
ids.push(Value::Id(id));
|
||||
}
|
||||
MaybeReference::Value(AnyId::Blob(blob_id)) => {
|
||||
ids.push(Value::BlobId(blob_id));
|
||||
}
|
||||
MaybeReference::Reference(id_ref) => {
|
||||
if let Some(id) = self.get_id(&id_ref) {
|
||||
ids.push(Value::Id(*id));
|
||||
ids.push(id);
|
||||
} else {
|
||||
return Err(SetError::not_found().with_description(format!(
|
||||
"Id reference {id_ref:?} not found."
|
||||
@@ -373,16 +435,20 @@ pub trait EvalObjectReferences {
|
||||
}
|
||||
|
||||
impl EvalObjectReferences for SetResponse {
|
||||
fn get_id(&self, id_ref: &str) -> Option<&Id> {
|
||||
fn get_id(&self, id_ref: &str) -> Option<Value> {
|
||||
self.created
|
||||
.get(id_ref)
|
||||
.and_then(|obj| obj.properties.get(&Property::Id))
|
||||
.and_then(|v| v.as_id())
|
||||
.and_then(|value| match value {
|
||||
Value::Id(id) => Value::Id(*id).into(),
|
||||
Value::BlobId(blob_id) => Value::BlobId(blob_id.clone()).into(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalObjectReferences for CopyResponse {
|
||||
fn get_id(&self, _id_ref: &str) -> Option<&Id> {
|
||||
fn get_id(&self, _id_ref: &str) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -396,12 +462,53 @@ impl EvalResult {
|
||||
Value::Id(id) => ids.push(id),
|
||||
Value::List(list) => {
|
||||
for value in list {
|
||||
if let Value::Id(id) = value {
|
||||
ids.push(id);
|
||||
} else {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Failed to evaluate {rr} result reference."
|
||||
)));
|
||||
match value {
|
||||
Value::Id(id) => ids.push(id),
|
||||
_ => {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Failed to evaluate {rr} result reference."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Failed to evaluate {rr} result reference."
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
} else {
|
||||
Err(MethodError::InvalidResultReference(format!(
|
||||
"Failed to evaluate {rr} result reference."
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_any_ids(
|
||||
self,
|
||||
rr: &ResultReference,
|
||||
) -> Result<Vec<MaybeReference<AnyId, String>>, MethodError> {
|
||||
if let EvalResult::Values(values) = self {
|
||||
let mut ids = Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
match value {
|
||||
Value::Id(id) => ids.push(MaybeReference::Value(id.into())),
|
||||
Value::BlobId(blob_id) => ids.push(MaybeReference::Value(blob_id.into())),
|
||||
Value::List(list) => {
|
||||
for value in list {
|
||||
match value {
|
||||
Value::Id(id) => ids.push(MaybeReference::Value(id.into())),
|
||||
Value::BlobId(blob_id) => {
|
||||
ids.push(MaybeReference::Value(blob_id.into()))
|
||||
}
|
||||
_ => {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Failed to evaluate {rr} result reference."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,9 +861,15 @@ mod tests {
|
||||
panic!("Expected Mailbox Set Request");
|
||||
}
|
||||
|
||||
response.created_ids.insert("a".to_string(), Id::new(5));
|
||||
response.created_ids.insert("b".to_string(), Id::new(6));
|
||||
response.created_ids.insert("c".to_string(), Id::new(7));
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
parser::{json::Parser, JsonObjectParser},
|
||||
request::reference::MaybeReference,
|
||||
};
|
||||
|
||||
use super::{blob::BlobId, id::Id, value::Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AnyId {
|
||||
Id(Id),
|
||||
Blob(BlobId),
|
||||
}
|
||||
|
||||
impl AnyId {
|
||||
pub fn as_id(&self) -> Option<&Id> {
|
||||
match self {
|
||||
AnyId::Id(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_blob_id(&self) -> Option<&BlobId> {
|
||||
match self {
|
||||
AnyId::Blob(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_id(self) -> Option<Id> {
|
||||
match self {
|
||||
AnyId::Id(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_blob_id(self) -> Option<BlobId> {
|
||||
match self {
|
||||
AnyId::Blob(id) => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for AnyId {
|
||||
fn from(id: Id) -> Self {
|
||||
Self::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlobId> for AnyId {
|
||||
fn from(id: BlobId) -> Self {
|
||||
Self::Blob(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MaybeReference<Id, String>> for MaybeReference<AnyId, String> {
|
||||
fn from(value: MaybeReference<Id, String>) -> Self {
|
||||
match value {
|
||||
MaybeReference::Value(value) => MaybeReference::Value(value.into()),
|
||||
MaybeReference::Reference(reference) => MaybeReference::Reference(reference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MaybeReference<BlobId, String>> for MaybeReference<AnyId, String> {
|
||||
fn from(value: MaybeReference<BlobId, String>) -> Self {
|
||||
match value {
|
||||
MaybeReference::Value(value) => MaybeReference::Value(value.into()),
|
||||
MaybeReference::Reference(reference) => MaybeReference::Reference(reference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyId> for Value {
|
||||
fn from(value: AnyId) -> Self {
|
||||
match value {
|
||||
AnyId::Id(id) => Value::Id(id),
|
||||
AnyId::Blob(blob_id) => Value::BlobId(blob_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AnyId> for Value {
|
||||
fn from(value: &AnyId) -> Self {
|
||||
match value {
|
||||
AnyId::Id(id) => Value::Id(*id),
|
||||
AnyId::Blob(blob_id) => Value::BlobId(blob_id.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for AnyId {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut id = Vec::with_capacity(16);
|
||||
|
||||
while let Some(ch) = parser.next_unescaped()? {
|
||||
id.push(ch);
|
||||
}
|
||||
|
||||
if id.is_empty() {
|
||||
return Err(parser.error_value());
|
||||
}
|
||||
|
||||
BlobId::from_base32(&id)
|
||||
.map(AnyId::Blob)
|
||||
.or_else(|| Id::from_bytes(&id).map(AnyId::Id))
|
||||
.ok_or_else(|| parser.error_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for AnyId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
AnyId::Id(id) => id.serialize(serializer),
|
||||
AnyId::Blob(id) => id.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,8 +119,8 @@ impl BlobId {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_base32(value: &str) -> Option<Self> {
|
||||
BlobId::from_iter(&mut Base32Reader::new(value.as_bytes()))
|
||||
pub fn from_base32(value: impl AsRef<[u8]>) -> Option<Self> {
|
||||
BlobId::from_iter(&mut Base32Reader::new(value.as_ref()))
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use utils::map::bitmap::BitmapItem;
|
||||
|
||||
use super::type_state::TypeState;
|
||||
use super::type_state::DataType;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
@@ -85,16 +85,18 @@ impl From<Collection> for u64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Collection> for TypeState {
|
||||
impl TryFrom<Collection> for DataType {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Collection) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Collection::Email => Ok(TypeState::Email),
|
||||
Collection::Mailbox => Ok(TypeState::Mailbox),
|
||||
Collection::Thread => Ok(TypeState::Thread),
|
||||
Collection::Identity => Ok(TypeState::Identity),
|
||||
Collection::EmailSubmission => Ok(TypeState::EmailSubmission),
|
||||
Collection::Email => Ok(DataType::Email),
|
||||
Collection::Mailbox => Ok(DataType::Mailbox),
|
||||
Collection::Thread => Ok(DataType::Thread),
|
||||
Collection::Identity => Ok(DataType::Identity),
|
||||
Collection::EmailSubmission => Ok(DataType::EmailSubmission),
|
||||
Collection::SieveScript => Ok(DataType::SieveScript),
|
||||
Collection::PushSubscription => Ok(DataType::PushSubscription),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ use std::ops::Deref;
|
||||
|
||||
use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE};
|
||||
|
||||
use crate::{
|
||||
parser::{json::Parser, JsonObjectParser},
|
||||
request::reference::MaybeReference,
|
||||
};
|
||||
use crate::parser::{json::Parser, JsonObjectParser};
|
||||
|
||||
use super::DocumentId;
|
||||
|
||||
@@ -63,38 +60,6 @@ impl JsonObjectParser for Id {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for MaybeReference<Id, String> {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let ch = parser
|
||||
.next_unescaped()?
|
||||
.ok_or_else(|| parser.error_value())?;
|
||||
|
||||
if ch != b'#' {
|
||||
let mut id = BASE32_INVERSE[ch as usize] as u64;
|
||||
|
||||
if id != u8::MAX as u64 {
|
||||
while let Some(ch) = parser.next_unescaped()? {
|
||||
let i = BASE32_INVERSE[ch as usize];
|
||||
if i != u8::MAX {
|
||||
id = (id << 5) | i as u64;
|
||||
} else {
|
||||
return Err(parser.error_value());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MaybeReference::Value(Id { id }))
|
||||
} else {
|
||||
Err(parser.error_value())
|
||||
}
|
||||
} else {
|
||||
String::parse(parser).map(MaybeReference::Reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub fn new(id: u64) -> Self {
|
||||
Self { id }
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use crate::parser::{json::Parser, JsonObjectParser};
|
||||
|
||||
pub mod acl;
|
||||
pub mod any_id;
|
||||
pub mod blob;
|
||||
pub mod collection;
|
||||
pub mod date;
|
||||
@@ -35,3 +38,35 @@ pub mod value;
|
||||
|
||||
pub type DocumentId = u32;
|
||||
pub type ChangeId = u64;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MaybeUnparsable<V> {
|
||||
Value(V),
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl<V: JsonObjectParser> JsonObjectParser for MaybeUnparsable<V> {
|
||||
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
|
||||
match V::parse(parser) {
|
||||
Ok(value) => Ok(MaybeUnparsable::Value(value)),
|
||||
Err(_) if parser.is_eof || parser.skip_string() => Ok(MaybeUnparsable::ParseError(
|
||||
String::from_utf8_lossy(parser.bytes[parser.pos_marker..parser.pos - 1].as_ref())
|
||||
.into_owned(),
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaybeUnparsable de/serialization
|
||||
impl<V: serde::Serialize> serde::Serialize for MaybeUnparsable<V> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
MaybeUnparsable::Value(value) => value.serialize(serializer),
|
||||
MaybeUnparsable::ParseError(str) => serializer.serialize_str(str),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +130,31 @@ pub enum Property {
|
||||
MayCreateChild,
|
||||
MayRename,
|
||||
MaySubmit,
|
||||
ResourceType,
|
||||
Used,
|
||||
HardLimit,
|
||||
WarnLimit,
|
||||
SoftLimit,
|
||||
Scope,
|
||||
Digest(DigestProperty),
|
||||
Data(DataProperty),
|
||||
_T(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub enum DigestProperty {
|
||||
Sha,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub enum DataProperty {
|
||||
AsText,
|
||||
AsBase64,
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct SetProperty {
|
||||
pub property: Property,
|
||||
@@ -165,8 +187,12 @@ impl JsonObjectParser for Property {
|
||||
} else {
|
||||
first_char = ch;
|
||||
}
|
||||
} else if ch == b':' && first_char == b'h' && hash == 0x0072_6564_6165 {
|
||||
return parse_header_property(parser);
|
||||
} else if ch == b':' {
|
||||
return if first_char == b'h' && hash == 0x0072_6564_6165 {
|
||||
parse_header_property(parser)
|
||||
} else {
|
||||
parse_sub_property(parser, first_char, hash)
|
||||
};
|
||||
} else {
|
||||
return parser.invalid_property();
|
||||
}
|
||||
@@ -348,6 +374,7 @@ fn parse_property(first_char: u8, hash: u128) -> Option<Property> {
|
||||
0x0064_4974_6e65_696c_4365_6369_7665 => Property::DeviceClientId,
|
||||
0x6e6f_6974_6973_6f70_7369 => Property::Disposition,
|
||||
0x0073_6449_626f_6c42_6e73 => Property::DsnBlobIds,
|
||||
0x0061_7461 => Property::Data(DataProperty::Default),
|
||||
_ => return None,
|
||||
},
|
||||
b'e' => match hash {
|
||||
@@ -539,6 +566,41 @@ fn parse_header_property(parser: &mut Parser) -> crate::parser::Result<Property>
|
||||
Ok(Property::Header(HeaderProperty { form, header, all }))
|
||||
}
|
||||
|
||||
fn parse_sub_property(
|
||||
parser: &mut Parser,
|
||||
first_char: u8,
|
||||
parent_hash: u128,
|
||||
) -> crate::parser::Result<Property> {
|
||||
let mut hash = 0;
|
||||
let mut shift = 0;
|
||||
|
||||
while let Some(ch) = parser.next_unescaped()? {
|
||||
if ch.is_ascii_alphanumeric() || ch == b'-' {
|
||||
if shift < 128 {
|
||||
hash |= (ch as u128) << shift;
|
||||
shift += 8;
|
||||
} else {
|
||||
return parser.invalid_property();
|
||||
}
|
||||
} else {
|
||||
return parser.invalid_property();
|
||||
}
|
||||
}
|
||||
|
||||
match (first_char, parent_hash, hash) {
|
||||
(b'd', 0x0061_7461, 0x7478_6554_7361) => Ok(Property::Data(DataProperty::AsText)),
|
||||
(b'd', 0x0061_7461, 0x3436_6573_6142_7361) => Ok(Property::Data(DataProperty::AsBase64)),
|
||||
(b'd', 0x0074_7365_6769, 0x0061_6873) => Ok(Property::Digest(DigestProperty::Sha)),
|
||||
(b'd', 0x0074_7365_6769, 0x0036_3532_2d61_6873) => {
|
||||
Ok(Property::Digest(DigestProperty::Sha256))
|
||||
}
|
||||
(b'd', 0x0074_7365_6769, 0x0032_3135_2d61_6873) => {
|
||||
Ok(Property::Digest(DigestProperty::Sha512))
|
||||
}
|
||||
_ => parser.invalid_property(),
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for ObjectProperty {
|
||||
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
|
||||
let mut first_char = 0;
|
||||
@@ -591,6 +653,7 @@ impl JsonObjectParser for ObjectProperty {
|
||||
},
|
||||
b'h' => match hash {
|
||||
0x7372_6564_6165 => Property::Headers,
|
||||
0x7469_6d69_4c64_7261 => Property::HardLimit,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b'i' => match hash {
|
||||
@@ -628,22 +691,33 @@ impl JsonObjectParser for ObjectProperty {
|
||||
},
|
||||
b'r' => match hash {
|
||||
0x006f_5474_7063 => Property::RcptTo,
|
||||
0x0065_7079_5465_6372_756f_7365 => Property::ResourceType,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b's' => match hash {
|
||||
0x0065_7a69 => Property::Size,
|
||||
0x0073_7472_6150_6275 => Property::SubParts,
|
||||
0x796c_7065_5270_746d => Property::SmtpReply,
|
||||
0x7469_6d69_4c74_666f => Property::SoftLimit,
|
||||
0x6570_6f63 => Property::Scope,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b't' => match hash {
|
||||
0x0065_7079 => Property::Type,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b'u' => match hash {
|
||||
0x0064_6573 => Property::Used,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b'v' => match hash {
|
||||
0x6575_6c61 => Property::Value,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
b'w' => match hash {
|
||||
0x7469_6d69_4c6e_7261 => Property::WarnLimit,
|
||||
_ => parser.invalid_property()?,
|
||||
},
|
||||
_ => parser.invalid_property()?,
|
||||
}))
|
||||
}
|
||||
@@ -810,6 +884,22 @@ impl Display for Property {
|
||||
Property::MayCreateChild => write!(f, "mayCreateChild"),
|
||||
Property::MayRename => write!(f, "mayRename"),
|
||||
Property::MaySubmit => write!(f, "maySubmit"),
|
||||
Property::Data(data) => f.write_str(match data {
|
||||
DataProperty::AsText => "data:asText",
|
||||
DataProperty::AsBase64 => "data:asBase64",
|
||||
DataProperty::Default => "data",
|
||||
}),
|
||||
Property::Digest(digest) => f.write_str(match digest {
|
||||
DigestProperty::Sha => "digest:sha",
|
||||
DigestProperty::Sha256 => "digest:sha-256",
|
||||
DigestProperty::Sha512 => "digest:sha-512",
|
||||
}),
|
||||
Property::ResourceType => write!(f, "resourceType"),
|
||||
Property::Used => write!(f, "used"),
|
||||
Property::HardLimit => write!(f, "hardLimit"),
|
||||
Property::Scope => write!(f, "scope"),
|
||||
Property::WarnLimit => write!(f, "warnLimit"),
|
||||
Property::SoftLimit => write!(f, "softLimit"),
|
||||
Property::_T(s) => write!(f, "{s}"),
|
||||
}
|
||||
}
|
||||
@@ -984,6 +1074,13 @@ impl From<&Property> for u8 {
|
||||
Property::IdentityId => 95,
|
||||
Property::InReplyTo => 96,
|
||||
Property::_T(_) => 97,
|
||||
Property::ResourceType => 98,
|
||||
Property::Used => 99,
|
||||
Property::HardLimit => 100,
|
||||
Property::WarnLimit => 101,
|
||||
Property::SoftLimit => 102,
|
||||
Property::Scope => 103,
|
||||
Property::Digest(_) | Property::Data(_) => unreachable!("invalid property"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1119,6 +1216,15 @@ impl SerializeInto for Property {
|
||||
value.serialize_into(buf);
|
||||
return;
|
||||
}
|
||||
Property::ResourceType => 98,
|
||||
Property::Used => 99,
|
||||
Property::HardLimit => 100,
|
||||
Property::WarnLimit => 101,
|
||||
Property::SoftLimit => 102,
|
||||
Property::Scope => 103,
|
||||
Property::Digest(_) | Property::Data(_) => {
|
||||
unreachable!("Property::Digest and Property::Data are not serializable")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1228,6 +1334,12 @@ impl DeserializeFrom for Property {
|
||||
95 => Some(Property::IdentityId),
|
||||
96 => Some(Property::InReplyTo),
|
||||
97 => String::deserialize_from(bytes).map(Property::_T),
|
||||
98 => Some(Property::ResourceType),
|
||||
99 => Some(Property::Used),
|
||||
100 => Some(Property::HardLimit),
|
||||
101 => Some(Property::WarnLimit),
|
||||
102 => Some(Property::SoftLimit),
|
||||
103 => Some(Property::Scope),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use utils::codec::{
|
||||
|
||||
use crate::parser::{base32::JsonBase32Reader, json::Parser, JsonObjectParser};
|
||||
|
||||
use super::{type_state::TypeState, ChangeId};
|
||||
use super::{type_state::DataType, ChangeId};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JMAPIntermediateState {
|
||||
@@ -48,7 +48,7 @@ pub enum State {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StateChange {
|
||||
pub account_id: u32,
|
||||
pub types: Vec<(TypeState, u64)>,
|
||||
pub types: Vec<(DataType, u64)>,
|
||||
}
|
||||
|
||||
impl StateChange {
|
||||
@@ -59,7 +59,7 @@ impl StateChange {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_change(mut self, type_state: TypeState, change_id: u64) -> Self {
|
||||
pub fn with_change(mut self, type_state: DataType, change_id: u64) -> Self {
|
||||
if let Some((_, last_change_id)) = self.types.iter_mut().find(|(ts, _)| ts == &type_state) {
|
||||
*last_change_id = change_id;
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::parser::{json::Parser, JsonObjectParser};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize)]
|
||||
#[repr(u8)]
|
||||
pub enum TypeState {
|
||||
pub enum DataType {
|
||||
#[serde(rename = "Email")]
|
||||
Email = 0,
|
||||
#[serde(rename = "EmailDelivery")]
|
||||
@@ -44,43 +44,64 @@ pub enum TypeState {
|
||||
Thread = 4,
|
||||
#[serde(rename = "Identity")]
|
||||
Identity = 5,
|
||||
None = 6,
|
||||
#[serde(rename = "Core")]
|
||||
Core = 6,
|
||||
#[serde(rename = "PushSubscription")]
|
||||
PushSubscription = 7,
|
||||
#[serde(rename = "SearchSnippet")]
|
||||
SearchSnippet = 8,
|
||||
#[serde(rename = "VacationResponse")]
|
||||
VacationResponse = 9,
|
||||
#[serde(rename = "MDN")]
|
||||
Mdn = 10,
|
||||
#[serde(rename = "Quota")]
|
||||
Quota = 11,
|
||||
#[serde(rename = "SieveScript")]
|
||||
SieveScript = 12,
|
||||
None = 13,
|
||||
}
|
||||
|
||||
impl BitmapItem for TypeState {
|
||||
impl BitmapItem for DataType {
|
||||
fn max() -> u64 {
|
||||
TypeState::None as u64
|
||||
DataType::None as u64
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
!matches!(self, TypeState::None)
|
||||
!matches!(self, DataType::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for TypeState {
|
||||
impl From<u64> for DataType {
|
||||
fn from(value: u64) -> Self {
|
||||
match value {
|
||||
0 => TypeState::Email,
|
||||
1 => TypeState::EmailDelivery,
|
||||
2 => TypeState::EmailSubmission,
|
||||
3 => TypeState::Mailbox,
|
||||
4 => TypeState::Thread,
|
||||
5 => TypeState::Identity,
|
||||
0 => DataType::Email,
|
||||
1 => DataType::EmailDelivery,
|
||||
2 => DataType::EmailSubmission,
|
||||
3 => DataType::Mailbox,
|
||||
4 => DataType::Thread,
|
||||
5 => DataType::Identity,
|
||||
6 => DataType::Core,
|
||||
7 => DataType::PushSubscription,
|
||||
8 => DataType::SearchSnippet,
|
||||
9 => DataType::VacationResponse,
|
||||
10 => DataType::Mdn,
|
||||
11 => DataType::Quota,
|
||||
12 => DataType::SieveScript,
|
||||
_ => {
|
||||
debug_assert!(false, "Invalid type_state value: {}", value);
|
||||
TypeState::None
|
||||
DataType::None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TypeState> for u64 {
|
||||
fn from(type_state: TypeState) -> u64 {
|
||||
impl From<DataType> for u64 {
|
||||
fn from(type_state: DataType) -> u64 {
|
||||
type_state as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonObjectParser for TypeState {
|
||||
impl JsonObjectParser for DataType {
|
||||
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -98,18 +119,25 @@ impl JsonObjectParser for TypeState {
|
||||
}
|
||||
|
||||
match hash {
|
||||
0x006c_6961_6d45 => Ok(TypeState::Email),
|
||||
0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(TypeState::EmailDelivery),
|
||||
0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(TypeState::EmailSubmission),
|
||||
0x0078_6f62_6c69_614d => Ok(TypeState::Mailbox),
|
||||
0x6461_6572_6854 => Ok(TypeState::Thread),
|
||||
0x7974_6974_6e65_6449 => Ok(TypeState::Identity),
|
||||
0x006c_6961_6d45 => Ok(DataType::Email),
|
||||
0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(DataType::EmailDelivery),
|
||||
0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(DataType::EmailSubmission),
|
||||
0x0078_6f62_6c69_614d => Ok(DataType::Mailbox),
|
||||
0x6461_6572_6854 => Ok(DataType::Thread),
|
||||
0x7974_6974_6e65_6449 => Ok(DataType::Identity),
|
||||
0x6572_6f43 => Ok(DataType::Core),
|
||||
0x6e6f_6974_7069_7263_7362_7553_6873_7550 => Ok(DataType::PushSubscription),
|
||||
0x0074_6570_7069_6e53_6863_7261_6553 => Ok(DataType::SearchSnippet),
|
||||
0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => Ok(DataType::VacationResponse),
|
||||
0x004e_444d => Ok(DataType::Mdn),
|
||||
0x0061_746f_7551 => Ok(DataType::Quota),
|
||||
0x0074_7069_7263_5365_7665_6953 => Ok(DataType::SieveScript),
|
||||
_ => Err(parser.error_value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for TypeState {
|
||||
impl TryFrom<&str> for DataType {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
@@ -126,63 +154,84 @@ impl TryFrom<&str> for TypeState {
|
||||
}
|
||||
|
||||
match hash {
|
||||
0x006c_6961_6d45 => Ok(TypeState::Email),
|
||||
0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(TypeState::EmailDelivery),
|
||||
0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(TypeState::EmailSubmission),
|
||||
0x0078_6f62_6c69_614d => Ok(TypeState::Mailbox),
|
||||
0x6461_6572_6854 => Ok(TypeState::Thread),
|
||||
0x7974_6974_6e65_6449 => Ok(TypeState::Identity),
|
||||
0x006c_6961_6d45 => Ok(DataType::Email),
|
||||
0x0079_7265_7669_6c65_446c_6961_6d45 => Ok(DataType::EmailDelivery),
|
||||
0x006e_6f69_7373_696d_6275_536c_6961_6d45 => Ok(DataType::EmailSubmission),
|
||||
0x0078_6f62_6c69_614d => Ok(DataType::Mailbox),
|
||||
0x6461_6572_6854 => Ok(DataType::Thread),
|
||||
0x7974_6974_6e65_6449 => Ok(DataType::Identity),
|
||||
0x6572_6f43 => Ok(DataType::Core),
|
||||
0x6e6f_6974_7069_7263_7362_7553_6873_7550 => Ok(DataType::PushSubscription),
|
||||
0x0074_6570_7069_6e53_6863_7261_6553 => Ok(DataType::SearchSnippet),
|
||||
0x6573_6e6f_7073_6552_6e6f_6974_6163_6156 => Ok(DataType::VacationResponse),
|
||||
0x004e_444d => Ok(DataType::Mdn),
|
||||
0x0061_746f_7551 => Ok(DataType::Quota),
|
||||
0x0074_7069_7263_5365_7665_6953 => Ok(DataType::SieveScript),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeState {
|
||||
impl DataType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TypeState::Email => "Email",
|
||||
TypeState::EmailDelivery => "EmailDelivery",
|
||||
TypeState::EmailSubmission => "EmailSubmission",
|
||||
TypeState::Mailbox => "Mailbox",
|
||||
TypeState::Thread => "Thread",
|
||||
TypeState::Identity => "Identity",
|
||||
TypeState::None => "",
|
||||
DataType::Email => "Email",
|
||||
DataType::EmailDelivery => "EmailDelivery",
|
||||
DataType::EmailSubmission => "EmailSubmission",
|
||||
DataType::Mailbox => "Mailbox",
|
||||
DataType::Thread => "Thread",
|
||||
DataType::Identity => "Identity",
|
||||
DataType::Core => "Core",
|
||||
DataType::PushSubscription => "PushSubscription",
|
||||
DataType::SearchSnippet => "SearchSnippet",
|
||||
DataType::VacationResponse => "VacationResponse",
|
||||
DataType::Mdn => "MDN",
|
||||
DataType::Quota => "Quota",
|
||||
DataType::SieveScript => "SieveScript",
|
||||
DataType::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TypeState {
|
||||
impl Display for DataType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeInto for TypeState {
|
||||
impl SerializeInto for DataType {
|
||||
fn serialize_into(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(*self as u8);
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeFrom for TypeState {
|
||||
impl DeserializeFrom for DataType {
|
||||
fn deserialize_from(bytes: &mut std::slice::Iter<'_, u8>) -> Option<Self> {
|
||||
match *bytes.next()? {
|
||||
0 => Some(TypeState::Email),
|
||||
1 => Some(TypeState::EmailDelivery),
|
||||
2 => Some(TypeState::EmailSubmission),
|
||||
3 => Some(TypeState::Mailbox),
|
||||
4 => Some(TypeState::Thread),
|
||||
5 => Some(TypeState::Identity),
|
||||
0 => Some(DataType::Email),
|
||||
1 => Some(DataType::EmailDelivery),
|
||||
2 => Some(DataType::EmailSubmission),
|
||||
3 => Some(DataType::Mailbox),
|
||||
4 => Some(DataType::Thread),
|
||||
5 => Some(DataType::Identity),
|
||||
6 => Some(DataType::Core),
|
||||
7 => Some(DataType::PushSubscription),
|
||||
8 => Some(DataType::SearchSnippet),
|
||||
9 => Some(DataType::VacationResponse),
|
||||
10 => Some(DataType::Mdn),
|
||||
11 => Some(DataType::Quota),
|
||||
12 => Some(DataType::SieveScript),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for TypeState {
|
||||
impl<'de> serde::Deserialize<'de> for DataType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
TypeState::try_from(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid JMAP type state"))
|
||||
DataType::try_from(<&str>::deserialize(deserializer)?)
|
||||
.map_err(|_| serde::de::Error::custom("invalid JMAP data type"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
any_id::AnyId,
|
||||
blob::BlobId,
|
||||
date::UTCDate,
|
||||
id::Id,
|
||||
@@ -62,8 +63,8 @@ pub enum Value {
|
||||
pub enum SetValue {
|
||||
Value(Value),
|
||||
Patch(Vec<Value>),
|
||||
IdReference(MaybeReference<Id, String>),
|
||||
IdReferences(Vec<MaybeReference<Id, String>>),
|
||||
IdReference(MaybeReference<AnyId, String>),
|
||||
IdReferences(Vec<MaybeReference<AnyId, String>>),
|
||||
ResultReference(ResultReference),
|
||||
}
|
||||
|
||||
@@ -191,24 +192,24 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_id(self) -> Id {
|
||||
pub fn try_unwrap_id(self) -> Option<Id> {
|
||||
match self {
|
||||
Value::Id(id) => id,
|
||||
_ => panic!("Expected id"),
|
||||
Value::Id(id) => id.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_bool(self) -> bool {
|
||||
pub fn try_unwrap_bool(self) -> Option<bool> {
|
||||
match self {
|
||||
Value::Bool(b) => b,
|
||||
_ => panic!("Expected bool"),
|
||||
Value::Bool(b) => b.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_keyword(self) -> Keyword {
|
||||
pub fn try_unwrap_keyword(self) -> Option<Keyword> {
|
||||
match self {
|
||||
Value::Keyword(k) => k,
|
||||
_ => panic!("Expected keyword"),
|
||||
Value::Keyword(k) => k.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,13 +241,6 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_id(self) -> Option<Id> {
|
||||
match self {
|
||||
Value::Id(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_blob_id(self) -> Option<BlobId> {
|
||||
match self {
|
||||
Value::BlobId(b) => Some(b),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jmap"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
@@ -35,7 +35,8 @@ async-stream = "0.3.5"
|
||||
base64 = "0.21"
|
||||
p256 = { version = "0.13", features = ["ecdh"] }
|
||||
hkdf = "0.12.3"
|
||||
sha2 = "0.10.1"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]}
|
||||
tokio-tungstenite = "0.20"
|
||||
tungstenite = "0.20"
|
||||
|
||||
@@ -31,7 +31,7 @@ use hyper::{
|
||||
body::{Bytes, Frame},
|
||||
header, StatusCode,
|
||||
};
|
||||
use jmap_proto::{error::request::RequestError, types::type_state::TypeState};
|
||||
use jmap_proto::{error::request::RequestError, types::type_state::DataType};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP, LONG_SLUMBER};
|
||||
@@ -63,7 +63,7 @@ impl JMAP {
|
||||
if type_state == "*" {
|
||||
types = Bitmap::all();
|
||||
break;
|
||||
} else if let Ok(type_state) = TypeState::try_from(type_state) {
|
||||
} else if let Ok(type_state) = DataType::try_from(type_state) {
|
||||
types.insert(type_state);
|
||||
} else {
|
||||
return RequestError::invalid_parameters().into_http_response();
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::{id::Id, state::State, type_state::TypeState};
|
||||
use jmap_proto::types::{id::Id, state::State, type_state::DataType};
|
||||
use serde::Serialize;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
@@ -71,7 +71,7 @@ pub enum StateChangeType {
|
||||
pub struct StateChangeResponse {
|
||||
#[serde(rename = "@type")]
|
||||
pub type_: StateChangeType,
|
||||
pub changed: VecMap<Id, VecMap<TypeState, State>>,
|
||||
pub changed: VecMap<Id, VecMap<DataType, State>>,
|
||||
}
|
||||
|
||||
impl StateChangeResponse {
|
||||
|
||||
@@ -70,9 +70,7 @@ impl JMAP {
|
||||
match &mut method_response {
|
||||
ResponseMethod::Set(set_response) => {
|
||||
// Add created ids
|
||||
if add_created_ids {
|
||||
set_response.update_created_ids(&mut response);
|
||||
}
|
||||
set_response.update_created_ids(&mut response);
|
||||
|
||||
// Publish state changes
|
||||
if let Some(state_change) = set_response.state_change.take() {
|
||||
@@ -81,9 +79,7 @@ impl JMAP {
|
||||
}
|
||||
ResponseMethod::ImportEmail(import_response) => {
|
||||
// Add created ids
|
||||
if add_created_ids {
|
||||
import_response.update_created_ids(&mut response);
|
||||
}
|
||||
import_response.update_created_ids(&mut response);
|
||||
|
||||
// Publish state changes
|
||||
if let Some(state_change) = import_response.state_change.take() {
|
||||
@@ -96,6 +92,10 @@ impl JMAP {
|
||||
self.broadcast_state_change(state_change).await;
|
||||
}
|
||||
}
|
||||
ResponseMethod::UploadBlob(upload_response) => {
|
||||
// Add created blobIds
|
||||
upload_response.update_created_ids(&mut response);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,10 @@ impl JMAP {
|
||||
}
|
||||
}
|
||||
|
||||
if !add_created_ids {
|
||||
response.created_ids.clear();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -177,6 +181,18 @@ impl JMAP {
|
||||
));
|
||||
}
|
||||
}
|
||||
get::RequestArguments::Quota => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.quota_get(req, access_token).await?.into()
|
||||
}
|
||||
get::RequestArguments::Blob(arguments) => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_get(req.with_arguments(arguments), access_token)
|
||||
.await?
|
||||
.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Query(mut req) => match req.take_arguments() {
|
||||
query::RequestArguments::Email(arguments) => {
|
||||
@@ -212,6 +228,11 @@ impl JMAP {
|
||||
));
|
||||
}
|
||||
}
|
||||
query::RequestArguments::Quota => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.quota_query(req).await?.into()
|
||||
}
|
||||
},
|
||||
RequestMethod::Set(mut req) => match req.take_arguments() {
|
||||
set::RequestArguments::Email => {
|
||||
@@ -262,7 +283,6 @@ impl JMAP {
|
||||
|
||||
self.email_copy(req, access_token, next_call).await?.into()
|
||||
}
|
||||
RequestMethod::CopyBlob(req) => self.blob_copy(req, access_token).await?.into(),
|
||||
RequestMethod::ImportEmail(req) => {
|
||||
access_token.assert_has_access(req.account_id, Collection::Email)?;
|
||||
|
||||
@@ -284,6 +304,21 @@ impl JMAP {
|
||||
|
||||
self.sieve_script_validate(req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::CopyBlob(req) => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_copy(req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::LookupBlob(req) => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_lookup(req).await?.into()
|
||||
}
|
||||
RequestMethod::UploadBlob(req) => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
||||
self.blob_upload_many(req, access_token).await?.into()
|
||||
}
|
||||
RequestMethod::Echo(req) => req.into(),
|
||||
RequestMethod::Error(error) => return Err(error),
|
||||
})
|
||||
|
||||
+112
-23
@@ -27,7 +27,7 @@ use jmap_proto::{
|
||||
error::request::RequestError,
|
||||
request::capability::Capability,
|
||||
response::serialize::serialize_hex,
|
||||
types::{acl::Acl, collection::Collection, id::Id},
|
||||
types::{acl::Acl, collection::Collection, id::Id, type_state::DataType},
|
||||
};
|
||||
use store::ahash::AHashSet;
|
||||
use utils::{listener::ServerInstance, map::vec_map::VecMap, UnwrapFailure};
|
||||
@@ -78,9 +78,11 @@ pub enum Capabilities {
|
||||
Core(CoreCapabilities),
|
||||
Mail(MailCapabilities),
|
||||
Submission(SubmissionCapabilities),
|
||||
VacationResponse(VacationResponseCapabilities),
|
||||
WebSocket(WebSocketCapabilities),
|
||||
Sieve(SieveCapabilities),
|
||||
SieveAccount(SieveAccountCapabilities),
|
||||
SieveSession(SieveSessionCapabilities),
|
||||
Blob(BlobCapabilities),
|
||||
Empty(EmptyCapabilities),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -112,9 +114,13 @@ pub struct WebSocketCapabilities {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct SieveCapabilities {
|
||||
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: usize,
|
||||
#[serde(rename(serialize = "maxSizeScript"))]
|
||||
@@ -156,11 +162,24 @@ pub struct SubmissionCapabilities {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct VacationResponseCapabilities {}
|
||||
pub struct BlobCapabilities {
|
||||
#[serde(rename(serialize = "maxSizeBlobSet"))]
|
||||
max_size_blob_set: usize,
|
||||
#[serde(rename(serialize = "maxDataSources"))]
|
||||
max_data_sources: usize,
|
||||
#[serde(rename(serialize = "supportedTypeNames"))]
|
||||
supported_type_names: Vec<DataType>,
|
||||
#[serde(rename(serialize = "supportedDigestAlgorithms"))]
|
||||
supported_digest_algorithms: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct EmptyCapabilities {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BaseCapabilities {
|
||||
pub capabilities: VecMap<Capability, Capabilities>,
|
||||
pub session: VecMap<Capability, Capabilities>,
|
||||
pub account: VecMap<Capability, Capabilities>,
|
||||
}
|
||||
|
||||
impl JMAP {
|
||||
@@ -179,6 +198,7 @@ impl JMAP {
|
||||
.clone()
|
||||
.unwrap_or_else(|| access_token.name.clone()),
|
||||
None,
|
||||
&self.config.capabilities.account,
|
||||
);
|
||||
|
||||
// Add secondary accounts
|
||||
@@ -198,7 +218,8 @@ impl JMAP {
|
||||
.unwrap_or_else(|| Id::from(*id).to_string()),
|
||||
is_personal,
|
||||
is_readonly,
|
||||
Some(&[Capability::Core, Capability::Mail, Capability::WebSocket]),
|
||||
Some(&[Capability::Mail, Capability::Quota, Capability::Blob]),
|
||||
&self.config.capabilities.account,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,15 +229,28 @@ impl JMAP {
|
||||
|
||||
impl crate::Config {
|
||||
pub fn add_capabilites(&mut self, settings: &utils::config::Config) {
|
||||
self.capabilities.capabilities.append(
|
||||
// Add core capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Core,
|
||||
Capabilities::Core(CoreCapabilities::new(self)),
|
||||
);
|
||||
self.capabilities.capabilities.append(
|
||||
|
||||
// Add email capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Mail,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Mail,
|
||||
Capabilities::Mail(MailCapabilities::new(self)),
|
||||
);
|
||||
self.capabilities.capabilities.append(
|
||||
|
||||
// Add submission capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Submission,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Submission,
|
||||
Capabilities::Submission(SubmissionCapabilities {
|
||||
max_delayed_send: 86400 * 30,
|
||||
@@ -230,16 +264,52 @@ impl crate::Config {
|
||||
]),
|
||||
}),
|
||||
);
|
||||
self.capabilities.capabilities.append(
|
||||
|
||||
// Add vacation response capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::VacationResponse,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::VacationResponse,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
|
||||
// Add Sieve capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Sieve,
|
||||
Capabilities::Sieve(SieveCapabilities::new(self, settings)),
|
||||
Capabilities::SieveSession(SieveSessionCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Sieve,
|
||||
Capabilities::SieveAccount(SieveAccountCapabilities::new(self, settings)),
|
||||
);
|
||||
|
||||
// Add Blob capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Blob,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Blob,
|
||||
Capabilities::Blob(BlobCapabilities::new(self)),
|
||||
);
|
||||
|
||||
// Add Quota capabilities
|
||||
self.capabilities.session.append(
|
||||
Capability::Quota,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
self.capabilities.account.append(
|
||||
Capability::Quota,
|
||||
Capabilities::Empty(EmptyCapabilities::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(base_url: &str, base_capabilities: &BaseCapabilities) -> Session {
|
||||
let mut capabilities = base_capabilities.capabilities.clone();
|
||||
let mut capabilities = base_capabilities.session.clone();
|
||||
capabilities.append(
|
||||
Capability::WebSocket,
|
||||
Capabilities::WebSocket(WebSocketCapabilities::new(base_url)),
|
||||
@@ -271,6 +341,7 @@ impl Session {
|
||||
username: String,
|
||||
name: String,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.username = username;
|
||||
|
||||
@@ -286,7 +357,7 @@ impl Session {
|
||||
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, true, false).add_capabilities(capabilities, &self.capabilities),
|
||||
Account::new(name, true, false).add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,11 +368,12 @@ impl Session {
|
||||
is_personal: bool,
|
||||
is_read_only: bool,
|
||||
capabilities: Option<&[Capability]>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) {
|
||||
self.accounts.set(
|
||||
account_id,
|
||||
Account::new(name, is_personal, is_read_only)
|
||||
.add_capabilities(capabilities, &self.capabilities),
|
||||
.add_capabilities(capabilities, account_capabilities),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,17 +403,16 @@ impl Account {
|
||||
pub fn add_capabilities(
|
||||
mut self,
|
||||
capabilities: Option<&[Capability]>,
|
||||
core_capabilities: &VecMap<Capability, Capabilities>,
|
||||
account_capabilities: &VecMap<Capability, Capabilities>,
|
||||
) -> Account {
|
||||
if let Some(capabilities) = capabilities {
|
||||
for capability in capabilities {
|
||||
self.account_capabilities.append(
|
||||
*capability,
|
||||
core_capabilities.get(capability).unwrap().clone(),
|
||||
);
|
||||
if let Some(value) = account_capabilities.get(capability) {
|
||||
self.account_capabilities.append(*capability, value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.account_capabilities = core_capabilities.clone();
|
||||
self.account_capabilities = account_capabilities.clone();
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -375,7 +446,7 @@ impl WebSocketCapabilities {
|
||||
}
|
||||
}
|
||||
|
||||
impl SieveCapabilities {
|
||||
impl SieveAccountCapabilities {
|
||||
pub fn new(config: &crate::Config, settings: &utils::config::Config) -> Self {
|
||||
let mut notification_methods = Vec::new();
|
||||
|
||||
@@ -399,7 +470,7 @@ impl SieveCapabilities {
|
||||
.collect::<Vec<String>>();
|
||||
extensions.sort_unstable();
|
||||
|
||||
SieveCapabilities {
|
||||
SieveAccountCapabilities {
|
||||
max_script_name: config.sieve_max_script_name,
|
||||
max_script_size: settings
|
||||
.property("sieve.untrusted.max-script-size")
|
||||
@@ -417,6 +488,13 @@ impl SieveCapabilities {
|
||||
None
|
||||
},
|
||||
ext_lists: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SieveSessionCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
implementation: concat!("Stalwart JMAP v", env!("CARGO_PKG_VERSION"),),
|
||||
}
|
||||
}
|
||||
@@ -447,3 +525,14 @@ impl MailCapabilities {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlobCapabilities {
|
||||
pub fn new(config: &crate::Config) -> Self {
|
||||
BlobCapabilities {
|
||||
max_size_blob_set: (config.request_max_size * 3 / 4) - 512,
|
||||
max_data_sources: config.request_max_calls,
|
||||
supported_type_names: vec![DataType::Email, DataType::Thread, DataType::SieveScript],
|
||||
supported_digest_algorithms: vec!["sha", "sha-256", "sha-512"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ use std::ops::Range;
|
||||
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
types::{acl::Acl, blob::BlobId},
|
||||
types::{
|
||||
acl::Acl,
|
||||
blob::{BlobId, BlobSection},
|
||||
},
|
||||
};
|
||||
use mail_parser::{
|
||||
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
|
||||
@@ -79,23 +82,31 @@ impl JMAP {
|
||||
}
|
||||
|
||||
if let Some(section) = &blob_id.section {
|
||||
Ok(self
|
||||
.get_blob(
|
||||
&blob_id.kind,
|
||||
(section.offset_start as u32)
|
||||
..(section.offset_start.saturating_add(section.size) as u32),
|
||||
)
|
||||
.await?
|
||||
.and_then(|bytes| match Encoding::from(section.encoding) {
|
||||
Encoding::None => Some(bytes),
|
||||
Encoding::Base64 => base64_decode(&bytes),
|
||||
Encoding::QuotedPrintable => quoted_printable_decode(&bytes),
|
||||
}))
|
||||
self.get_blob_section(&blob_id.kind, section).await
|
||||
} else {
|
||||
self.get_blob(&blob_id.kind, 0..u32::MAX).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_blob_section(
|
||||
&self,
|
||||
kind: &BlobKind,
|
||||
section: &BlobSection,
|
||||
) -> Result<Option<Vec<u8>>, MethodError> {
|
||||
Ok(self
|
||||
.get_blob(
|
||||
kind,
|
||||
(section.offset_start as u32)
|
||||
..(section.offset_start.saturating_add(section.size) as u32),
|
||||
)
|
||||
.await?
|
||||
.and_then(|bytes| match Encoding::from(section.encoding) {
|
||||
Encoding::None => Some(bytes),
|
||||
Encoding::Base64 => base64_decode(&bytes),
|
||||
Encoding::QuotedPrintable => quoted_printable_decode(&bytes),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_blob(
|
||||
&self,
|
||||
kind: &BlobKind,
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
method::{
|
||||
get::{GetRequest, GetResponse},
|
||||
lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse},
|
||||
},
|
||||
object::{blob::GetArguments, Object},
|
||||
types::{
|
||||
collection::Collection,
|
||||
id::Id,
|
||||
property::{DataProperty, DigestProperty, Property},
|
||||
type_state::DataType,
|
||||
value::Value,
|
||||
MaybeUnparsable,
|
||||
},
|
||||
};
|
||||
use mail_builder::encoders::base64::base64_encode;
|
||||
use sha1::{Digest, Sha1};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use store::BlobKind;
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
|
||||
impl JMAP {
|
||||
pub async fn blob_get(
|
||||
&self,
|
||||
mut request: GetRequest<GetArguments>,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request
|
||||
.unwrap_blob_ids(self.config.get_max_objects)?
|
||||
.unwrap_or_default();
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::Data(DataProperty::Default),
|
||||
Property::Size,
|
||||
]);
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: None,
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
let range_from = request.arguments.offset.unwrap_or(0);
|
||||
let range_to = request
|
||||
.arguments
|
||||
.length
|
||||
.map(|length| range_from.saturating_add(length))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
for blob_id in ids {
|
||||
if let Some(bytes) = self.blob_download(&blob_id, access_token).await? {
|
||||
let mut blob = Object::with_capacity(properties.len());
|
||||
let bytes_range = if range_from == 0 && range_to == usize::MAX {
|
||||
&bytes[..]
|
||||
} else {
|
||||
let range_to = if range_to != usize::MAX && range_to > bytes.len() {
|
||||
blob.append(Property::IsTruncated, true);
|
||||
bytes.len()
|
||||
} else {
|
||||
range_to
|
||||
};
|
||||
let bytes_range = bytes.get(range_from..range_to).unwrap_or_default();
|
||||
bytes_range
|
||||
};
|
||||
|
||||
for property in &properties {
|
||||
let mut property = property.clone();
|
||||
let value: Value = match &property {
|
||||
Property::Id => Value::BlobId(blob_id.clone()),
|
||||
Property::Size => bytes.len().into(),
|
||||
Property::Digest(digest) => match digest {
|
||||
DigestProperty::Sha => {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
DigestProperty::Sha256 => {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
DigestProperty::Sha512 => {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(bytes_range);
|
||||
String::from_utf8(
|
||||
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
Property::Data(data) => match data {
|
||||
DataProperty::AsText => match std::str::from_utf8(bytes_range) {
|
||||
Ok(text) => text.to_string().into(),
|
||||
Err(_) => {
|
||||
blob.append(Property::IsEncodingProblem, true);
|
||||
Value::Null
|
||||
}
|
||||
},
|
||||
DataProperty::AsBase64 => {
|
||||
String::from_utf8(base64_encode(bytes_range).unwrap_or_default())
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
DataProperty::Default => match std::str::from_utf8(bytes_range) {
|
||||
Ok(text) => {
|
||||
property = Property::Data(DataProperty::AsText);
|
||||
text.to_string().into()
|
||||
}
|
||||
Err(_) => {
|
||||
property = Property::Data(DataProperty::AsBase64);
|
||||
blob.append(Property::IsEncodingProblem, true);
|
||||
String::from_utf8(
|
||||
base64_encode(bytes_range).unwrap_or_default(),
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => Value::Null,
|
||||
};
|
||||
blob.append(property, value);
|
||||
}
|
||||
|
||||
// Add result to response
|
||||
response.list.push(blob);
|
||||
} else {
|
||||
response.not_found.push(blob_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn blob_lookup(
|
||||
&self,
|
||||
request: BlobLookupRequest,
|
||||
) -> Result<BlobLookupResponse, MethodError> {
|
||||
let mut include_email = false;
|
||||
let mut include_mailbox = false;
|
||||
let mut include_thread = false;
|
||||
|
||||
let type_names = request
|
||||
.type_names
|
||||
.into_iter()
|
||||
.map(|tn| match tn {
|
||||
MaybeUnparsable::Value(value) => {
|
||||
match &value {
|
||||
DataType::Email => {
|
||||
include_email = true;
|
||||
}
|
||||
DataType::Mailbox => {
|
||||
include_mailbox = true;
|
||||
}
|
||||
DataType::Thread => {
|
||||
include_thread = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
MaybeUnparsable::ParseError(_) => Err(MethodError::UnknownDataType),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let req_account_id = request.account_id.document_id();
|
||||
let mut response = BlobLookupResponse {
|
||||
account_id: request.account_id,
|
||||
list: Vec::with_capacity(request.ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for id in request.ids {
|
||||
match id {
|
||||
MaybeUnparsable::Value(id) => {
|
||||
let mut matched_ids = VecMap::new();
|
||||
|
||||
match &id.kind {
|
||||
BlobKind::Linked {
|
||||
account_id,
|
||||
collection,
|
||||
document_id,
|
||||
} if *account_id == req_account_id => {
|
||||
if *account_id != req_account_id {
|
||||
response.not_found.push(MaybeUnparsable::Value(id));
|
||||
continue;
|
||||
}
|
||||
|
||||
match DataType::try_from(Collection::from(*collection)) {
|
||||
Ok(data_type) if type_names.contains(&data_type) => {
|
||||
matched_ids.append(data_type, vec![Id::from(*document_id)]);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
BlobKind::LinkedMaildir {
|
||||
account_id,
|
||||
document_id,
|
||||
} if *account_id == req_account_id => {
|
||||
if include_email || include_thread {
|
||||
if let Some(thread_id) = self
|
||||
.get_property::<u32>(
|
||||
req_account_id,
|
||||
Collection::Email,
|
||||
*document_id,
|
||||
Property::ThreadId,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if include_email {
|
||||
matched_ids.append(
|
||||
DataType::Email,
|
||||
vec![Id::from_parts(thread_id, *document_id)],
|
||||
);
|
||||
}
|
||||
if include_thread {
|
||||
matched_ids
|
||||
.append(DataType::Thread, vec![Id::from(thread_id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if include_mailbox {
|
||||
if let Some(mailboxes) = self
|
||||
.get_property::<Vec<u32>>(
|
||||
req_account_id,
|
||||
Collection::Email,
|
||||
*document_id,
|
||||
Property::MailboxIds,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
matched_ids.append(
|
||||
DataType::Mailbox,
|
||||
mailboxes.into_iter().map(Id::from).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
BlobKind::Temporary { account_id, .. } if *account_id == req_account_id => {
|
||||
}
|
||||
_ => {
|
||||
response.not_found.push(MaybeUnparsable::Value(id));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
response.list.push(BlobInfo { id, matched_ids });
|
||||
}
|
||||
_ => response.not_found.push(id),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ use jmap_proto::types::{blob::BlobId, id::Id};
|
||||
|
||||
pub mod copy;
|
||||
pub mod download;
|
||||
pub mod get;
|
||||
pub mod upload;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use jmap_proto::{
|
||||
error::{method::MethodError, request::RequestError},
|
||||
error::{method::MethodError, request::RequestError, set::SetError},
|
||||
method::upload::{
|
||||
BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject,
|
||||
},
|
||||
request::reference::MaybeReference,
|
||||
types::{blob::BlobId, id::Id},
|
||||
};
|
||||
use store::BlobKind;
|
||||
@@ -38,6 +42,174 @@ pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(true);
|
||||
|
||||
impl JMAP {
|
||||
pub async fn blob_upload_many(
|
||||
&self,
|
||||
request: BlobUploadRequest,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<BlobUploadResponse, MethodError> {
|
||||
let mut response = BlobUploadResponse {
|
||||
account_id: request.account_id,
|
||||
created: Default::default(),
|
||||
not_created: Default::default(),
|
||||
};
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
if request.create.len() > self.config.set_max_objects {
|
||||
return Err(MethodError::RequestTooLarge);
|
||||
}
|
||||
|
||||
'outer: for (create_id, upload_object) in request.create {
|
||||
let mut data = Vec::new();
|
||||
|
||||
for data_source in upload_object.data {
|
||||
let bytes = match data_source {
|
||||
DataSourceObject::Id { id, length, offset } => {
|
||||
let id = match id {
|
||||
MaybeReference::Value(id) => id,
|
||||
MaybeReference::Reference(reference) => {
|
||||
if let Some(obj) = response.created.get(&reference) {
|
||||
obj.id.clone()
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::not_found().with_description(format!(
|
||||
"Id reference {reference:?} not found."
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !self.has_access_blob(&id, access_token).await? {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::forbidden().with_description(format!(
|
||||
"You do not have access to blobId {id}."
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
let offset = offset.unwrap_or(0) as u32;
|
||||
let length = length
|
||||
.map(|length| (length as u32).saturating_add(offset))
|
||||
.unwrap_or(u32::MAX);
|
||||
let bytes = if let Some(section) = &id.section {
|
||||
self.get_blob_section(&id.kind, section)
|
||||
.await?
|
||||
.map(|bytes| {
|
||||
if offset == 0 && length == u32::MAX {
|
||||
bytes
|
||||
} else {
|
||||
bytes
|
||||
.get(
|
||||
offset as usize
|
||||
..std::cmp::min(length as usize, bytes.len()),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.to_vec()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.get_blob(&id.kind, offset..length).await?
|
||||
};
|
||||
if let Some(bytes) = bytes {
|
||||
bytes
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::blob_not_found()
|
||||
.with_description(format!("BlobId {id} not found.")),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
DataSourceObject::Value(bytes) => bytes,
|
||||
};
|
||||
|
||||
if bytes.len() + data.len() < self.config.upload_max_size {
|
||||
data.extend(bytes);
|
||||
} else {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::too_large().with_description(format!(
|
||||
"Upload size exceeds maximum of {} bytes.",
|
||||
self.config.upload_max_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("Must specify at least one valid DataSourceObject."),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// Enforce quota
|
||||
let (total_files, total_bytes) = self
|
||||
.store
|
||||
.get_tmp_blob_usage(account_id, self.config.upload_tmp_ttl)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
context = "blob_store",
|
||||
account_id = account_id,
|
||||
error = ?err,
|
||||
"Failed to obtain blob quota");
|
||||
MethodError::ServerPartialFail
|
||||
})?;
|
||||
|
||||
if ((self.config.upload_tmp_quota_size > 0
|
||||
&& total_bytes + data.len() > self.config.upload_tmp_quota_size)
|
||||
|| (self.config.upload_tmp_quota_amount > 0
|
||||
&& total_files + 1 > self.config.upload_tmp_quota_amount))
|
||||
&& !access_token.is_super_user()
|
||||
{
|
||||
response.not_created.append(
|
||||
create_id,
|
||||
SetError::over_quota().with_description(format!(
|
||||
"You have exceeded the blob upload quota of {} files or {} bytes.",
|
||||
self.config.upload_tmp_quota_amount, self.config.upload_tmp_quota_size
|
||||
)),
|
||||
);
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
// Write blob
|
||||
let blob_id = BlobId::temporary(account_id);
|
||||
match self.store.put_blob(&blob_id.kind, &data).await {
|
||||
Ok(_) => {
|
||||
response.created.insert(
|
||||
create_id,
|
||||
BlobUploadResponseObject {
|
||||
id: blob_id,
|
||||
type_: upload_object.type_,
|
||||
size: data.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(event = "error",
|
||||
context = "blob_store",
|
||||
account_id = account_id,
|
||||
blob_id = ?blob_id,
|
||||
size = data.len(),
|
||||
error = ?err,
|
||||
"Failed to upload blob");
|
||||
return Err(MethodError::ServerPartialFail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn blob_upload(
|
||||
&self,
|
||||
account_id: Id,
|
||||
|
||||
@@ -62,6 +62,11 @@ impl JMAP {
|
||||
|
||||
Collection::EmailSubmission
|
||||
}
|
||||
RequestArguments::Quota => {
|
||||
access_token.assert_is_member(request.account_id)?;
|
||||
|
||||
return Err(MethodError::CannotCalculateChanges);
|
||||
}
|
||||
};
|
||||
|
||||
let max_changes = if self.config.changes_max_results > 0
|
||||
@@ -73,7 +78,7 @@ impl JMAP {
|
||||
};
|
||||
let mut response = ChangesResponse {
|
||||
account_id: request.account_id,
|
||||
old_state: State::Initial,
|
||||
old_state: request.since_state.clone(),
|
||||
new_state: State::Initial,
|
||||
has_more_changes: false,
|
||||
created: vec![],
|
||||
|
||||
@@ -51,6 +51,7 @@ impl JMAP {
|
||||
query::RequestArguments::EmailSubmission => {
|
||||
changes::RequestArguments::EmailSubmission
|
||||
}
|
||||
query::RequestArguments::Quota => changes::RequestArguments::Quota,
|
||||
_ => return Err(MethodError::UnknownMethod("Unknown method".to_string())),
|
||||
},
|
||||
},
|
||||
@@ -97,6 +98,7 @@ impl JMAP {
|
||||
query::RequestArguments::EmailSubmission => {
|
||||
self.email_submission_query(query).await?
|
||||
}
|
||||
query::RequestArguments::Quota => self.quota_query(query).await?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ use jmap_proto::{
|
||||
keyword::Keyword,
|
||||
property::Property,
|
||||
state::{State, StateChange},
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
value::{MaybePatchValue, Value},
|
||||
},
|
||||
};
|
||||
@@ -139,38 +139,41 @@ impl JMAP {
|
||||
(Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => {
|
||||
mailboxes = ids
|
||||
.into_iter()
|
||||
.map(|id| id.unwrap_id().document_id())
|
||||
.filter_map(|id| id.try_unwrap_id()?.document_id().into())
|
||||
.collect();
|
||||
}
|
||||
|
||||
(Property::MailboxIds, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
let document_id = patch.next().unwrap().unwrap_id().document_id();
|
||||
if patch.next().unwrap().unwrap_bool() {
|
||||
if !mailboxes.contains(&document_id) {
|
||||
mailboxes.push(document_id);
|
||||
if let Some(id) = patch.next().unwrap().try_unwrap_id() {
|
||||
let document_id = id.document_id();
|
||||
if patch.next().unwrap().try_unwrap_bool().unwrap_or_default() {
|
||||
if !mailboxes.contains(&document_id) {
|
||||
mailboxes.push(document_id);
|
||||
}
|
||||
} else {
|
||||
mailboxes.retain(|id| id != &document_id);
|
||||
}
|
||||
} else {
|
||||
mailboxes.retain(|id| id != &document_id);
|
||||
}
|
||||
}
|
||||
|
||||
(Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => {
|
||||
keywords = keywords_
|
||||
.into_iter()
|
||||
.map(|keyword| keyword.unwrap_keyword())
|
||||
.filter_map(|keyword| keyword.try_unwrap_keyword())
|
||||
.collect();
|
||||
}
|
||||
|
||||
(Property::Keywords, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
let keyword = patch.next().unwrap().unwrap_keyword();
|
||||
if patch.next().unwrap().unwrap_bool() {
|
||||
if !keywords.contains(&keyword) {
|
||||
keywords.push(keyword);
|
||||
if let Some(keyword) = patch.next().unwrap().try_unwrap_keyword() {
|
||||
if patch.next().unwrap().try_unwrap_bool().unwrap_or_default() {
|
||||
if !keywords.contains(&keyword) {
|
||||
keywords.push(keyword);
|
||||
}
|
||||
} else {
|
||||
keywords.retain(|k| k != &keyword);
|
||||
}
|
||||
} else {
|
||||
keywords.retain(|k| k != &keyword);
|
||||
}
|
||||
}
|
||||
(Property::ReceivedAt, MaybePatchValue::Value(Value::Date(value))) => {
|
||||
@@ -252,9 +255,9 @@ impl JMAP {
|
||||
response.new_state = self.get_state(account_id, Collection::Email).await?;
|
||||
if let State::Exact(change_id) = &response.new_state {
|
||||
response.state_change = StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, *change_id)
|
||||
.with_change(TypeState::Mailbox, *change_id)
|
||||
.with_change(TypeState::Thread, *change_id)
|
||||
.with_change(DataType::Email, *change_id)
|
||||
.with_change(DataType::Mailbox, *change_id)
|
||||
.with_change(DataType::Thread, *change_id)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ impl JMAP {
|
||||
'outer: for id in ids {
|
||||
// Obtain the email object
|
||||
if !message_ids.contains(id.document_id()) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
let mut values = match self
|
||||
@@ -158,7 +158,7 @@ impl JMAP {
|
||||
{
|
||||
Some(values) => values,
|
||||
None => {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -185,7 +185,7 @@ impl JMAP {
|
||||
document_id = id.document_id(),
|
||||
blob_id = ?blob_id,
|
||||
"Blob not found");
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@@ -244,7 +244,7 @@ impl JMAP {
|
||||
collection = ?Collection::Email,
|
||||
document_id = id.document_id(),
|
||||
"Mailbox property not found");
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +272,7 @@ impl JMAP {
|
||||
collection = ?Collection::Email,
|
||||
document_id = id.document_id(),
|
||||
"Keywords property not found");
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use jmap_proto::{
|
||||
id::Id,
|
||||
property::Property,
|
||||
state::{State, StateChange},
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
},
|
||||
};
|
||||
use mail_parser::MessageParser;
|
||||
@@ -172,9 +172,9 @@ impl JMAP {
|
||||
response.new_state = self.get_state(account_id, Collection::Email).await?;
|
||||
if let State::Exact(change_id) = &response.new_state {
|
||||
response.state_change = StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, *change_id)
|
||||
.with_change(TypeState::Mailbox, *change_id)
|
||||
.with_change(TypeState::Thread, *change_id)
|
||||
.with_change(DataType::Email, *change_id)
|
||||
.with_change(DataType::Mailbox, *change_id)
|
||||
.with_change(DataType::Thread, *change_id)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use jmap_proto::{
|
||||
keyword::Keyword,
|
||||
property::Property,
|
||||
state::{State, StateChange},
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
value::{MaybePatchValue, SetValue, Value},
|
||||
},
|
||||
};
|
||||
@@ -161,38 +161,41 @@ impl JMAP {
|
||||
(Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => {
|
||||
mailboxes = ids
|
||||
.into_iter()
|
||||
.map(|id| id.unwrap_id().document_id())
|
||||
.filter_map(|id| id.try_unwrap_id()?.document_id().into())
|
||||
.collect();
|
||||
}
|
||||
|
||||
(Property::MailboxIds, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
let document_id = patch.next().unwrap().unwrap_id().document_id();
|
||||
if patch.next().unwrap().unwrap_bool() {
|
||||
if !mailboxes.contains(&document_id) {
|
||||
mailboxes.push(document_id);
|
||||
if let Some(document_id) = patch.next().unwrap().try_unwrap_id() {
|
||||
let document_id = document_id.document_id();
|
||||
if patch.next().unwrap().try_unwrap_bool().unwrap_or_default() {
|
||||
if !mailboxes.contains(&document_id) {
|
||||
mailboxes.push(document_id);
|
||||
}
|
||||
} else {
|
||||
mailboxes.retain(|id| id != &document_id);
|
||||
}
|
||||
} else {
|
||||
mailboxes.retain(|id| id != &document_id);
|
||||
}
|
||||
}
|
||||
|
||||
(Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => {
|
||||
keywords = keywords_
|
||||
.into_iter()
|
||||
.map(|keyword| keyword.unwrap_keyword())
|
||||
.filter_map(|keyword| keyword.try_unwrap_keyword())
|
||||
.collect();
|
||||
}
|
||||
|
||||
(Property::Keywords, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
let keyword = patch.next().unwrap().unwrap_keyword();
|
||||
if patch.next().unwrap().unwrap_bool() {
|
||||
if !keywords.contains(&keyword) {
|
||||
keywords.push(keyword);
|
||||
if let Some(keyword) = patch.next().unwrap().try_unwrap_keyword() {
|
||||
if patch.next().unwrap().try_unwrap_bool().unwrap_or_default() {
|
||||
if !keywords.contains(&keyword) {
|
||||
keywords.push(keyword);
|
||||
}
|
||||
} else {
|
||||
keywords.retain(|k| k != &keyword);
|
||||
}
|
||||
} else {
|
||||
keywords.retain(|k| k != &keyword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,31 +809,35 @@ impl JMAP {
|
||||
(Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => {
|
||||
mailboxes.set(
|
||||
ids.into_iter()
|
||||
.map(|id| id.unwrap_id().document_id())
|
||||
.filter_map(|id| id.try_unwrap_id()?.document_id().into())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
(Property::MailboxIds, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
mailboxes.update(
|
||||
patch.next().unwrap().unwrap_id().document_id(),
|
||||
patch.next().unwrap().unwrap_bool(),
|
||||
);
|
||||
if let Some(id) = patch.next().unwrap().try_unwrap_id() {
|
||||
mailboxes.update(
|
||||
id.document_id(),
|
||||
patch.next().unwrap().try_unwrap_bool().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
(Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => {
|
||||
keywords.set(
|
||||
keywords_
|
||||
.into_iter()
|
||||
.map(|keyword| keyword.unwrap_keyword())
|
||||
.filter_map(|keyword| keyword.try_unwrap_keyword())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
(Property::Keywords, MaybePatchValue::Patch(patch)) => {
|
||||
let mut patch = patch.into_iter();
|
||||
keywords.update(
|
||||
patch.next().unwrap().unwrap_keyword(),
|
||||
patch.next().unwrap().unwrap_bool(),
|
||||
);
|
||||
if let Some(keyword) = patch.next().unwrap().try_unwrap_keyword() {
|
||||
keywords.update(
|
||||
keyword,
|
||||
patch.next().unwrap().try_unwrap_bool().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
(property, _) => {
|
||||
response.invalid_property_update(id, property);
|
||||
@@ -1031,9 +1038,9 @@ impl JMAP {
|
||||
};
|
||||
if let State::Exact(change_id) = &new_state {
|
||||
response.state_change = StateChange::new(account_id)
|
||||
.with_change(TypeState::Email, *change_id)
|
||||
.with_change(TypeState::Mailbox, *change_id)
|
||||
.with_change(TypeState::Thread, *change_id)
|
||||
.with_change(DataType::Email, *change_id)
|
||||
.with_change(DataType::Mailbox, *change_id)
|
||||
.with_change(DataType::Thread, *change_id)
|
||||
.into();
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ impl JMAP {
|
||||
// Obtain the identity object
|
||||
let document_id = id.document_id();
|
||||
if !identity_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
let mut push = if let Some(push) = self
|
||||
@@ -88,7 +88,7 @@ impl JMAP {
|
||||
{
|
||||
push
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
let mut result = Object::with_capacity(properties.len());
|
||||
|
||||
@@ -71,6 +71,7 @@ pub mod identity;
|
||||
pub mod mailbox;
|
||||
pub mod principal;
|
||||
pub mod push;
|
||||
pub mod quota;
|
||||
pub mod services;
|
||||
pub mod sieve;
|
||||
pub mod submission;
|
||||
|
||||
@@ -96,7 +96,7 @@ impl JMAP {
|
||||
// Obtain the mailbox object
|
||||
let document_id = id.document_id();
|
||||
if !mailbox_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ impl JMAP {
|
||||
{
|
||||
Some(values) => values,
|
||||
None => {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use jmap_proto::{
|
||||
id::Id,
|
||||
property::Property,
|
||||
state::StateChange,
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
value::{MaybePatchValue, SetValue, Value},
|
||||
},
|
||||
};
|
||||
@@ -245,11 +245,11 @@ impl JMAP {
|
||||
// Write changes
|
||||
if !changes.is_empty() {
|
||||
let state_change =
|
||||
StateChange::new(account_id).with_change(TypeState::Mailbox, changes.change_id);
|
||||
StateChange::new(account_id).with_change(DataType::Mailbox, changes.change_id);
|
||||
ctx.response.state_change = if did_remove_emails {
|
||||
state_change
|
||||
.with_change(TypeState::Email, changes.change_id)
|
||||
.with_change(TypeState::Thread, changes.change_id)
|
||||
.with_change(DataType::Email, changes.change_id)
|
||||
.with_change(DataType::Thread, changes.change_id)
|
||||
} else {
|
||||
state_change
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ impl JMAP {
|
||||
let name = if let Some(name) = self.get_account_name(id.document_id()).await? {
|
||||
name
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ impl JMAP {
|
||||
{
|
||||
principal
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
method::get::{GetRequest, GetResponse, RequestArguments},
|
||||
object::Object,
|
||||
types::{collection::Collection, property::Property, type_state::TypeState, value::Value},
|
||||
types::{collection::Collection, property::Property, type_state::DataType, value::Value},
|
||||
};
|
||||
use store::{write::now, BitmapKey, ValueKey};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
@@ -74,7 +74,7 @@ impl JMAP {
|
||||
// Obtain the push subscription object
|
||||
let document_id = id.document_id();
|
||||
if !push_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
let mut push = if let Some(push) = self
|
||||
@@ -88,7 +88,7 @@ impl JMAP {
|
||||
{
|
||||
push
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
let mut result = Object::with_capacity(properties.len());
|
||||
@@ -215,7 +215,7 @@ impl JMAP {
|
||||
for type_state in value {
|
||||
if let Some(type_state) = type_state
|
||||
.as_string()
|
||||
.and_then(|type_state| TypeState::try_from(type_state).ok())
|
||||
.and_then(|type_state| DataType::try_from(type_state).ok())
|
||||
{
|
||||
type_states.insert(type_state);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub mod set;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use jmap_proto::types::{id::Id, state::StateChange, type_state::TypeState};
|
||||
use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -47,7 +47,7 @@ pub struct PushSubscription {
|
||||
pub id: u32,
|
||||
pub url: String,
|
||||
pub expires: u64,
|
||||
pub types: Bitmap<TypeState>,
|
||||
pub types: Bitmap<DataType>,
|
||||
pub keys: Option<EncryptionKeys>,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use jmap_proto::{
|
||||
collection::Collection,
|
||||
date::UTCDate,
|
||||
property::Property,
|
||||
type_state::TypeState,
|
||||
type_state::DataType,
|
||||
value::{MaybePatchValue, Value},
|
||||
},
|
||||
};
|
||||
@@ -99,12 +99,13 @@ impl JMAP {
|
||||
}
|
||||
|
||||
// Add expiry time if missing
|
||||
if !push.properties.contains_key(&Property::Expires) {
|
||||
push.append(
|
||||
Property::Expires,
|
||||
Value::Date(UTCDate::from_timestamp(now() as i64 + EXPIRES_MAX)),
|
||||
)
|
||||
}
|
||||
let expires = if let Some(expires) = push.properties.get(&Property::Expires) {
|
||||
expires.clone()
|
||||
} else {
|
||||
let expires = Value::Date(UTCDate::from_timestamp(now() as i64 + EXPIRES_MAX));
|
||||
push.append(Property::Expires, expires.clone());
|
||||
expires
|
||||
};
|
||||
|
||||
// Generate random verification code
|
||||
push.append(
|
||||
@@ -130,7 +131,13 @@ impl JMAP {
|
||||
.value(Property::Value, push, F_VALUE);
|
||||
push_ids.insert(document_id);
|
||||
self.write_batch(batch).await?;
|
||||
response.created(id, document_id);
|
||||
response.created.insert(
|
||||
id,
|
||||
Object::with_capacity(1)
|
||||
.with_property(Property::Id, Value::Id(document_id.into()))
|
||||
.with_property(Property::Keys, Value::Null)
|
||||
.with_property(Property::Expires, expires),
|
||||
);
|
||||
}
|
||||
|
||||
// Process updates
|
||||
@@ -258,7 +265,7 @@ fn validate_push_value(
|
||||
if value.iter().all(|value| {
|
||||
value
|
||||
.as_string()
|
||||
.and_then(|value| TypeState::try_from(value).ok())
|
||||
.and_then(|value| DataType::try_from(value).ok())
|
||||
.is_some()
|
||||
}) =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
method::get::{GetRequest, GetResponse, RequestArguments},
|
||||
object::Object,
|
||||
types::{id::Id, property::Property, state::State, type_state::DataType, value::Value},
|
||||
};
|
||||
|
||||
use crate::{auth::AccessToken, JMAP};
|
||||
|
||||
impl JMAP {
|
||||
pub async fn quota_get(
|
||||
&self,
|
||||
mut request: GetRequest<RequestArguments>,
|
||||
access_token: &AccessToken,
|
||||
) -> Result<GetResponse, MethodError> {
|
||||
let ids = request.unwrap_ids(self.config.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
Property::Id,
|
||||
Property::ResourceType,
|
||||
Property::Used,
|
||||
Property::WarnLimit,
|
||||
Property::SoftLimit,
|
||||
Property::HardLimit,
|
||||
Property::Scope,
|
||||
Property::Name,
|
||||
Property::Description,
|
||||
Property::Types,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let quota_ids = [0u32];
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
vec![Id::new(0)]
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: State::Initial.into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: vec![],
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the sieve script object
|
||||
let document_id = id.document_id();
|
||||
if !quota_ids.contains(&document_id) {
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut result = Object::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
let value = match property {
|
||||
Property::Id => Value::Id(id),
|
||||
Property::ResourceType => "octets".to_string().into(),
|
||||
Property::Used => (self.get_used_quota(account_id).await? as u64).into(),
|
||||
Property::HardLimit => access_token.quota.into(),
|
||||
Property::Scope => "account".to_string().into(),
|
||||
Property::Name => access_token.name.clone().into(),
|
||||
Property::Description => access_token.description.clone().into(),
|
||||
Property::Types => vec![
|
||||
Value::Text(DataType::Email.to_string()),
|
||||
Value::Text(DataType::SieveScript.to_string()),
|
||||
]
|
||||
.into(),
|
||||
|
||||
_ => Value::Null,
|
||||
};
|
||||
result.append(property.clone(), value);
|
||||
}
|
||||
response.list.push(result);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
method::query::{QueryRequest, QueryResponse, RequestArguments},
|
||||
types::{id::Id, state::State},
|
||||
};
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
impl JMAP {
|
||||
pub async fn quota_query(
|
||||
&self,
|
||||
request: QueryRequest<RequestArguments>,
|
||||
) -> Result<QueryResponse, MethodError> {
|
||||
Ok(QueryResponse {
|
||||
account_id: request.account_id,
|
||||
query_state: State::Initial,
|
||||
can_calculate_changes: false,
|
||||
position: 0,
|
||||
ids: vec![Id::new(0)],
|
||||
total: Some(1),
|
||||
limit: None,
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Name(value) => filters.push(query::Filter::has_text(
|
||||
Property::Name,
|
||||
&value,
|
||||
Language::None,
|
||||
)),
|
||||
Filter::Type(value) => filters.push(query::Filter::has_text(
|
||||
Property::Type,
|
||||
&value,
|
||||
Language::None,
|
||||
)),
|
||||
Filter::Scope(value) => filters.push(query::Filter::has_text(
|
||||
Property::Scope,
|
||||
&value,
|
||||
Language::None,
|
||||
)),
|
||||
Filter::ResourceType(value) => filters.push(query::Filter::has_text(
|
||||
Property::ResourceType,
|
||||
&value,
|
||||
Language::None,
|
||||
)),
|
||||
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
|
||||
filters.push(cond.into());
|
||||
}
|
||||
other => return Err(MethodError::UnsupportedFilter(other.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
let result_set = self
|
||||
.filter(account_id, Collection::Quota, filters)
|
||||
.await?;
|
||||
|
||||
let (response, paginate) = self.build_query_response(&result_set, &request).await?;
|
||||
|
||||
if let Some(paginate) = paginate {
|
||||
// Parse sort criteria
|
||||
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
|
||||
for comparator in request
|
||||
.sort
|
||||
.and_then(|s| if !s.is_empty() { s.into() } else { None })
|
||||
.unwrap_or_else(|| vec![Comparator::descending(SortProperty::Name)])
|
||||
{
|
||||
comparators.push(match comparator.property {
|
||||
SortProperty::Name => {
|
||||
query::Comparator::field(Property::Name, comparator.is_ascending)
|
||||
}
|
||||
SortProperty::Used => {
|
||||
query::Comparator::field(Property::Used, comparator.is_ascending)
|
||||
}
|
||||
other => return Err(MethodError::UnsupportedSort(other.to_string())),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort results
|
||||
self.sort(result_set, comparators, paginate, response).await
|
||||
} else {
|
||||
Ok(response)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use jmap_proto::types::{state::StateChange, type_state::TypeState};
|
||||
use jmap_proto::types::{state::StateChange, type_state::DataType};
|
||||
use mail_parser::MessageParser;
|
||||
use store::ahash::AHashMap;
|
||||
use utils::ipc::{DeliveryResult, IngestMessage};
|
||||
@@ -122,10 +122,10 @@ impl JMAP {
|
||||
if ingested_message.change_id != u64::MAX {
|
||||
self.broadcast_state_change(
|
||||
StateChange::new(uid)
|
||||
.with_change(TypeState::EmailDelivery, ingested_message.change_id)
|
||||
.with_change(TypeState::Email, ingested_message.change_id)
|
||||
.with_change(TypeState::Mailbox, ingested_message.change_id)
|
||||
.with_change(TypeState::Thread, ingested_message.change_id),
|
||||
.with_change(DataType::EmailDelivery, ingested_message.change_id)
|
||||
.with_change(DataType::Email, ingested_message.change_id)
|
||||
.with_change(DataType::Mailbox, ingested_message.change_id)
|
||||
.with_change(DataType::Thread, ingested_message.change_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
use jmap_proto::types::{id::Id, state::StateChange, type_state::TypeState};
|
||||
use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType};
|
||||
use store::ahash::AHashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use utils::{config::Config, map::bitmap::Bitmap};
|
||||
@@ -43,7 +43,7 @@ pub enum Event {
|
||||
Subscribe {
|
||||
id: u32,
|
||||
account_id: u32,
|
||||
types: Bitmap<TypeState>,
|
||||
types: Bitmap<DataType>,
|
||||
tx: mpsc::Sender<StateChange>,
|
||||
},
|
||||
Publish {
|
||||
@@ -61,7 +61,7 @@ pub enum Event {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Subscriber {
|
||||
types: Bitmap<TypeState>,
|
||||
types: Bitmap<DataType>,
|
||||
subscription: SubscriberType,
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn spawn_state_manager(
|
||||
tokio::spawn(async move {
|
||||
let mut subscribers: AHashMap<u32, AHashMap<u32, Subscriber>> = AHashMap::default();
|
||||
let mut shared_accounts: AHashMap<u32, Vec<u32>> = AHashMap::default();
|
||||
let mut shared_accounts_map: AHashMap<u32, AHashMap<u32, Bitmap<TypeState>>> =
|
||||
let mut shared_accounts_map: AHashMap<u32, AHashMap<u32, Bitmap<DataType>>> =
|
||||
AHashMap::default();
|
||||
|
||||
let mut last_purge = Instant::now();
|
||||
@@ -154,13 +154,13 @@ pub fn spawn_state_manager(
|
||||
.insert(account_id, Bitmap::all());
|
||||
}
|
||||
for (shared_account_id, shared_collections) in acl.access_to.iter() {
|
||||
let mut types: Bitmap<TypeState> = Bitmap::new();
|
||||
let mut types: Bitmap<DataType> = Bitmap::new();
|
||||
for collection in *shared_collections {
|
||||
if let Ok(type_state) = TypeState::try_from(collection) {
|
||||
if let Ok(type_state) = DataType::try_from(collection) {
|
||||
types.insert(type_state);
|
||||
if type_state == TypeState::Email {
|
||||
types.insert(TypeState::EmailDelivery);
|
||||
types.insert(TypeState::Thread);
|
||||
if type_state == DataType::Email {
|
||||
types.insert(DataType::EmailDelivery);
|
||||
types.insert(DataType::Thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,7 @@ impl JMAP {
|
||||
&self,
|
||||
id: u32,
|
||||
account_id: u32,
|
||||
types: Bitmap<TypeState>,
|
||||
types: Bitmap<DataType>,
|
||||
) -> Option<mpsc::Receiver<StateChange>> {
|
||||
let (change_tx, change_rx) = mpsc::channel::<StateChange>(IPC_CHANNEL_BUFFER);
|
||||
let state_tx = self.state_tx.clone();
|
||||
|
||||
@@ -72,7 +72,7 @@ impl JMAP {
|
||||
// Obtain the sieve script object
|
||||
let document_id = id.document_id();
|
||||
if !push_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
let mut push = if let Some(push) = self
|
||||
@@ -86,7 +86,7 @@ impl JMAP {
|
||||
{
|
||||
push
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
let mut result = Object::with_capacity(properties.len());
|
||||
|
||||
@@ -263,8 +263,8 @@ impl JMAP {
|
||||
match id {
|
||||
MaybeReference::Value(id) => id.document_id(),
|
||||
MaybeReference::Reference(id_ref) => match ctx.response.get_id(&id_ref) {
|
||||
Some(id) => id.document_id(),
|
||||
None => return Ok(ctx.response),
|
||||
Some(Value::Id(id)) => id.document_id(),
|
||||
_ => return Ok(ctx.response),
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -78,7 +78,7 @@ impl JMAP {
|
||||
// Obtain the email_submission object
|
||||
let document_id = id.document_id();
|
||||
if !email_submission_ids.contains(document_id) {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
}
|
||||
let mut push = if let Some(push) = self
|
||||
@@ -92,7 +92,7 @@ impl JMAP {
|
||||
{
|
||||
push
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ impl JMAP {
|
||||
}
|
||||
response.list.push(thread);
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
response.not_found.push(id.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse, RequestArguments},
|
||||
object::Object,
|
||||
request::reference::MaybeReference,
|
||||
types::{collection::Collection, id::Id, property::Property, value::Value},
|
||||
types::{any_id::AnyId, collection::Collection, id::Id, property::Property, value::Value},
|
||||
};
|
||||
use store::query::Filter;
|
||||
|
||||
@@ -60,10 +60,14 @@ impl JMAP {
|
||||
let do_get = if let Some(MaybeReference::Value(ids)) = request.ids {
|
||||
let mut do_get = false;
|
||||
for id in ids {
|
||||
if id.is_singleton() {
|
||||
do_get = true;
|
||||
} else {
|
||||
response.not_found.push(id);
|
||||
match id.try_unwrap() {
|
||||
Some(AnyId::Id(id)) if id.is_singleton() => {
|
||||
do_get = true;
|
||||
}
|
||||
Some(id) => {
|
||||
response.not_found.push(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
do_get
|
||||
@@ -104,10 +108,10 @@ impl JMAP {
|
||||
}
|
||||
response.list.push(result);
|
||||
} else {
|
||||
response.not_found.push(Id::singleton());
|
||||
response.not_found.push(Id::singleton().into());
|
||||
}
|
||||
} else {
|
||||
response.not_found.push(Id::singleton());
|
||||
response.not_found.push(Id::singleton().into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use jmap_proto::{
|
||||
request::websocket::{
|
||||
WebSocketMessage, WebSocketRequestError, WebSocketResponse, WebSocketStateChange,
|
||||
},
|
||||
types::type_state::TypeState,
|
||||
types::type_state::DataType,
|
||||
};
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tungstenite::Message;
|
||||
@@ -80,7 +80,7 @@ impl JMAP {
|
||||
return;
|
||||
};
|
||||
let mut changes = WebSocketStateChange::new(None);
|
||||
let mut change_types: Bitmap<TypeState> = Bitmap::new();
|
||||
let mut change_types: Bitmap<DataType> = Bitmap::new();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
||||
@@ -7,7 +7,7 @@ homepage = "https://stalw.art"
|
||||
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managesieve"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -39,19 +39,19 @@ impl<T: AsyncRead + AsyncWrite + IsTls> Session<T> {
|
||||
} else {
|
||||
response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER\"\r\n");
|
||||
};
|
||||
if let Some(sieve) =
|
||||
self.jmap
|
||||
.config
|
||||
.capabilities
|
||||
.capabilities
|
||||
.iter()
|
||||
.find_map(|(_, item)| {
|
||||
if let Capabilities::Sieve(sieve) = item {
|
||||
Some(sieve)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
if let Some(sieve) = self
|
||||
.jmap
|
||||
.config
|
||||
.capabilities
|
||||
.account
|
||||
.iter()
|
||||
.find_map(|(_, item)| {
|
||||
if let Capabilities::SieveAccount(sieve) = item {
|
||||
Some(sieve)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
response.extend_from_slice(b"\"SIEVE\" \"");
|
||||
response.extend_from_slice(sieve.extensions.join(" ").as_bytes());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nlp"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
|
||||
keywords = ["smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "utils"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use jmap::{mailbox::INBOX_ID, JMAP};
|
||||
use jmap_client::client::Client;
|
||||
use jmap_proto::types::id::Id;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
directory::sql::create_test_user_with_email,
|
||||
jmap::{jmap_json_request, mailbox::destroy_all_mailboxes},
|
||||
};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
println!("Running blob tests...");
|
||||
let directory = server.directory.as_ref();
|
||||
create_test_user_with_email(directory, "jdoe@example.com", "12345", "John Doe").await;
|
||||
let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap());
|
||||
|
||||
server
|
||||
.store
|
||||
.delete_account_blobs(account_id.document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob/set simple test
|
||||
let response = jmap_json_request(
|
||||
r#"[[
|
||||
"Blob/upload",
|
||||
{
|
||||
"accountId": "$$",
|
||||
"create": {
|
||||
"abc": {
|
||||
"data" : [
|
||||
{
|
||||
"data:asBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/AAAZ4gk3AAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="
|
||||
}
|
||||
],
|
||||
"type": "image/png"
|
||||
}
|
||||
}
|
||||
},
|
||||
"R1"
|
||||
]]"#
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer("/methodResponses/0/1/created/abc/type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default(),
|
||||
"image/png",
|
||||
"Response: {:?}",
|
||||
response
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer("/methodResponses/0/1/created/abc/size")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or_default(),
|
||||
95,
|
||||
"Response: {:?}",
|
||||
response
|
||||
);
|
||||
|
||||
// Blob/get simple test
|
||||
let blob_id = jmap_json_request(
|
||||
r#"[[
|
||||
"Blob/upload",
|
||||
{
|
||||
"accountId": "$$",
|
||||
"create": {
|
||||
"abc": {
|
||||
"data" : [
|
||||
{
|
||||
"data:asText": "The quick brown fox jumped over the lazy dog."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"R1"
|
||||
]]"#
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await
|
||||
.pointer("/methodResponses/0/1/created/abc/id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let response = jmap_json_request(
|
||||
r#"[
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"ids" : [
|
||||
"%%"
|
||||
],
|
||||
"properties" : [
|
||||
"data:asText",
|
||||
"digest:sha",
|
||||
"size"
|
||||
]
|
||||
},
|
||||
"R1"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"ids" : [
|
||||
"%%"
|
||||
],
|
||||
"properties" : [
|
||||
"data:asText",
|
||||
"digest:sha",
|
||||
"digest:sha-256",
|
||||
"size"
|
||||
],
|
||||
"offset" : 4,
|
||||
"length" : 9
|
||||
},
|
||||
"R2"
|
||||
]
|
||||
]"#
|
||||
.replace("$$", &account_id.to_string())
|
||||
.replace("%%", &blob_id),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await;
|
||||
|
||||
for (pointer, expected) in [
|
||||
(
|
||||
"/methodResponses/0/1/list/0/data:asText",
|
||||
"The quick brown fox jumped over the lazy dog.",
|
||||
),
|
||||
(
|
||||
"/methodResponses/0/1/list/0/digest:sha",
|
||||
"wIVPufsDxBzOOALLDSIFKebu+U4=",
|
||||
),
|
||||
("/methodResponses/0/1/list/0/size", "45"),
|
||||
("/methodResponses/1/1/list/0/data:asText", "quick bro"),
|
||||
(
|
||||
"/methodResponses/1/1/list/0/digest:sha",
|
||||
"QiRAPtfyX8K6tm1iOAtZ87Xj3Ww=",
|
||||
),
|
||||
(
|
||||
"/methodResponses/1/1/list/0/digest:sha-256",
|
||||
"gdg9INW7lwHK6OQ9u0dwDz2ZY/gubi0En0xlFpKt0OA=",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer(pointer)
|
||||
.and_then(|v| match v {
|
||||
Value::String(s) => Some(s.to_string()),
|
||||
Value::Number(n) => Some(n.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
expected,
|
||||
"Pointer {pointer:?} Response: {response:?}",
|
||||
);
|
||||
}
|
||||
|
||||
server
|
||||
.store
|
||||
.delete_account_blobs(account_id.document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob/upload Complex Example
|
||||
let response = jmap_json_request(
|
||||
r##"[
|
||||
[
|
||||
"Blob/upload",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"create": {
|
||||
"b4": {
|
||||
"data": [
|
||||
{
|
||||
"data:asText": "The quick brown fox jumped over the lazy dog."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"S4"
|
||||
],
|
||||
[
|
||||
"Blob/upload",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"create": {
|
||||
"cat": {
|
||||
"data": [
|
||||
{
|
||||
"data:asText": "How"
|
||||
},
|
||||
{
|
||||
"blobId": "#b4",
|
||||
"length": 7,
|
||||
"offset": 3
|
||||
},
|
||||
{
|
||||
"data:asText": "was t"
|
||||
},
|
||||
{
|
||||
"blobId": "#b4",
|
||||
"length": 1,
|
||||
"offset": 1
|
||||
},
|
||||
{
|
||||
"data:asBase64": "YXQ/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"CAT"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"properties": [
|
||||
"data:asText",
|
||||
"size"
|
||||
],
|
||||
"ids": [
|
||||
"#cat"
|
||||
]
|
||||
},
|
||||
"G4"
|
||||
]
|
||||
]"##
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await;
|
||||
|
||||
for (pointer, expected) in [
|
||||
(
|
||||
"/methodResponses/2/1/list/0/data:asText",
|
||||
"How quick was that?",
|
||||
),
|
||||
("/methodResponses/2/1/list/0/size", "19"),
|
||||
] {
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer(pointer)
|
||||
.and_then(|v| match v {
|
||||
Value::String(s) => Some(s.to_string()),
|
||||
Value::Number(n) => Some(n.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
expected,
|
||||
"Pointer {pointer:?} Response: {response:?}",
|
||||
);
|
||||
}
|
||||
server
|
||||
.store
|
||||
.delete_account_blobs(account_id.document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob/get Example with Range and Encoding Errors
|
||||
let response = jmap_json_request(
|
||||
r##"[
|
||||
[
|
||||
"Blob/upload",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"create": {
|
||||
"b1": {
|
||||
"data": [
|
||||
{
|
||||
"data:asBase64": "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUggYEgZG9nLg=="
|
||||
}
|
||||
]
|
||||
},
|
||||
"b2": {
|
||||
"data": [
|
||||
{
|
||||
"data:asText": "hello world"
|
||||
}
|
||||
],
|
||||
"type" : "text/plain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"S1"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"ids": [
|
||||
"#b1",
|
||||
"#b2"
|
||||
]
|
||||
},
|
||||
"G1"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"ids": [
|
||||
"#b1",
|
||||
"#b2"
|
||||
],
|
||||
"properties": [
|
||||
"data:asText",
|
||||
"size"
|
||||
]
|
||||
},
|
||||
"G2"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"ids": [
|
||||
"#b1",
|
||||
"#b2"
|
||||
],
|
||||
"properties": [
|
||||
"data:asBase64",
|
||||
"size"
|
||||
]
|
||||
},
|
||||
"G3"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"offset": 0,
|
||||
"length": 5,
|
||||
"ids": [
|
||||
"#b1",
|
||||
"#b2"
|
||||
]
|
||||
},
|
||||
"G4"
|
||||
],
|
||||
[
|
||||
"Blob/get",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"offset": 20,
|
||||
"length": 100,
|
||||
"ids": [
|
||||
"#b1",
|
||||
"#b2"
|
||||
]
|
||||
},
|
||||
"G5"
|
||||
]
|
||||
]"##
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await;
|
||||
|
||||
for (pointer, expected) in [
|
||||
(
|
||||
"/methodResponses/1/1/list/0/data:asBase64",
|
||||
"VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUggYEgZG9nLg==",
|
||||
),
|
||||
("/methodResponses/1/1/list/1/data:asText", "hello world"),
|
||||
("/methodResponses/2/1/list/0/isEncodingProblem", "true"),
|
||||
("/methodResponses/2/1/list/1/data:asText", "hello world"),
|
||||
(
|
||||
"/methodResponses/3/1/list/0/data:asBase64",
|
||||
"VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUggYEgZG9nLg==",
|
||||
),
|
||||
(
|
||||
"/methodResponses/3/1/list/1/data:asBase64",
|
||||
"aGVsbG8gd29ybGQ=",
|
||||
),
|
||||
("/methodResponses/4/1/list/0/data:asText", "The q"),
|
||||
("/methodResponses/4/1/list/1/data:asText", "hello"),
|
||||
("/methodResponses/5/1/list/0/isEncodingProblem", "true"),
|
||||
("/methodResponses/5/1/list/0/isTruncated", "true"),
|
||||
("/methodResponses/5/1/list/1/isTruncated", "true"),
|
||||
] {
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer(pointer)
|
||||
.and_then(|v| match v {
|
||||
Value::String(s) => Some(s.to_string()),
|
||||
Value::Number(n) => Some(n.to_string()),
|
||||
Value::Bool(b) => Some(b.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
expected,
|
||||
"Pointer {pointer:?} Response: {response:?}",
|
||||
);
|
||||
}
|
||||
server
|
||||
.store
|
||||
.delete_account_blobs(account_id.document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Blob/lookup
|
||||
admin_client.set_default_account_id(account_id.to_string());
|
||||
let blob_id = admin_client
|
||||
.email_import(
|
||||
concat!(
|
||||
"From: bill@example.com\r\n",
|
||||
"To: jdoe@example.com\r\n",
|
||||
"Subject: TPS Report\r\n",
|
||||
"\r\n",
|
||||
"I'm going to need those TPS reports ASAP. ",
|
||||
"So, if you could do that, that'd be great."
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
[&Id::from(INBOX_ID).to_string()],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_blob_id();
|
||||
|
||||
let response = jmap_json_request(
|
||||
r#"[[
|
||||
"Blob/lookup",
|
||||
{
|
||||
"accountId" : "$$",
|
||||
"typeNames": [
|
||||
"Mailbox",
|
||||
"Thread",
|
||||
"Email"
|
||||
],
|
||||
"ids": [
|
||||
"%%",
|
||||
"not-a-blob"
|
||||
]
|
||||
},
|
||||
"R1"
|
||||
]]"#
|
||||
.replace("$$", &account_id.to_string())
|
||||
.replace("%%", &blob_id),
|
||||
"jdoe@example.com",
|
||||
"12345",
|
||||
)
|
||||
.await;
|
||||
|
||||
for pointer in [
|
||||
"/methodResponses/0/1/list/0/matchedIds/Email",
|
||||
"/methodResponses/0/1/list/0/matchedIds/Mailbox",
|
||||
"/methodResponses/0/1/list/0/matchedIds/Thread",
|
||||
] {
|
||||
assert_eq!(
|
||||
response
|
||||
.pointer(pointer)
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.len())
|
||||
.unwrap_or_default(),
|
||||
1,
|
||||
"Pointer {pointer:?} Response: {response:?}",
|
||||
);
|
||||
}
|
||||
|
||||
// Remove test data
|
||||
admin_client.set_default_account_id(account_id.to_string());
|
||||
destroy_all_mailboxes(admin_client).await;
|
||||
server.store.assert_is_empty().await;
|
||||
}
|
||||
+51
-2
@@ -23,10 +23,12 @@
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use directory::config::ConfigDirectory;
|
||||
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
||||
use jmap_client::client::{Client, Credentials};
|
||||
use jmap_proto::types::id::Id;
|
||||
use reqwest::header;
|
||||
use smtp::core::{SmtpSessionManager, SMTP};
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use utils::{config::ServerProtocol, UnwrapFailure};
|
||||
@@ -40,6 +42,7 @@ use crate::{
|
||||
pub mod auth_acl;
|
||||
pub mod auth_limits;
|
||||
pub mod auth_oauth;
|
||||
pub mod blob;
|
||||
pub mod crypto;
|
||||
pub mod delivery;
|
||||
pub mod email_changes;
|
||||
@@ -219,12 +222,12 @@ refresh-token-renew = "2s"
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn jmap_tests() {
|
||||
tracing::subscriber::set_global_default(
|
||||
/*tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::WARN)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap();*/
|
||||
|
||||
let delete = true;
|
||||
let mut params = init_jmap_tests(delete).await;
|
||||
@@ -251,6 +254,7 @@ pub async fn jmap_tests() {
|
||||
websocket::test(params.server.clone(), &mut params.client).await;
|
||||
quota::test(params.server.clone(), &mut params.client).await;
|
||||
crypto::test(params.server.clone(), &mut params.client).await;
|
||||
blob::test(params.server.clone(), &mut params.client).await;
|
||||
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
@@ -338,6 +342,51 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn jmap_raw_request(body: impl AsRef<str>, username: &str, secret: &str) -> String {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
|
||||
headers.insert(
|
||||
header::AUTHORIZATION,
|
||||
header::HeaderValue::from_str(&format!(
|
||||
"Basic {}",
|
||||
general_purpose::STANDARD.encode(format!("{}:{}", username, secret))
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
const BODY_TEMPLATE: &str = r#"{
|
||||
"using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:quota" ],
|
||||
"methodCalls": $$
|
||||
}"#;
|
||||
|
||||
String::from_utf8(
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(1000))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
.post("https://127.0.0.1:8899/jmap")
|
||||
.body(BODY_TEMPLATE.replace("$$", body.as_ref()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_vec(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn jmap_json_request(
|
||||
body: impl AsRef<str>,
|
||||
username: &str,
|
||||
secret: &str,
|
||||
) -> serde_json::Value {
|
||||
serde_json::from_str(&jmap_raw_request(body, username, secret).await).unwrap()
|
||||
}
|
||||
|
||||
pub fn find_values(string: &str, name: &str) -> Vec<String> {
|
||||
let mut last_pos = 0;
|
||||
let mut values = Vec::new();
|
||||
|
||||
@@ -44,7 +44,7 @@ use jmap::{
|
||||
JMAP,
|
||||
};
|
||||
use jmap_client::{client::Client, mailbox::Role, push_subscription::Keys};
|
||||
use jmap_proto::types::{id::Id, type_state::TypeState};
|
||||
use jmap_proto::types::{id::Id, type_state::DataType};
|
||||
use reqwest::header::CONTENT_ENCODING;
|
||||
use store::ahash::AHashSet;
|
||||
use tokio::{net::TcpStream, sync::mpsc};
|
||||
@@ -138,7 +138,7 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.unwrap()
|
||||
.take_id();
|
||||
|
||||
assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
|
||||
|
||||
// Receive states just for the requested types
|
||||
client
|
||||
@@ -192,14 +192,14 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
push_server.fail_requests.store(false, Ordering::Relaxed);
|
||||
assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
|
||||
|
||||
// Make a mailbox change and expect state change
|
||||
client
|
||||
.mailbox_rename(&mailbox_id, "My Mailbox")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
|
||||
//expect_nothing(&mut event_rx).await;
|
||||
|
||||
// Multiple change updates should be grouped and pushed in intervals
|
||||
@@ -209,7 +209,7 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
assert_state(&mut event_rx, &account_id, &[TypeState::Mailbox]).await;
|
||||
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
|
||||
expect_nothing(&mut event_rx).await;
|
||||
|
||||
// Destroy mailbox
|
||||
@@ -365,7 +365,7 @@ async fn expect_nothing(event_rx: &mut mpsc::Receiver<PushMessage>) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state: &[TypeState]) {
|
||||
async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state: &[DataType]) {
|
||||
assert_eq!(
|
||||
expect_push(event_rx)
|
||||
.await
|
||||
@@ -375,8 +375,8 @@ async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| x.0)
|
||||
.collect::<AHashSet<&TypeState>>(),
|
||||
state.iter().collect::<AHashSet<&TypeState>>()
|
||||
.collect::<AHashSet<&DataType>>(),
|
||||
state.iter().collect::<AHashSet<&DataType>>()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -33,7 +33,10 @@ use jmap_proto::types::{collection::Collection, id::Id};
|
||||
|
||||
use crate::{
|
||||
directory::sql::{add_to_group, create_test_user_with_email, set_test_quota},
|
||||
jmap::{delivery::SmtpConnection, mailbox::destroy_all_mailboxes, test_account_login},
|
||||
jmap::{
|
||||
delivery::SmtpConnection, jmap_raw_request, mailbox::destroy_all_mailboxes,
|
||||
test_account_login,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
@@ -110,6 +113,26 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test JMAP Quotas extension
|
||||
let response = jmap_raw_request(
|
||||
r#"[[ "Quota/get", {
|
||||
"accountId": "$$",
|
||||
"ids": null
|
||||
}, "0" ]]"#
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"robert@example.com",
|
||||
"aabbcc",
|
||||
)
|
||||
.await;
|
||||
assert!(response.contains("\"used\":0"), "{}", response);
|
||||
assert!(response.contains("\"hardLimit\":1024"), "{}", response);
|
||||
assert!(response.contains("\"scope\":\"account\""), "{}", response);
|
||||
assert!(
|
||||
response.contains("\"name\":\"robert@example.com\""),
|
||||
"{}",
|
||||
response
|
||||
);
|
||||
|
||||
// Test Email/import quota
|
||||
let inbox_id = Id::new(INBOX_ID as u64).to_string();
|
||||
let mut message_ids = Vec::new();
|
||||
@@ -143,6 +166,20 @@ pub async fn test(server: Arc<JMAP>, admin_client: &mut Client) {
|
||||
.await,
|
||||
);
|
||||
|
||||
// Test JMAP Quotas extension
|
||||
let response = jmap_raw_request(
|
||||
r#"[[ "Quota/get", {
|
||||
"accountId": "$$",
|
||||
"ids": null
|
||||
}, "0" ]]"#
|
||||
.replace("$$", &account_id.to_string()),
|
||||
"robert@example.com",
|
||||
"aabbcc",
|
||||
)
|
||||
.await;
|
||||
assert!(response.contains("\"used\":1024"), "{}", response);
|
||||
assert!(response.contains("\"hardLimit\":1024"), "{}", response);
|
||||
|
||||
// Delete messages and check available quota
|
||||
for message_id in message_ids {
|
||||
client.email_destroy(&message_id).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user