This commit is contained in:
mdecimus
2023-11-01 12:08:24 +01:00
parent ee088d5184
commit df45384fcd
94 changed files with 3024 additions and 489 deletions
+13
View File
@@ -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
View File
@@ -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",
+2 -2
View File
@@ -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.
+7
View File
@@ -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`
-----------------------------------
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "imap"
version = "0.4.0"
version = "0.4.2"
edition = "2021"
resolver = "2"
+2 -2
View File
@@ -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;
}
+4 -4
View File
@@ -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;
}
+6 -6
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+5 -5
View File
@@ -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;
+4 -4
View File
@@ -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;
}
+2 -2
View File
@@ -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;
}
+5 -9
View File
@@ -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;
}
_ => {}
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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"
+19
View File
@@ -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)?;
+8
View File
@@ -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.")
}
+2
View File
@@ -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",
+48 -10
View File
@@ -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)
}
+1 -1
View File
@@ -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());
}
}
}
+92
View File
@@ -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)
}
}
+2
View File
@@ -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)]
+17
View File
@@ -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",
+31 -5
View File
@@ -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());
}
}
}
+232
View File
@@ -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());
}
}
}
+107
View File
@@ -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);
}
}
}
+1
View File
@@ -21,6 +21,7 @@
* for more details.
*/
pub mod blob;
pub mod email;
pub mod email_submission;
pub mod index;
+1 -1
View File
@@ -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)
+8 -3
View File
@@ -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()),
+28 -2
View File
@@ -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",
})
}
}
+6 -2
View File
@@ -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),
}
+30 -10
View File
@@ -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!(
+5 -5
View File
@@ -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 => {
+21 -5
View File
@@ -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 {
+194 -81
View File
@@ -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();
+147
View File
@@ -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),
}
}
}
+2 -2
View File
@@ -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)]
+9 -7
View File
@@ -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(()),
}
}
+1 -36
View File
@@ -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 }
+35
View File
@@ -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),
}
}
}
+114 -2
View File
@@ -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,
}
}
+3 -3
View File
@@ -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 {
+98 -49
View File
@@ -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"))
}
}
+12 -18
View File
@@ -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),
+3 -2
View File
@@ -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"
+2 -2
View File
@@ -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();
+2 -2
View File
@@ -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 {
+42 -7
View File
@@ -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
View File
@@ -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"],
}
}
}
+24 -13
View File
@@ -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,
+288
View File
@@ -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)
}
}
+1
View File
@@ -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)]
+173 -1
View File
@@ -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,
+6 -1
View File
@@ -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![],
+2
View File
@@ -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!(),
};
+21 -18
View File
@@ -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()
}
}
+5 -5
View File
@@ -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;
}
}
+4 -4
View File
@@ -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()
}
}
+35 -28
View File
@@ -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();
}
+2 -2
View File
@@ -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());
+1
View File
@@ -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;
+2 -2
View File
@@ -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;
}
}
+4 -4
View File
@@ -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
}
+2 -2
View File
@@ -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;
};
+4 -4
View File
@@ -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);
}
+2 -2
View File
@@ -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>,
}
+16 -9
View File
@@ -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()
}) =>
{
+99
View File
@@ -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)
}
}
+25
View File
@@ -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;
+113
View File
@@ -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)
}*/
}
}
+5 -5
View File
@@ -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;
}
+10 -10
View File
@@ -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();
+2 -2
View File
@@ -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());
+2 -2
View File
@@ -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(),
+2 -2
View File
@@ -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;
};
+1 -1
View File
@@ -92,7 +92,7 @@ impl JMAP {
}
response.list.push(thread);
} else {
response.not_found.push(id);
response.not_found.push(id.into());
}
}
+11 -7
View File
@@ -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());
}
}
+2 -2
View File
@@ -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! {
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "managesieve"
version = "0.4.0"
version = "0.4.2"
edition = "2021"
resolver = "2"
+13 -13
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nlp"
version = "0.4.0"
version = "0.4.2"
edition = "2021"
resolver = "2"
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "utils"
version = "0.4.0"
version = "0.4.2"
edition = "2021"
resolver = "2"
+509
View File
@@ -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
View File
@@ -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();
+8 -8
View File
@@ -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
View File
@@ -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();