summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2023-11-01 12:08:24 +0100
committermdecimus <mauro@stalw.art>2023-11-01 12:08:24 +0100
commitdf45384fcd21c64928fa604a40f4c49bef036302 (patch)
tree36c162ca5fa6396c7320d2c7f12eef15f9f5089a
parentee088d51843735cbd78af714b262a0807ab6e259 (diff)
v0.4.2v0.4.2
-rw-r--r--CHANGELOG.md13
-rw-r--r--Cargo.lock19
-rw-r--r--README.md4
-rw-r--r--UPGRADING.md7
-rw-r--r--crates/cli/Cargo.toml2
-rw-r--r--crates/imap/Cargo.toml2
-rw-r--r--crates/imap/src/op/acl.rs4
-rw-r--r--crates/imap/src/op/append.rs8
-rw-r--r--crates/imap/src/op/copy_move.rs12
-rw-r--r--crates/imap/src/op/create.rs4
-rw-r--r--crates/imap/src/op/delete.rs10
-rw-r--r--crates/imap/src/op/expunge.rs8
-rw-r--r--crates/imap/src/op/fetch.rs4
-rw-r--r--crates/imap/src/op/idle.rs14
-rw-r--r--crates/imap/src/op/rename.rs4
-rw-r--r--crates/imap/src/op/store.rs8
-rw-r--r--crates/imap/src/op/subscribe.rs4
-rw-r--r--crates/install/Cargo.toml2
-rw-r--r--crates/jmap-proto/src/error/method.rs19
-rw-r--r--crates/jmap-proto/src/error/set.rs8
-rw-r--r--crates/jmap-proto/src/method/changes.rs2
-rw-r--r--crates/jmap-proto/src/method/get.rs58
-rw-r--r--crates/jmap-proto/src/method/import.rs2
-rw-r--r--crates/jmap-proto/src/method/lookup.rs92
-rw-r--r--crates/jmap-proto/src/method/mod.rs2
-rw-r--r--crates/jmap-proto/src/method/query.rs17
-rw-r--r--crates/jmap-proto/src/method/query_changes.rs1
-rw-r--r--crates/jmap-proto/src/method/set.rs36
-rw-r--r--crates/jmap-proto/src/method/upload.rs232
-rw-r--r--crates/jmap-proto/src/object/blob.rs107
-rw-r--r--crates/jmap-proto/src/object/mod.rs1
-rw-r--r--crates/jmap-proto/src/parser/impls.rs2
-rw-r--r--crates/jmap-proto/src/parser/json.rs11
-rw-r--r--crates/jmap-proto/src/request/capability.rs6
-rw-r--r--crates/jmap-proto/src/request/method.rs30
-rw-r--r--crates/jmap-proto/src/request/mod.rs8
-rw-r--r--crates/jmap-proto/src/request/parser.rs40
-rw-r--r--crates/jmap-proto/src/request/reference.rs21
-rw-r--r--crates/jmap-proto/src/request/websocket.rs10
-rw-r--r--crates/jmap-proto/src/response/mod.rs26
-rw-r--r--crates/jmap-proto/src/response/references.rs275
-rw-r--r--crates/jmap-proto/src/types/any_id.rs147
-rw-r--r--crates/jmap-proto/src/types/blob.rs4
-rw-r--r--crates/jmap-proto/src/types/collection.rs16
-rw-r--r--crates/jmap-proto/src/types/id.rs37
-rw-r--r--crates/jmap-proto/src/types/mod.rs35
-rw-r--r--crates/jmap-proto/src/types/property.rs116
-rw-r--r--crates/jmap-proto/src/types/state.rs6
-rw-r--r--crates/jmap-proto/src/types/type_state.rs147
-rw-r--r--crates/jmap-proto/src/types/value.rs30
-rw-r--r--crates/jmap/Cargo.toml5
-rw-r--r--crates/jmap/src/api/event_source.rs4
-rw-r--r--crates/jmap/src/api/mod.rs4
-rw-r--r--crates/jmap/src/api/request.rs49
-rw-r--r--crates/jmap/src/api/session.rs135
-rw-r--r--crates/jmap/src/blob/download.rs37
-rw-r--r--crates/jmap/src/blob/get.rs288
-rw-r--r--crates/jmap/src/blob/mod.rs1
-rw-r--r--crates/jmap/src/blob/upload.rs174
-rw-r--r--crates/jmap/src/changes/get.rs7
-rw-r--r--crates/jmap/src/changes/query.rs2
-rw-r--r--crates/jmap/src/email/copy.rs39
-rw-r--r--crates/jmap/src/email/get.rs10
-rw-r--r--crates/jmap/src/email/import.rs8
-rw-r--r--crates/jmap/src/email/set.rs63
-rw-r--r--crates/jmap/src/identity/get.rs4
-rw-r--r--crates/jmap/src/lib.rs1
-rw-r--r--crates/jmap/src/mailbox/get.rs4
-rw-r--r--crates/jmap/src/mailbox/set.rs8
-rw-r--r--crates/jmap/src/principal/get.rs4
-rw-r--r--crates/jmap/src/push/get.rs8
-rw-r--r--crates/jmap/src/push/mod.rs4
-rw-r--r--crates/jmap/src/push/set.rs25
-rw-r--r--crates/jmap/src/quota/get.rs99
-rw-r--r--crates/jmap/src/quota/mod.rs25
-rw-r--r--crates/jmap/src/quota/query.rs113
-rw-r--r--crates/jmap/src/services/ingest.rs10
-rw-r--r--crates/jmap/src/services/state.rs20
-rw-r--r--crates/jmap/src/sieve/get.rs4
-rw-r--r--crates/jmap/src/sieve/set.rs4
-rw-r--r--crates/jmap/src/submission/get.rs4
-rw-r--r--crates/jmap/src/thread/get.rs2
-rw-r--r--crates/jmap/src/vacation/get.rs18
-rw-r--r--crates/jmap/src/websocket/stream.rs4
-rw-r--r--crates/main/Cargo.toml2
-rw-r--r--crates/managesieve/Cargo.toml2
-rw-r--r--crates/managesieve/src/op/capability.rs26
-rw-r--r--crates/nlp/Cargo.toml2
-rw-r--r--crates/smtp/Cargo.toml2
-rw-r--r--crates/utils/Cargo.toml2
-rw-r--r--tests/src/jmap/blob.rs509
-rw-r--r--tests/src/jmap/mod.rs53
-rw-r--r--tests/src/jmap/push_subscription.rs16
-rw-r--r--tests/src/jmap/quota.rs39
94 files changed, 3024 insertions, 489 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 726a9174..91e31deb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index d525badb..80e61090 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/README.md b/README.md
index 77a938be..d463a184 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/UPGRADING.md b/UPGRADING.md
index bb8dff5c..3cab29d8 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -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`
-----------------------------------
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 68f48ff2..48aa53d1 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -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"
diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml
index ae2649e0..2c1aedf9 100644
--- a/crates/imap/Cargo.toml
+++ b/crates/imap/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "imap"
-version = "0.4.0"
+version = "0.4.2"
edition = "2021"
resolver = "2"
diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs
index bef68105..2b7d04ee 100644
--- a/crates/imap/src/op/acl.rs
+++ b/crates/imap/src/op/acl.rs
@@ -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;
}
diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs
index 184bcf5a..4faabb98 100644
--- a/crates/imap/src/op/append.rs
+++ b/crates/imap/src/op/append.rs
@@ -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;
}
diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs
index 5e2abf69..c6188db0 100644
--- a/crates/imap/src/op/copy_move.rs
+++ b/crates/imap/src/op/copy_move.rs
@@ -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;
}
diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs
index 4bb95f05..460694fd 100644
--- a/crates/imap/src/op/create.rs
+++ b/crates/imap/src/op/create.rs
@@ -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;
diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs
index 52281a81..8f95d606 100644
--- a/crates/imap/src/op/delete.rs
+++ b/crates/imap/src/op/delete.rs
@@ -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;
diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs
index 4b963088..fe545320 100644
--- a/crates/imap/src/op/expunge.rs
+++ b/crates/imap/src/op/expunge.rs
@@ -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;
}
diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs
index abb0f4d3..98d3a417 100644
--- a/crates/imap/src/op/fetch.rs
+++ b/crates/imap/src/op/fetch.rs
@@ -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;
}
diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs
index ffa813d5..7c1d2ede 100644
--- a/crates/imap/src/op/idle.rs
+++ b/crates/imap/src/op/idle.rs
@@ -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;
}
_ => {}
diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs
index 7ed754ec..fea2dd30 100644
--- a/crates/imap/src/op/rename.rs
+++ b/crates/imap/src/op/rename.rs
@@ -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;
diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs
index f56a3491..f2d5e0e3 100644
--- a/crates/imap/src/op/store.rs
+++ b/crates/imap/src/op/store.rs
@@ -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;
}
diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs
index 1562272d..c383865e 100644
--- a/crates/imap/src/op/subscribe.rs
+++ b/crates/imap/src/op/subscribe.rs
@@ -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;
diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml
index bbb02647..54ad89fc 100644
--- a/crates/install/Cargo.toml
+++ b/crates/install/Cargo.toml
@@ -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"
diff --git a/crates/jmap-proto/src/error/method.rs b/crates/jmap-proto/src/error/method.rs
index b335cb94..97af70af 100644
--- a/crates/jmap-proto/src/error/method.rs
+++ b/crates/jmap-proto/src/error/method.rs
@@ -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)?;
diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs
index 2cdf693a..83ee75e0 100644
--- a/crates/jmap-proto/src/error/set.rs
+++ b/crates/jmap-proto/src/error/set.rs
@@ -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.")
}
diff --git a/crates/jmap-proto/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs
index 52acb712..6cb2a181 100644
--- a/crates/jmap-proto/src/method/changes.rs
+++ b/crates/jmap-proto/src/method/changes.rs
@@ -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",
diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs
index 1d38c4ac..eb3dd7ea 100644
--- a/crates/jmap-proto/src/method/get.rs
+++ b/crates/jmap-proto/src/method/get.rs
@@ -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)
}
diff --git a/crates/jmap-proto/src/method/import.rs b/crates/jmap-proto/src/method/import.rs
index c7ce8629..591698c8 100644
--- a/crates/jmap-proto/src/method/import.rs
+++ b/crates/jmap-proto/src/method/import.rs
@@ -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());
}
}
}
diff --git a/crates/jmap-proto/src/method/lookup.rs b/crates/jmap-proto/src/method/lookup.rs
new file mode 100644
index 00000000..3f01e192
--- /dev/null
+++ b/crates/jmap-proto/src/method/lookup.rs
@@ -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)
+ }
+}
diff --git a/crates/jmap-proto/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs
index d9a34de6..26e8e228 100644
--- a/crates/jmap-proto/src/method/mod.rs
+++ b/crates/jmap-proto/src/method/mod.rs
@@ -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)]
diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs
index 1df7c3bc..b5eee2ea 100644
--- a/crates/jmap-proto/src/method/query.rs
+++ b/crates/jmap-proto/src/method/query.rs
@@ -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,
})
}
diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs
index d9820afd..2af1c470 100644
--- a/crates/jmap-proto/src/method/query_changes.rs
+++ b/crates/jmap-proto/src/method/query_changes.rs
@@ -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",
diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs
index 2b28b300..5820cd1e 100644
--- a/crates/jmap-proto/src/method/set.rs
+++ b/crates/jmap-proto/src/method/set.rs
@@ -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());
}
}
}
diff --git a/crates/jmap-proto/src/method/upload.rs b/crates/jmap-proto/src/method/upload.rs
new file mode 100644
index 00000000..4c887ab8
--- /dev/null
+++ b/crates/jmap-proto/src/method/upload.rs
@@ -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());
+ }
+ }
+}
diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs
new file mode 100644
index 00000000..75dd89ad
--- /dev/null
+++ b/crates/jmap-proto/src/object/blob.rs
@@ -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);
+ }
+ }
+}
diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs
index d9b3f0da..99734006 100644
--- a/crates/jmap-proto/src/object/mod.rs
+++ b/crates/jmap-proto/src/object/mod.rs
@@ -21,6 +21,7 @@
* for more details.
*/
+pub mod blob;
pub mod email;
pub mod email_submission;
pub mod index;
diff --git a/crates/jmap-proto/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs
index 318d70e4..a793058f 100644
--- a/crates/jmap-proto/src/parser/impls.rs
+++ b/crates/jmap-proto/src/parser/impls.rs
@@ -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)
diff --git a/crates/jmap-proto/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs
index a03560c9..3a8ed474 100644
--- a/crates/jmap-proto/src/parser/json.rs
+++ b/crates/jmap-proto/src/parser/json.rs
@@ -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,
@@ -82,6 +82,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;
self.iter.next().copied()
diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs
index 5e27a187..67887170 100644
--- a/crates/jmap-proto/src/request/capability.rs
+++ b/crates/jmap-proto/src/request/capability.rs
@@ -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()),
diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs
index 5616dbaa..e3c67011 100644
--- a/crates/jmap-proto/src/request/method.rs
+++ b/crates/jmap-proto/src/request/method.rs
@@ -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",
})
}
}
diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs
index 8a5b6ba7..b25b1cff 100644
--- a/crates/jmap-proto/src/request/mod.rs
+++ b/crates/jmap-proto/src/request/mod.rs
@@ -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),
}
diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs
index 24a41dbf..a43803e4 100644
--- a/crates/jmap-proto/src/request/parser.rs
+++ b/crates/jmap-proto/src/request/parser.rs
@@ -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)
diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs
index 320dc63e..3f856080 100644
--- a/crates/jmap-proto/src/request/reference.rs
+++ b/crates/jmap-proto/src/request/reference.rs
@@ -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!(
diff --git a/crates/jmap-proto/src/request/websocket.rs b/crates/jmap-proto/src/request/websocket.rs
index 97584f64..f5ee79d2 100644
--- a/crates/jmap-proto/src/request/websocket.rs
+++ b/crates/jmap-proto/src/request/websocket.rs
@@ -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 => {
diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs
index b028b8e1..273cc844 100644
--- a/crates/jmap-proto/src/response/mod.rs
+++ b/crates/jmap-proto/src/response/mod.rs
@@ -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 {
diff --git a/crates/jmap-proto/src/response/references.rs b/crates/jmap-proto/src/response/references.rs
index 06ec15ed..ed35729d 100644
--- a/crates/jmap-proto/src/response/references.rs
+++ b/crates/jmap-proto/src/response/references.rs
@@ -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();
diff --git a/crates/jmap-proto/src/types/any_id.rs b/crates/jmap-proto/src/types/any_id.rs
new file mode 100644
index 00000000..a3786ebc
--- /dev/null
+++ b/crates/jmap-proto/src/types/any_id.rs
@@ -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),
+ }
+ }
+}
diff --git a/crates/jmap-proto/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs
index 4be8ad0b..eaee0442 100644
--- a/crates/jmap-proto/src/types/blob.rs
+++ b/crates/jmap-proto/src/types/blob.rs
@@ -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)]
diff --git a/crates/jmap-proto/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs
index 93c6979f..5684d534 100644
--- a/crates/jmap-proto/src/types/collection.rs
+++ b/crates/jmap-proto/src/types/collection.rs
@@ -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(()),
}
}
diff --git a/crates/jmap-proto/src/types/id.rs b/crates/jmap-proto/src/types/id.rs
index b423f413..46bdb11b 100644
--- a/crates/jmap-proto/src/types/id.rs
+++ b/crates/jmap-proto/src/types/id.rs
@@ -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 }
diff --git a/crates/jmap-proto/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs
index 55430e99..1a03ee30 100644
--- a/crates/jmap-proto/src/types/mod.rs
+++ b/crates/jmap-proto/src/types/mod.rs
@@ -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),
+ }
+ }
+}
diff --git a/crates/jmap-proto/src/types/property.rs b/crates/jmap-proto/src/types/property.rs
index 4e0dbe6b..66d1482b 100644
--- a/crates/jmap-proto/src/types/property.rs
+++ b/crates/jmap-proto/src/types/property.rs
@@ -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,
}
}
diff --git a/crates/jmap-proto/src/types/state.rs b/crates/jmap-proto/src/types/state.rs
index 2c2de523..50993b99 100644
--- a/crates/jmap-proto/src/types/state.rs
+++ b/crates/jmap-proto/src/types/state.rs
@@ -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 {
diff --git a/crates/jmap-proto/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs
index 938b4e84..4a46b1a5 100644
--- a/crates/jmap-proto/src/types/type_state.rs
+++ b/crates/jmap-proto/src/types/type_state.rs
@@ -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"))
}
}
diff --git a/crates/jmap-proto/src/types/value.rs b/crates/jmap-proto/src/types/value.rs
index 62d00527..b5d7b1e3 100644
--- a/crates/jmap-proto/src/types/value.rs
+++ b/crates/jmap-proto/src/types/value.rs
@@ -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),
diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml
index fb972099..dd118584 100644
--- a/crates/jmap/Cargo.toml
+++ b/crates/jmap/Cargo.toml
@@ -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"
diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs
index e8a85373..a49261f4 100644
--- a/crates/jmap/src/api/event_source.rs
+++ b/crates/jmap/src/api/event_source.rs
@@ -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();
diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs
index 0943ef9d..927225d6 100644
--- a/crates/jmap/src/api/mod.rs
+++ b/crates/jmap/src/api/mod.rs
@@ -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 {
diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs
index 8d30a3af..0f474be1 100644
--- a/crates/jmap/src/api/request.rs
+++ b/crates/jmap/src/api/request.rs
@@ -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),
})
diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs
index 6f5ee95a..305ab023 100644
--- a/crates/jmap/src/api/session.rs
+++ b/crates/jmap/src/api/session.rs
@@ -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::SieveSession(SieveSessionCapabilities::default()),
+ );
+ self.capabilities.account.append(
Capability::Sieve,
- Capabilities::Sieve(SieveCapabilities::new(self, settings)),
+ 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"],
+ }
+ }
+}
diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs
index 3ef8449c..466db936 100644
--- a/crates/jmap/src/blob/download.rs
+++ b/crates/jmap/src/blob/download.rs
@@ -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,
diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs
new file mode 100644
index 00000000..d3d65760
--- /dev/null
+++ b/crates/jmap/src/blob/get.rs
@@ -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)
+ }
+}
diff --git a/crates/jmap/src/blob/mod.rs b/crates/jmap/src/blob/mod.rs
index 98e52afe..56818560 100644
--- a/crates/jmap/src/blob/mod.rs
+++ b/crates/jmap/src/blob/mod.rs
@@ -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)]
diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs
index 52455130..69f7ef85 100644
--- a/crates/jmap/src/blob/upload.rs
+++ b/crates/jmap/src/blob/upload.rs
@@ -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,
diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs
index b5835b3d..4488716a 100644
--- a/crates/jmap/src/changes/get.rs
+++ b/crates/jmap/src/changes/get.rs
@@ -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![],
diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs
index 3971ce5e..cbeaf7ea 100644
--- a/crates/jmap/src/changes/query.rs
+++ b/crates/jmap/src/changes/query.rs
@@ -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!(),
};
diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs
index 5134ca62..65332801 100644
--- a/crates/jmap/src/email/copy.rs
+++ b/crates/jmap/src/email/copy.rs
@@ -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()
}
}
diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs
index aa3f70bf..40edb02b 100644
--- a/crates/jmap/src/email/get.rs
+++ b/crates/jmap/src/email/get.rs
@@ -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;
}
}
diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs
index 9c598763..5423d074 100644
--- a/crates/jmap/src/email/import.rs
+++ b/crates/jmap/src/email/import.rs
@@ -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()
}
}
diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs
index a24a9e0c..e31c80ff 100644
--- a/crates/jmap/src/email/set.rs
+++ b/crates/jmap/src/email/set.rs
@@ -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();
}
diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs
index e0174739..35f6ac9e 100644
--- a/crates/jmap/src/identity/get.rs
+++ b/crates/jmap/src/identity/get.rs
@@ -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());
diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs
index d99ef8bc..aa8564ec 100644
--- a/crates/jmap/src/lib.rs
+++ b/crates/jmap/src/lib.rs
@@ -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;
diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs
index a18b6474..985bfc44 100644
--- a/crates/jmap/src/mailbox/get.rs
+++ b/crates/jmap/src/mailbox/get.rs
@@ -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;
}
}
diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs
index 4070a8c6..9ab4412e 100644
--- a/crates/jmap/src/mailbox/set.rs
+++ b/crates/jmap/src/mailbox/set.rs
@@ -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
}
diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs
index e0e4a93a..95e52062 100644
--- a/crates/jmap/src/principal/get.rs
+++ b/crates/jmap/src/principal/get.rs
@@ -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;
};
diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs
index 77323630..f49c5606 100644
--- a/crates/jmap/src/push/get.rs
+++ b/crates/jmap/src/push/get.rs
@@ -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);
}
diff --git a/crates/jmap/src/push/mod.rs b/crates/jmap/src/push/mod.rs
index fb6afad8..ab1169e3 100644
--- a/crates/jmap/src/push/mod.rs
+++ b/crates/jmap/src/push/mod.rs
@@ -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>,
}
diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs
index 6a8ff557..0de17765 100644
--- a/crates/jmap/src/push/set.rs
+++ b/crates/jmap/src/push/set.rs
@@ -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()
}) =>
{
diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs
new file mode 100644
index 00000000..ad2d6690
--- /dev/null
+++ b/crates/jmap/src/quota/get.rs
@@ -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)
+ }
+}
diff --git a/crates/jmap/src/quota/mod.rs b/crates/jmap/src/quota/mod.rs
new file mode 100644
index 00000000..cc153667
--- /dev/null
+++ b/crates/jmap/src/quota/mod.rs
@@ -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;
diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs
new file mode 100644
index 00000000..48e8834f
--- /dev/null
+++ b/crates/jmap/src/quota/query.rs
@@ -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)
+ }*/
+ }
+}
diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs
index 566e4821..74847632 100644
--- a/crates/jmap/src/services/ingest.rs
+++ b/crates/jmap/src/services/ingest.rs
@@ -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;
}
diff --git a/crates/jmap/src/services/state.rs b/crates/jmap/src/services/state.rs
index d8fd4fef..b294c8cc 100644
--- a/crates/jmap/src/services/state.rs
+++ b/crates/jmap/src/services/state.rs
@@ -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();
diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs
index 330e78c8..d33f5fcf 100644
--- a/crates/jmap/src/sieve/get.rs
+++ b/crates/jmap/src/sieve/get.rs
@@ -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());
diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs
index 7fee2e18..eeae7daa 100644
--- a/crates/jmap/src/sieve/set.rs
+++ b/crates/jmap/src/sieve/set.rs
@@ -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(),
diff --git a/crates/jmap/src/submission/get.rs b/crates/jmap/src/submission/get.rs
index b6277fcd..5cca5b29 100644
--- a/crates/jmap/src/submission/get.rs
+++ b/crates/jmap/src/submission/get.rs
@@ -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;
};
diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs
index 53acac78..5b8cc1b1 100644
--- a/crates/jmap/src/thread/get.rs
+++ b/crates/jmap/src/thread/get.rs
@@ -92,7 +92,7 @@ impl JMAP {
}
response.list.push(thread);
} else {
- response.not_found.push(id);
+ response.not_found.push(id.into());
}
}
diff --git a/crates/jmap/src/vacation/get.rs b/crates/jmap/src/vacation/get.rs
index 26fccaa1..53cc8218 100644
--- a/crates/jmap/src/vacation/get.rs
+++ b/crates/jmap/src/vacation/get.rs
@@ -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());
}
}
diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs
index 37462d46..e3ed2c23 100644
--- a/crates/jmap/src/websocket/stream.rs
+++ b/crates/jmap/src/websocket/stream.rs
@@ -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! {
diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml
index 53f2c0f9..00f00d14 100644
--- a/crates/main/Cargo.toml
+++ b/crates/main/Cargo.toml
@@ -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"
diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml
index 5500ff67..ef5e04bc 100644
--- a/crates/managesieve/Cargo.toml
+++ b/crates/managesieve/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "managesieve"
-version = "0.4.0"
+version = "0.4.2"
edition = "2021"
resolver = "2"
diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs
index db37c0d1..b9af880d 100644
--- a/crates/managesieve/src/op/capability.rs
+++ b/crates/managesieve/src/op/capability.rs
@@ -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());
diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml
index e32041ba..d40d75bc 100644
--- a/crates/nlp/Cargo.toml
+++ b/crates/nlp/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "nlp"
-version = "0.4.0"
+version = "0.4.2"
edition = "2021"
resolver = "2"
diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml
index 3a7d156b..62a4fa46 100644
--- a/crates/smtp/Cargo.toml
+++ b/crates/smtp/Cargo.toml
@@ -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"
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
index d7b64cc8..75070c85 100644
--- a/crates/utils/Cargo.toml
+++ b/crates/utils/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "utils"
-version = "0.4.0"
+version = "0.4.2"
edition = "2021"
resolver = "2"
diff --git a/tests/src/jmap/blob.rs b/tests/src/jmap/blob.rs
new file mode 100644
index 00000000..15722a38
--- /dev/null
+++ b/tests/src/jmap/blob.rs
@@ -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;
+}
diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs
index 4f7c76d1..e980dfa7 100644
--- a/tests/src/jmap/mod.rs
+++ b/tests/src/jmap/mod.rs
@@ -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();
diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs
index aae18e76..e5662787 100644
--- a/tests/src/jmap/push_subscription.rs
+++ b/tests/src/jmap/push_subscription.rs
@@ -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>>()
);
}
diff --git a/tests/src/jmap/quota.rs b/tests/src/jmap/quota.rs
index 5ba41da5..23f53526 100644
--- a/tests/src/jmap/quota.rs
+++ b/tests/src/jmap/quota.rs
@@ -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();