From d9999a61fe73ddd69961f7f45c1f4fdf7aeecb7c Mon Sep 17 00:00:00 2001 From: mdecimus Date: Sat, 27 Sep 2025 20:02:16 +0200 Subject: [PATCH] JMAP protocol layer refactoring (part 5) --- Cargo.lock | 18 +- crates/jmap-proto/src/method/get.rs | 10 +- crates/jmap-proto/src/method/query_changes.rs | 18 +- crates/jmap-proto/src/object/blob.rs | 10 +- crates/jmap-proto/src/object/email.rs | 56 +- .../jmap-proto/src/object/email_submission.rs | 13 +- crates/jmap-proto/src/object/identity.rs | 10 +- crates/jmap-proto/src/object/mailbox.rs | 120 ++++- crates/jmap-proto/src/object/mod.rs | 100 +++- crates/jmap-proto/src/object/principal.rs | 26 +- .../src/object/push_subscription.rs | 10 +- crates/jmap-proto/src/object/quota.rs | 10 +- crates/jmap-proto/src/object/sieve.rs | 10 +- crates/jmap-proto/src/object/thread.rs | 10 +- .../src/object/vacation_response.rs | 13 +- crates/jmap-proto/src/request/mod.rs | 24 +- crates/jmap-proto/src/types/date.rs | 7 - crates/jmap/Cargo.toml | 6 +- crates/jmap/src/api/acl.rs | 65 +++ crates/jmap/src/api/auth.rs | 224 +++----- crates/jmap/src/api/event_source.rs | 10 +- crates/jmap/src/api/mod.rs | 6 +- crates/jmap/src/api/request.rs | 188 +++---- crates/jmap/src/blob/copy.rs | 16 +- crates/jmap/src/blob/get.rs | 59 +-- crates/jmap/src/blob/upload.rs | 22 +- crates/jmap/src/changes/get.rs | 145 +++--- crates/jmap/src/changes/query.rs | 258 +++++---- crates/jmap/src/email/body.rs | 108 ++-- crates/jmap/src/email/copy.rs | 41 +- crates/jmap/src/email/get.rs | 270 +++++----- crates/jmap/src/email/headers.rs | 294 +++++++---- crates/jmap/src/email/import.rs | 6 +- crates/jmap/src/email/mod.rs | 20 +- crates/jmap/src/email/parse.rs | 153 +++--- crates/jmap/src/email/query.rs | 488 +++++++++--------- crates/jmap/src/email/set.rs | 95 ++-- crates/jmap/src/identity/get.rs | 99 ++-- crates/jmap/src/identity/set.rs | 42 +- crates/jmap/src/lib.rs | 15 +- crates/jmap/src/mailbox/get.rs | 140 ++--- crates/jmap/src/mailbox/query.rs | 223 ++++---- crates/jmap/src/mailbox/set.rs | 48 +- crates/jmap/src/principal/get.rs | 54 +- crates/jmap/src/principal/query.rs | 80 +-- crates/jmap/src/push/get.rs | 67 ++- crates/jmap/src/push/set.rs | 42 +- crates/jmap/src/quota/get.rs | 64 ++- crates/jmap/src/quota/query.rs | 73 +-- crates/jmap/src/quota/set.rs | 23 - crates/jmap/src/sieve/get.rs | 50 +- crates/jmap/src/sieve/query.rs | 46 +- crates/jmap/src/sieve/set.rs | 38 +- crates/jmap/src/sieve/validate.rs | 29 +- crates/jmap/src/submission/get.rs | 180 ++++--- crates/jmap/src/submission/query.rs | 124 ++--- crates/jmap/src/submission/set.rs | 62 +-- crates/jmap/src/thread/get.rs | 30 +- crates/jmap/src/vacation/get.rs | 103 ++-- crates/jmap/src/vacation/set.rs | 28 +- crates/types/Cargo.toml | 1 + crates/types/src/blob.rs | 7 + crates/types/src/id.rs | 7 + crates/types/src/keyword.rs | 8 + crates/types/src/special_use.rs | 7 + crates/types/src/type_state.rs | 7 + 66 files changed, 2562 insertions(+), 2074 deletions(-) create mode 100644 crates/jmap/src/api/acl.rs delete mode 100644 crates/jmap/src/quota/set.rs diff --git a/Cargo.lock b/Cargo.lock index 2847910f..1284bdac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3845,11 +3845,13 @@ dependencies = [ "directory", "email", "futures-util", + "hashify", "hkdf", "http-body-util", "http_proto", "hyper 1.7.0", "hyper-util", + "jmap-tools", "jmap_proto", "lz4_flex", "mail-auth", @@ -3874,9 +3876,9 @@ dependencies = [ "spam-filter", "store", "tokio", - "tokio-tungstenite 0.27.0", + "tokio-tungstenite 0.28.0", "trc", - "tungstenite 0.27.0", + "tungstenite 0.28.0", "types", "utils", ] @@ -3908,6 +3910,7 @@ name = "jmap-tools" version = "0.1.1" dependencies = [ "hashify", + "rkyv", "serde", "serde_json", ] @@ -8340,14 +8343,14 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.27.0", + "tungstenite 0.28.0", ] [[package]] @@ -8650,9 +8653,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -8693,6 +8696,7 @@ dependencies = [ "blake3", "compact_str", "hashify", + "jmap-tools", "rkyv", "serde", "trc", diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs index 0ebd947b..f5b46f92 100644 --- a/crates/jmap-proto/src/method/get.rs +++ b/crates/jmap-proto/src/method/get.rs @@ -7,7 +7,7 @@ use crate::{ object::JmapObject, request::{ - MaybeInvalid, + IntoValid, MaybeInvalid, deserialize::{DeserializeArguments, deserialize_request}, reference::{MaybeIdReference, MaybeResultReference, ResultReference}, }, @@ -37,7 +37,7 @@ pub struct GetResponse { pub list: Vec>, #[serde(rename = "notFound")] - pub not_found: Vec>, + pub not_found: Vec, } impl<'de, T: JmapObject> DeserializeArguments<'de> for GetRequest { @@ -120,11 +120,7 @@ impl GetRequest { 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()) - .collect::>(), - )) + Ok(Some(ids.into_valid().collect::>())) } else { Err(trc::JmapEvent::RequestTooLarge.into_err()) } diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs index 091b409f..fba2fd51 100644 --- a/crates/jmap-proto/src/method/query_changes.rs +++ b/crates/jmap-proto/src/method/query_changes.rs @@ -5,7 +5,7 @@ */ use crate::{ - method::query::{Comparator, Filter, FilterWrapper}, + method::query::{Comparator, Filter, FilterWrapper, QueryRequest}, object::JmapObject, request::deserialize::{DeserializeArguments, deserialize_request}, types::state::State, @@ -118,3 +118,19 @@ impl Default for QueryChangesRequest { } } } + +impl From> for QueryRequest { + fn from(request: QueryChangesRequest) -> Self { + QueryRequest { + account_id: request.account_id, + filter: request.filter, + sort: request.sort, + position: None, + anchor: None, + anchor_offset: None, + limit: None, + calculate_total: request.calculate_total, + arguments: T::QueryArguments::default(), + } + } +} diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index ccfbeb40..dd6f6d4a 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -8,7 +8,7 @@ use crate::{ object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::deserialize::DeserializeArguments, }; -use jmap_tools::{Element, Key, Property}; +use jmap_tools::{Element, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::{blob::BlobId, id::Id}; @@ -171,6 +171,8 @@ impl JmapObject for Blob { type Id = BlobId; + type Right = Null; + type Filter = (); type Comparator = (); @@ -192,6 +194,12 @@ impl From for BlobValue { } } +impl From for BlobProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} + impl JmapObjectId for BlobValue { fn as_id(&self) -> Option { None diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index d68fe1fb..8e192a99 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -5,11 +5,12 @@ */ use crate::{ + method::query::{Comparator, Filter}, object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::{MaybeInvalid, deserialize::DeserializeArguments}, types::date::UTCDate, }; -use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Null, Property}; use mail_parser::HeaderName; use std::{borrow::Cow, fmt::Display, str::FromStr}; use store::fts::{FilterItem, FilterType}; @@ -75,6 +76,7 @@ pub enum EmailProperty { // Other Keyword(Keyword), + IdValue(Id), Pointer(JsonPointer), } @@ -85,7 +87,7 @@ pub struct HeaderProperty { pub all: bool, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum HeaderForm { Raw, Text, @@ -161,6 +163,7 @@ impl Property for EmailProperty { EmailProperty::IsTruncated => "isTruncated", EmailProperty::Header(header) => return header.to_string().into(), EmailProperty::Keyword(keyword) => return keyword.to_string().into(), + EmailProperty::IdValue(id) => return id.to_string().into(), EmailProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), } .into() @@ -439,6 +442,8 @@ impl JmapObject for Email { type Id = Id; + type Right = Null; + type Filter = EmailFilter; type Comparator = EmailComparator; @@ -447,7 +452,7 @@ impl JmapObject for Email { type SetArguments = (); - type QueryArguments = (); + type QueryArguments = EmailQueryArguments; type CopyArguments = (); @@ -732,6 +737,15 @@ impl Display for EmailComparator { } } +impl Filter { + pub fn is_immutable(&self) -> bool { + match self { + Filter::Property(f) => f.is_immutable(), + Filter::And | Filter::Or | Filter::Not | Filter::Close => true, + } + } +} + impl EmailFilter { pub fn is_immutable(&self) -> bool { matches!( @@ -756,6 +770,12 @@ impl EmailFilter { } } +impl Comparator { + pub fn is_immutable(&self) -> bool { + self.property.is_immutable() + } +} + impl EmailComparator { pub fn is_immutable(&self) -> bool { matches!( @@ -787,12 +807,6 @@ impl FilterItem for EmailFilter { } } -impl From for EmailValue { - fn from(id: Id) -> Self { - EmailValue::Id(id) - } -} - impl JmapObjectId for EmailValue { fn as_id(&self) -> Option { if let EmailValue::Id(id) = self { @@ -829,3 +843,27 @@ impl TryFrom for EmailValue { } } } + +impl From for EmailValue { + fn from(id: Id) -> Self { + EmailValue::Id(id) + } +} + +impl From for EmailValue { + fn from(id: BlobId) -> Self { + EmailValue::BlobId(id) + } +} + +impl From for EmailValue { + fn from(date: UTCDate) -> Self { + EmailValue::Date(date) + } +} + +impl From for EmailProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 54b847a5..3078f357 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -13,7 +13,7 @@ use crate::{ request::{deserialize::DeserializeArguments, reference::MaybeIdReference}, types::date::UTCDate, }; -use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property, Value}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Null, Property, Value}; use std::{borrow::Cow, str::FromStr}; use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; @@ -26,6 +26,7 @@ pub enum EmailSubmissionProperty { Id, IdentityId, ThreadId, + EmailId, Envelope, MailFrom, RcptTo, @@ -98,6 +99,7 @@ impl Property for EmailSubmissionProperty { EmailSubmissionProperty::Displayed => "displayed", EmailSubmissionProperty::MailFrom => "mailFrom", EmailSubmissionProperty::RcptTo => "rcptTo", + EmailSubmissionProperty::EmailId => "emailId", EmailSubmissionProperty::Pointer(json_pointer) => { return json_pointer.to_string().into(); } @@ -164,6 +166,7 @@ impl EmailSubmissionProperty { "id" => EmailSubmissionProperty::Id, "identityId" => EmailSubmissionProperty::IdentityId, "threadId" => EmailSubmissionProperty::ThreadId, + "emailId" => EmailSubmissionProperty::EmailId, "envelope" => EmailSubmissionProperty::Envelope, "mailFrom" => EmailSubmissionProperty::MailFrom, "rcptTo" => EmailSubmissionProperty::RcptTo, @@ -314,6 +317,8 @@ impl JmapObject for EmailSubmission { type Id = Id; + type Right = Null; + type Filter = EmailSubmissionFilter; type Comparator = EmailSubmissionComparator; @@ -465,3 +470,9 @@ impl TryFrom for EmailSubmissionValue { } } } + +impl From for EmailSubmissionProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs index 9597866c..84bc93fb 100644 --- a/crates/jmap-proto/src/object/identity.rs +++ b/crates/jmap-proto/src/object/identity.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; @@ -131,6 +131,8 @@ impl JmapObject for Identity { type Id = Id; + type Right = Null; + type Filter = (); type Comparator = (); @@ -180,3 +182,9 @@ impl TryFrom for IdentityValue { } } } + +impl From for IdentityProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index 00f9f5ce..a430cb8a 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -7,10 +7,10 @@ use std::{borrow::Cow, str::FromStr}; use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; -use types::{id::Id, special_use::SpecialUse}; +use types::{acl::Acl, id::Id, special_use::SpecialUse}; use crate::{ - object::{AnyId, JmapObject, JmapObjectId, MaybeReference, parse_ref}, + object::{AnyId, JmapObject, JmapObjectId, JmapRight, MaybeReference, parse_ref}, request::deserialize::DeserializeArguments, }; @@ -30,6 +30,16 @@ pub enum MailboxProperty { UnreadThreads, ShareWith, MyRights, + IsSubscribed, + + // Other + IdValue(Id), + Rights(MailboxRight), + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MailboxRight { MayReadItems, MayAddItems, MayRemoveItems, @@ -38,10 +48,7 @@ pub enum MailboxProperty { MayCreateChild, MayRename, MaySubmit, - IsSubscribed, - - // Other - Pointer(JsonPointer), + MayDelete, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -53,7 +60,11 @@ pub enum MailboxValue { impl Property for MailboxProperty { fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { - MailboxProperty::parse(value, key.is_none()) + if let Some(Key::Property(MailboxProperty::ShareWith)) = key { + Id::from_str(value).ok().map(MailboxProperty::IdValue) + } else { + MailboxProperty::parse(value, key.is_none()) + } } fn to_cow(&self) -> Cow<'static, str> { @@ -69,21 +80,31 @@ impl Property for MailboxProperty { MailboxProperty::TotalThreads => "totalThreads", MailboxProperty::UnreadEmails => "unreadEmails", MailboxProperty::UnreadThreads => "unreadThreads", - MailboxProperty::MayReadItems => "mayReadItems", - MailboxProperty::MayAddItems => "mayAddItems", - MailboxProperty::MayRemoveItems => "mayRemoveItems", - MailboxProperty::MaySetSeen => "maySetSeen", - MailboxProperty::MaySetKeywords => "maySetKeywords", - MailboxProperty::MayCreateChild => "mayCreateChild", - MailboxProperty::MayRename => "mayRename", - MailboxProperty::MaySubmit => "maySubmit", MailboxProperty::ShareWith => "shareWith", + MailboxProperty::Rights(mailbox_right) => mailbox_right.as_str(), MailboxProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + MailboxProperty::IdValue(id) => return id.to_string().into(), } .into() } } +impl MailboxRight { + pub fn as_str(&self) -> &'static str { + match self { + MailboxRight::MayReadItems => "mayReadItems", + MailboxRight::MayAddItems => "mayAddItems", + MailboxRight::MayRemoveItems => "mayRemoveItems", + MailboxRight::MaySetSeen => "maySetSeen", + MailboxRight::MaySetKeywords => "maySetKeywords", + MailboxRight::MayCreateChild => "mayCreateChild", + MailboxRight::MayRename => "mayRename", + MailboxRight::MaySubmit => "maySubmit", + MailboxRight::MayDelete => "mayDelete", + } + } +} + impl Element for MailboxValue { type Property = MailboxProperty; @@ -126,14 +147,15 @@ impl MailboxProperty { b"unreadThreads" => MailboxProperty::UnreadThreads, b"shareWith" => MailboxProperty::ShareWith, b"myRights" => MailboxProperty::MyRights, - b"mayReadItems" => MailboxProperty::MayReadItems, - b"mayAddItems" => MailboxProperty::MayAddItems, - b"mayRemoveItems" => MailboxProperty::MayRemoveItems, - b"maySetSeen" => MailboxProperty::MaySetSeen, - b"maySetKeywords" => MailboxProperty::MaySetKeywords, - b"mayCreateChild" => MailboxProperty::MayCreateChild, - b"mayRename" => MailboxProperty::MayRename, - b"maySubmit" => MailboxProperty::MaySubmit, + b"mayReadItems" => MailboxProperty::Rights(MailboxRight::MayReadItems), + b"mayAddItems" => MailboxProperty::Rights(MailboxRight::MayAddItems), + b"mayRemoveItems" => MailboxProperty::Rights(MailboxRight::MayRemoveItems), + b"maySetSeen" => MailboxProperty::Rights(MailboxRight::MaySetSeen), + b"maySetKeywords" => MailboxProperty::Rights(MailboxRight::MaySetKeywords), + b"mayCreateChild" => MailboxProperty::Rights(MailboxRight::MayCreateChild), + b"mayRename" => MailboxProperty::Rights(MailboxRight::MayRename), + b"maySubmit" => MailboxProperty::Rights(MailboxRight::MaySubmit), + b"mayDelete" => MailboxProperty::Rights(MailboxRight::MayDelete), b"isSubscribed" => MailboxProperty::IsSubscribed, ) .or_else(|| { @@ -227,6 +249,8 @@ impl JmapObject for Mailbox { type Id = Id; + type Right = MailboxRight; + type Filter = MailboxFilter; type Comparator = MailboxComparator; @@ -388,3 +412,53 @@ impl TryFrom for MailboxValue { } } } + +impl JmapRight for MailboxRight { + fn from_acl(acl: Acl) -> &'static [Self] { + match acl { + Acl::ReadItems => &[MailboxRight::MayReadItems], + Acl::AddItems => &[MailboxRight::MayAddItems], + Acl::RemoveItems => &[MailboxRight::MayRemoveItems], + Acl::ModifyItems => &[MailboxRight::MaySetSeen, MailboxRight::MaySetKeywords], + Acl::CreateChild => &[MailboxRight::MayCreateChild], + Acl::Modify => &[MailboxRight::MayRename], + Acl::Submit => &[MailboxRight::MaySubmit], + Acl::Delete => &[MailboxRight::MayDelete], + _ => &[], + } + } + + fn to_acl(&self) -> Acl { + match self { + MailboxRight::MayReadItems => Acl::ReadItems, + MailboxRight::MayAddItems => Acl::AddItems, + MailboxRight::MayRemoveItems => Acl::RemoveItems, + MailboxRight::MaySetSeen => Acl::ModifyItems, + MailboxRight::MaySetKeywords => Acl::ModifyItems, + MailboxRight::MayCreateChild => Acl::CreateChild, + MailboxRight::MayRename => Acl::Modify, + MailboxRight::MaySubmit => Acl::Submit, + MailboxRight::MayDelete => Acl::Delete, + } + } + + fn all_rights() -> &'static [Self] { + &[ + MailboxRight::MayReadItems, + MailboxRight::MayAddItems, + MailboxRight::MayRemoveItems, + MailboxRight::MaySetSeen, + MailboxRight::MaySetKeywords, + MailboxRight::MayCreateChild, + MailboxRight::MayRename, + MailboxRight::MaySubmit, + MailboxRight::MayDelete, + ] + } +} + +impl From for MailboxProperty { + fn from(right: MailboxRight) -> Self { + MailboxProperty::Rights(right) + } +} diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index c852ce87..5a6d6f41 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -5,10 +5,10 @@ */ use crate::request::deserialize::DeserializeArguments; -use jmap_tools::{Element, Property}; +use jmap_tools::{Element, Null, Property}; use serde::Serialize; use std::{fmt::Debug, str::FromStr}; -use types::{blob::BlobId, id::Id}; +use types::{acl::Acl, blob::BlobId, id::Id}; pub mod blob; pub mod email; @@ -24,21 +24,34 @@ pub mod thread; pub mod vacation_response; pub trait JmapObject: std::fmt::Debug { - type Property: Property + FromStr + Serialize + Debug; - type Element: Element + From + JmapObjectId + Debug; - type Id: FromStr + TryFrom + Serialize + Debug; + type Property: Property + FromStr + Serialize + Debug + Sync + Send; + type Element: Element + + From + + JmapObjectId + + Debug + + Sync + + Send; + type Id: FromStr + TryFrom + Serialize + Debug + Sync + Send; - type Filter: Default + for<'de> DeserializeArguments<'de> + Debug; - type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug; + type Filter: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; + type Comparator: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; - type GetArguments: Default + for<'de> DeserializeArguments<'de> + Debug; - type SetArguments: Default + for<'de> DeserializeArguments<'de> + Debug; - type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug; - type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug; + type Right: JmapRight + Into + Debug + Sync + Send; + + type GetArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; + type SetArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; + type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; + type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; const ID_PROPERTY: Self::Property; } +pub trait JmapRight: Clone + Copy + Sized + 'static { + fn from_acl(acl: Acl) -> &'static [Self]; + fn all_rights() -> &'static [Self]; + fn to_acl(&self) -> Acl; +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(untagged)] pub enum AnyId { @@ -123,3 +136,68 @@ impl<'de> serde::Deserialize<'de> for AnyId { } } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] +pub struct NullObject; + +impl JmapObject for NullObject { + type Property = Null; + type Element = Null; + type Id = Null; + + type Filter = (); + type Comparator = (); + + type Right = Null; + + type GetArguments = (); + type SetArguments = (); + type QueryArguments = (); + type CopyArguments = (); + + const ID_PROPERTY: Self::Property = Null; +} + +impl JmapRight for Null { + fn from_acl(_: Acl) -> &'static [Self] { + unreachable!() + } + + fn all_rights() -> &'static [Self] { + unreachable!() + } + + fn to_acl(&self) -> Acl { + unreachable!() + } +} + +impl FromStr for NullObject { + type Err = (); + + fn from_str(_: &str) -> Result { + unreachable!() + } +} + +impl JmapObjectId for Null { + fn as_id(&self) -> Option { + unreachable!() + } + + fn as_any_id(&self) -> Option { + unreachable!() + } + + fn as_id_ref(&self) -> Option<&str> { + unreachable!() + } +} + +impl TryFrom for Null { + type Error = (); + + fn try_from(_: AnyId) -> Result { + unreachable!() + } +} diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index d2a96c3d..ec546b5e 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use jmap_tools::{Element, Key, Property}; -use std::{borrow::Cow, str::FromStr}; +use jmap_tools::{Element, Key, Null, Property}; +use std::{borrow::Cow, fmt::Display, str::FromStr}; use types::id::Id; use crate::{ @@ -144,6 +144,8 @@ impl JmapObject for Principal { type Id = Id; + type Right = Null; + type Filter = PrincipalFilter; type Comparator = PrincipalComparator; @@ -302,3 +304,23 @@ impl TryFrom for PrincipalValue { } } } + +impl Display for PrincipalFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + PrincipalFilter::AccountIds(_) => "accountIds", + PrincipalFilter::Email(_) => "email", + PrincipalFilter::Name(_) => "name", + PrincipalFilter::Text(_) => "text", + PrincipalFilter::Type(_) => "type", + PrincipalFilter::Timezone(_) => "timezone", + PrincipalFilter::_T(other) => other, + }) + } +} + +impl From for PrincipalProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs index 4f05ffb9..31cc29ad 100644 --- a/crates/jmap-proto/src/object/push_subscription.rs +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -6,7 +6,7 @@ use crate::object::{AnyId, JmapObject, JmapObjectId}; use crate::types::date::UTCDate; -use jmap_tools::{Element, JsonPointer, JsonPointerItem}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Null}; use jmap_tools::{Key, Property}; use std::borrow::Cow; use std::str::FromStr; @@ -150,6 +150,8 @@ impl JmapObject for PushSubscription { type Id = Id; + type Right = Null; + type Filter = (); type Comparator = (); @@ -201,3 +203,9 @@ impl TryFrom for PushSubscriptionValue { } } } + +impl From for PushSubscriptionProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs index ee552159..5bc9f9df 100644 --- a/crates/jmap-proto/src/object/quota.rs +++ b/crates/jmap-proto/src/object/quota.rs @@ -8,7 +8,7 @@ use crate::{ object::{AnyId, JmapObject, JmapObjectId}, request::deserialize::DeserializeArguments, }; -use jmap_tools::{Element, Key, Property}; +use jmap_tools::{Element, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::{id::Id, type_state::DataType}; @@ -121,6 +121,8 @@ impl JmapObject for Quota { type Id = Id; + type Right = Null; + type Filter = QuotaFilter; type Comparator = QuotaComparator; @@ -257,3 +259,9 @@ impl TryFrom for QuotaValue { } } } + +impl From for QuotaProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index 04305106..4ab3db90 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -8,7 +8,7 @@ use crate::{ object::{AnyId, DeserializeArguments, JmapObject, JmapObjectId, MaybeReference, parse_ref}, request::reference::MaybeIdReference, }; -use jmap_tools::{Element, Key, Property}; +use jmap_tools::{Element, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::{blob::BlobId, id::Id}; @@ -140,6 +140,8 @@ impl JmapObject for Sieve { type Id = Id; + type Right = Null; + type Filter = SieveFilter; type Comparator = SieveComparator; @@ -270,3 +272,9 @@ impl TryFrom for SieveValue { } } } + +impl From for SieveProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs index 345cb4a4..4414de08 100644 --- a/crates/jmap-proto/src/object/thread.rs +++ b/crates/jmap-proto/src/object/thread.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use jmap_tools::{Element, Key, Property}; +use jmap_tools::{Element, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; @@ -89,6 +89,8 @@ impl JmapObject for Thread { type Id = Id; + type Right = Null; + type Filter = (); type Comparator = (); @@ -136,3 +138,9 @@ impl TryFrom for ThreadValue { } } } + +impl From for ThreadProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs index 19c5fb4f..17d7a88d 100644 --- a/crates/jmap-proto/src/object/vacation_response.rs +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -8,7 +8,7 @@ use crate::{ object::{AnyId, JmapObject, JmapObjectId}, types::date::UTCDate, }; -use jmap_tools::{Element, Key, Property}; +use jmap_tools::{Element, Key, Null, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; @@ -21,6 +21,7 @@ pub enum VacationResponseProperty { IsEnabled, FromDate, ToDate, + Subject, TextBody, HtmlBody, } @@ -44,6 +45,7 @@ impl Property for VacationResponseProperty { VacationResponseProperty::FromDate => "fromDate", VacationResponseProperty::IsEnabled => "isEnabled", VacationResponseProperty::ToDate => "toDate", + VacationResponseProperty::Subject => "subject", } .into() } @@ -87,6 +89,7 @@ impl VacationResponseProperty { b"toDate" => VacationResponseProperty::ToDate, b"textBody" => VacationResponseProperty::TextBody, b"htmlBody" => VacationResponseProperty::HtmlBody, + b"subject" => VacationResponseProperty::Subject, ) } } @@ -115,6 +118,8 @@ impl JmapObject for VacationResponse { type Id = Id; + type Right = Null; + type Filter = (); type Comparator = (); @@ -136,6 +141,12 @@ impl From for VacationResponseValue { } } +impl From for VacationResponseProperty { + fn from(_: Null) -> Self { + unimplemented!() + } +} + impl JmapObjectId for VacationResponseValue { fn as_id(&self) -> Option { match self { diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index dc684b29..571c5bec 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -32,7 +32,7 @@ use crate::{ mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, sieve::Sieve, thread::Thread, vacation_response::VacationResponse, }, - request::capability::CapabilityIds, + request::{capability::CapabilityIds, reference::MaybeIdReference}, }; use jmap_tools::{Null, Value}; use std::{collections::HashMap, fmt::Debug, str::FromStr}; @@ -182,3 +182,25 @@ where } } } + +pub trait IntoValid { + type Item; + + fn into_valid(self) -> impl Iterator; +} + +impl IntoValid for Vec> { + type Item = T; + + fn into_valid(self) -> impl Iterator { + self.into_iter().filter_map(|v| v.try_unwrap()) + } +} + +impl IntoValid for Vec> { + type Item = T; + + fn into_valid(self) -> impl Iterator { + self.into_iter().filter_map(|v| v.try_unwrap()) + } +} diff --git a/crates/jmap-proto/src/types/date.rs b/crates/jmap-proto/src/types/date.rs index 5d5ebd75..fdd0bc2c 100644 --- a/crates/jmap-proto/src/types/date.rs +++ b/crates/jmap-proto/src/types/date.rs @@ -5,7 +5,6 @@ */ use std::{fmt::Display, str::FromStr}; -use store::SerializeInfallible; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct UTCDate { @@ -226,12 +225,6 @@ impl<'de> serde::Deserialize<'de> for UTCDate { } } -impl SerializeInfallible for UTCDate { - fn serialize(&self) -> Vec { - (self.timestamp() as u64).serialize() - } -} - impl From for u64 { fn from(value: UTCDate) -> Self { value.timestamp() as u64 diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 461ed2c0..fa0456cd 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -24,6 +24,7 @@ mail-builder = { version = "0.4" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } mail-auth = { version = "0.7.1", features = ["generate"] } sieve-rs = { version = "0.7", features = ["rkyv"] } +jmap-tools = { path = "/Users/me/code/jmap-tool", features = ["rkyv"] } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } @@ -38,8 +39,8 @@ hkdf = "0.12.3" sha1 = "0.10" sha2 = "0.10" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} -tokio-tungstenite = "0.27" -tungstenite = "0.27" +tokio-tungstenite = "0.28" +tungstenite = "0.28" chrono = "0.4" rand = "0.9.0" pkcs8 = { version = "0.10.2", features = ["alloc", "std"] } @@ -49,6 +50,7 @@ aes-gcm-siv = "0.11.1" rsa = "0.9.2" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" +hashify = "0.2" [features] test_mode = [] diff --git a/crates/jmap/src/api/acl.rs b/crates/jmap/src/api/acl.rs new file mode 100644 index 00000000..e69369ab --- /dev/null +++ b/crates/jmap/src/api/acl.rs @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::auth::AccessToken; +use jmap_proto::object::{JmapObject, JmapRight}; +use jmap_tools::{Key, Map, Value}; +use types::{ + acl::{Acl, AclGrant}, + id::Id, +}; +use utils::map::bitmap::Bitmap; + +pub struct JmapRights; + +impl JmapRights { + pub fn all_rights() -> Value<'static, T::Property, T::Element> { + let rights = T::Right::all_rights(); + let mut obj = Map::with_capacity(rights.len()); + + for right in rights { + obj.insert_unchecked(Key::Property((*right).into()), Value::Bool(true)); + } + + Value::Object(obj) + } + + pub fn rights(acls: Bitmap) -> Value<'static, T::Property, T::Element> { + let mut obj = Map::with_capacity(3); + + for acl in acls.into_iter() { + for right in T::Right::from_acl(acl) { + obj.insert_unchecked(Key::Property((*right).into()), Value::Bool(true)); + } + } + + Value::Object(obj) + } + + pub fn share_with( + account_id: u32, + access_token: &AccessToken, + grants: &[AclGrant], + ) -> Value<'static, T::Property, T::Element> { + if access_token.is_member(account_id) + || grants.iter().any(|item| { + access_token.is_member(item.account_id) && item.grants.contains(Acl::Administer) + }) + { + let mut share_with = Map::with_capacity(grants.len()); + for grant in grants { + share_with.insert_unchecked( + Key::Owned(Id::from(grant.account_id).to_string()), + Self::rights::(grant.grants), + ); + } + + Value::Object(share_with) + } else { + Value::Null + } + } +} diff --git a/crates/jmap/src/api/auth.rs b/crates/jmap/src/api/auth.rs index b365526e..fb2ed15a 100644 --- a/crates/jmap/src/api/auth.rs +++ b/crates/jmap/src/api/auth.rs @@ -2,10 +2,9 @@ use common::{Server, auth::AccessToken}; use directory::{Permission, QueryParams, backend::internal::manage::ManageDirectory}; use jmap_proto::{ error::set::SetError, - request::RequestMethod, - types::{ - property::Property, - value::{MaybePatchValue, Value}, + request::{ + CopyRequestMethod, GetRequestMethod, QueryChangesRequestMethod, QueryRequestMethod, + RequestMethod, SetRequestMethod, method::MethodObject, }, }; use types::{ @@ -16,12 +15,6 @@ use types::{ use utils::map::bitmap::Bitmap; pub trait JmapAcl { - fn acl_get( - &self, - value: &[AclGrant], - access_token: &AccessToken, - account_id: u32, - ) -> impl Future + Send; fn acl_set( &self, changes: &mut Vec, @@ -40,46 +33,16 @@ pub trait JmapAcl { pub trait JmapAuthorization { fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self>; - fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()>; + fn assert_has_jmap_permission( + &self, + request: &RequestMethod, + object: MethodObject, + ) -> trc::Result<()>; fn assert_has_access(&self, to_account_id: Id, to_collection: Collection) -> trc::Result<&Self>; } impl JmapAcl for Server { - async fn acl_get( - &self, - value: &[AclGrant], - access_token: &AccessToken, - account_id: u32, - ) -> Value { - if access_token.is_member(account_id) - || value.iter().any(|item| { - access_token.is_member(item.account_id) && item.grants.contains(Acl::Administer) - }) - { - let mut acl_obj = jmap_proto::types::value::Object::with_capacity(value.len() / 2); - for item in value { - if let Some(name) = self - .store() - .get_principal(item.account_id) - .await - .unwrap_or_default() - { - acl_obj.append( - Property::_T(name.name), - item.grants - .map(|acl_item| Value::Text(acl_item.to_string())) - .collect::>(), - ); - } - } - - Value::Object(acl_obj) - } else { - Value::Null - } - } - async fn acl_set( &self, changes: &mut Vec, @@ -87,7 +50,7 @@ impl JmapAcl for Server { acl_changes: MaybePatchValue, ) -> Result<(), SetError> { match acl_changes { - MaybePatchValue::Value(Value::List(values)) => { + MaybePatchValue::Value(Value::Array(values)) => { *changes = self.map_acl_set(values).await?; } MaybePatchValue::Patch(patch) => { @@ -130,7 +93,7 @@ impl JmapAcl for Server { } _ => { return Err(SetError::invalid_properties() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description("Invalid ACL property.")); } } @@ -140,7 +103,7 @@ impl JmapAcl for Server { async fn map_acl_set(&self, acl_set: Vec) -> Result, SetError> { let mut acls = Vec::with_capacity(acl_set.len() / 2); for item in acl_set.chunks_exact(2) { - if let (Value::Text(account_name), Value::UnsignedInt(grants)) = (&item[0], &item[1]) { + if let (Value::Str(account_name), Value::Number(grants)) = (&item[0], &item[1]) { match self .core .storage @@ -156,18 +119,18 @@ impl JmapAcl for Server { } Ok(None) => { return Err(SetError::invalid_properties() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description(format!("Account {account_name} does not exist."))); } _ => { return Err(SetError::forbidden() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description("Temporary server failure during lookup")); } } } else { return Err(SetError::invalid_properties() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description("Invalid ACL value found.")); } } @@ -179,9 +142,7 @@ impl JmapAcl for Server { &self, acl_patch: Vec, ) -> Result<(AclGrant, Option), SetError> { - if let (Value::Text(account_name), Value::UnsignedInt(grants)) = - (&acl_patch[0], &acl_patch[1]) - { + if let (Value::Str(account_name), Value::Number(grants)) = (&acl_patch[0], &acl_patch[1]) { match self .core .storage @@ -197,15 +158,15 @@ impl JmapAcl for Server { acl_patch.get(2).map(|v| v.as_bool().unwrap_or(false)), )), Ok(None) => Err(SetError::invalid_properties() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description(format!("Account {account_name} does not exist."))), _ => Err(SetError::forbidden() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description("Temporary server failure during lookup")), } } else { Err(SetError::invalid_properties() - .with_property(Property::Acl) + .with_key_value(Property::Acl) .with_description("Invalid ACL value found.")) } } @@ -237,109 +198,72 @@ impl JmapAuthorization for AccessToken { } } - fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> { + fn assert_has_jmap_permission( + &self, + request: &RequestMethod, + object: MethodObject, + ) -> trc::Result<()> { let permission = match request { - RequestMethod::Get(m) => match &m.arguments { - jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet, - jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet, - jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet, - jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet, - jmap_proto::method::get::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionGet - } - jmap_proto::method::get::RequestArguments::PushSubscription => { - Permission::JmapPushSubscriptionGet - } - jmap_proto::method::get::RequestArguments::SieveScript => { - Permission::JmapSieveScriptGet - } - jmap_proto::method::get::RequestArguments::VacationResponse => { - Permission::JmapVacationResponseGet - } - jmap_proto::method::get::RequestArguments::Principal => { - Permission::JmapPrincipalGet - } - jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet, - jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet, + RequestMethod::Get(m) => match &m { + GetRequestMethod::Email(_) => Permission::JmapEmailGet, + GetRequestMethod::Mailbox(_) => Permission::JmapMailboxGet, + GetRequestMethod::Thread(_) => Permission::JmapThreadGet, + GetRequestMethod::Identity(_) => Permission::JmapIdentityGet, + GetRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionGet, + GetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionGet, + GetRequestMethod::Sieve(_) => Permission::JmapSieveScriptGet, + GetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseGet, + GetRequestMethod::Principal(_) => Permission::JmapPrincipalGet, + GetRequestMethod::Quota(_) => Permission::JmapQuotaGet, + GetRequestMethod::Blob(_) => Permission::JmapBlobGet, }, - RequestMethod::Set(m) => match &m.arguments { - jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet, - jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet, - jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet, - jmap_proto::method::set::RequestArguments::EmailSubmission(_) => { - Permission::JmapEmailSubmissionSet - } - jmap_proto::method::set::RequestArguments::PushSubscription => { - Permission::JmapPushSubscriptionSet - } - jmap_proto::method::set::RequestArguments::SieveScript(_) => { - Permission::JmapSieveScriptSet - } - jmap_proto::method::set::RequestArguments::VacationResponse => { - Permission::JmapVacationResponseSet - } + RequestMethod::Set(m) => match &m { + SetRequestMethod::Email(_) => Permission::JmapEmailSet, + SetRequestMethod::Mailbox(_) => Permission::JmapMailboxSet, + SetRequestMethod::Identity(_) => Permission::JmapIdentitySet, + SetRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionSet, + SetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionSet, + SetRequestMethod::Sieve(_) => Permission::JmapSieveScriptSet, + SetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseSet, }, - RequestMethod::Changes(m) => match m.arguments { - jmap_proto::method::changes::RequestArguments::Email => { - Permission::JmapEmailChanges - } - jmap_proto::method::changes::RequestArguments::Mailbox => { - Permission::JmapMailboxChanges - } - jmap_proto::method::changes::RequestArguments::Thread => { - Permission::JmapThreadChanges - } - jmap_proto::method::changes::RequestArguments::Identity => { - Permission::JmapIdentityChanges - } - jmap_proto::method::changes::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionChanges - } - jmap_proto::method::changes::RequestArguments::Quota => { - Permission::JmapQuotaChanges - } + RequestMethod::Changes(_) => match object { + MethodObject::Email => Permission::JmapEmailChanges, + MethodObject::Mailbox => Permission::JmapMailboxChanges, + MethodObject::Thread => Permission::JmapThreadChanges, + MethodObject::Identity => Permission::JmapIdentityChanges, + MethodObject::EmailSubmission => Permission::JmapEmailSubmissionChanges, + MethodObject::Quota => Permission::JmapQuotaChanges, + MethodObject::Core + | MethodObject::Blob + | MethodObject::PushSubscription + | MethodObject::SearchSnippet + | MethodObject::VacationResponse + | MethodObject::SieveScript + | MethodObject::Principal => Permission::JmapEmailChanges, // Unimplemented }, - RequestMethod::Copy(m) => match m.arguments { - jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy, + RequestMethod::Copy(m) => match &m { + CopyRequestMethod::Email(_) => Permission::JmapEmailCopy, + CopyRequestMethod::Blob(_) => Permission::JmapBlobCopy, }, - RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy, RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, - RequestMethod::QueryChanges(m) => match m.arguments { - jmap_proto::method::query::RequestArguments::Email(_) => { - Permission::JmapEmailQueryChanges - } - jmap_proto::method::query::RequestArguments::Mailbox(_) => { - Permission::JmapMailboxQueryChanges - } - jmap_proto::method::query::RequestArguments::EmailSubmission => { + RequestMethod::QueryChanges(m) => match m { + QueryChangesRequestMethod::Email(_) => Permission::JmapEmailQueryChanges, + QueryChangesRequestMethod::Mailbox(_) => Permission::JmapMailboxQueryChanges, + QueryChangesRequestMethod::EmailSubmission(_) => { Permission::JmapEmailSubmissionQueryChanges } - jmap_proto::method::query::RequestArguments::SieveScript => { - Permission::JmapSieveScriptQueryChanges - } - jmap_proto::method::query::RequestArguments::Principal => { - Permission::JmapPrincipalQueryChanges - } - jmap_proto::method::query::RequestArguments::Quota => { - Permission::JmapQuotaQueryChanges - } + QueryChangesRequestMethod::Sieve(_) => Permission::JmapSieveScriptQueryChanges, + QueryChangesRequestMethod::Principal(_) => Permission::JmapPrincipalQueryChanges, + QueryChangesRequestMethod::Quota(_) => Permission::JmapQuotaQueryChanges, }, - RequestMethod::Query(m) => match m.arguments { - jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery, - jmap_proto::method::query::RequestArguments::Mailbox(_) => { - Permission::JmapMailboxQuery - } - jmap_proto::method::query::RequestArguments::EmailSubmission => { - Permission::JmapEmailSubmissionQuery - } - jmap_proto::method::query::RequestArguments::SieveScript => { - Permission::JmapSieveScriptQuery - } - jmap_proto::method::query::RequestArguments::Principal => { - Permission::JmapPrincipalQuery - } - jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery, + RequestMethod::Query(m) => match m { + QueryRequestMethod::Email(_) => Permission::JmapEmailQuery, + QueryRequestMethod::Mailbox(_) => Permission::JmapMailboxQuery, + QueryRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionQuery, + QueryRequestMethod::Sieve(_) => Permission::JmapSieveScriptQuery, + QueryRequestMethod::Principal(_) => Permission::JmapPrincipalQuery, + QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery, }, RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index c857a1fb..721331ba 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -12,7 +12,7 @@ use hyper::{ body::{Bytes, Frame}, }; use jmap_proto::response::status::StateChangeResponse; -use std::future::Future; +use std::{future::Future, str::FromStr}; use std::{ sync::Arc, time::{Duration, Instant}, @@ -48,19 +48,19 @@ impl EventSourceHandler for Server { for (key, value) in http_proto::form_urlencoded::parse(req.uri().query().unwrap_or_default().as_bytes()) { - match key.as_ref() { + hashify::fnc_map!(key.as_bytes(), "types" => { for type_state in value.split(',') { if type_state == "*" { types = Bitmap::all(); break; - } else if let Ok(type_state) = DataType::try_from(type_state) { + } else if let Ok(type_state) = DataType::from_str(type_state) { types.insert(type_state); } else { return Err(trc::ResourceEvent::BadParameters.into_err()); } } - } + }, "closeafter" => match value.as_ref() { "state" => { close_after_state = true; @@ -75,7 +75,7 @@ impl EventSourceHandler for Server { Err(_) => return Err(trc::ResourceEvent::BadParameters.into_err()), }, _ => {} - } + ); } let mut ping = if ping > 0 { diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index 8d2db484..7558933a 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::blob::UploadResponse; use http_proto::{HttpResponse, JsonResponse, ToHttpResponse}; use hyper::StatusCode; use jmap_proto::{ @@ -12,8 +13,7 @@ use jmap_proto::{ response::Response, }; -use crate::blob::UploadResponse; - +pub mod acl; pub mod auth; pub mod event_source; pub mod request; @@ -29,7 +29,7 @@ pub trait ToJmapHttpResponse { fn into_http_response(self) -> HttpResponse; } -impl ToJmapHttpResponse for Response { +impl ToJmapHttpResponse for Response<'_> { fn into_http_response(self) -> HttpResponse { JsonResponse::new(self).into_http_response() } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index cbcd8c1e..80602a03 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -30,12 +30,11 @@ use crate::{ use common::{Server, auth::AccessToken}; use http_proto::HttpSessionData; use jmap_proto::{ - method::{ - get, query, - set::{self}, + request::{ + Call, CopyRequestMethod, GetRequestMethod, QueryRequestMethod, Request, RequestMethod, + SetRequestMethod, method::MethodName, }, - request::{Call, Request, RequestMethod, method::MethodName}, - response::{Response, ResponseMethod}, + response::{Response, ResponseMethod, SetResponseMethod}, }; use trc::JmapEvent; use types::collection::Collection; @@ -53,7 +52,7 @@ pub trait RequestHandler: Sync + Send { fn handle_method_call( &self, method: RequestMethod, - method_name: &'static str, + method_name: MethodName, access_token: &AccessToken, next_call: &mut Option>, session: &HttpSessionData, @@ -62,12 +61,12 @@ pub trait RequestHandler: Sync + Send { impl RequestHandler for Server { #![allow(clippy::large_futures)] - async fn handle_jmap_request( + async fn handle_jmap_request<'x>( &self, - request: Request, + request: Request<'x>, access_token: Arc, session: &HttpSessionData, - ) -> Response { + ) -> Response<'x> { let mut response = Response::new( access_token.state(), request.created_ids.unwrap_or_default(), @@ -94,7 +93,7 @@ impl RequestHandler for Server { match self .handle_method_call( call.method, - method_name, + call.name, &access_token, &mut next_call, session, @@ -105,7 +104,29 @@ impl RequestHandler for Server { match &mut method_response { ResponseMethod::Set(set_response) => { // Add created ids - set_response.update_created_ids(&mut response); + match set_response { + SetResponseMethod::Email(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::Mailbox(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::Identity(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::EmailSubmission(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::PushSubscription(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::Sieve(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::VacationResponse(set_response) => { + set_response.update_created_ids(&mut response); + } + } } ResponseMethod::ImportEmail(import_response) => { // Add created ids @@ -152,165 +173,163 @@ impl RequestHandler for Server { response } - async fn handle_method_call( + async fn handle_method_call<'x>( &self, - method: RequestMethod, - method_name: &'static str, + method: RequestMethod<'x>, + method_name: MethodName, access_token: &AccessToken, - next_call: &mut Option>, + next_call: &mut Option>>, session: &HttpSessionData, - ) -> trc::Result { + ) -> trc::Result> { let op_start = Instant::now(); // Check permissions - access_token.assert_has_jmap_permission(&method)?; + access_token.assert_has_jmap_permission(&method, method_name.obj)?; + let todo = "use account id if missing"; // Handle method let response = match method { - RequestMethod::Get(mut req) => match req.take_arguments() { - get::RequestArguments::Email(arguments) => { + RequestMethod::Get(mut req) => match req { + GetRequestMethod::Email(req) => { access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_get(req.with_arguments(arguments), access_token) - .await? - .into() + self.email_get(req, access_token).await?.into() } - get::RequestArguments::Mailbox => { + GetRequestMethod::Mailbox(req) => { access_token.assert_has_access(req.account_id, Collection::Mailbox)?; self.mailbox_get(req, access_token).await?.into() } - get::RequestArguments::Thread => { + GetRequestMethod::Thread(req) => { access_token.assert_has_access(req.account_id, Collection::Email)?; self.thread_get(req).await?.into() } - get::RequestArguments::Identity => { + GetRequestMethod::Identity(req) => { access_token.assert_is_member(req.account_id)?; self.identity_get(req).await?.into() } - get::RequestArguments::EmailSubmission => { + GetRequestMethod::EmailSubmission(req) => { access_token.assert_is_member(req.account_id)?; self.email_submission_get(req).await?.into() } - get::RequestArguments::PushSubscription => { + GetRequestMethod::PushSubscription(req) => { self.push_subscription_get(req, access_token).await?.into() } - get::RequestArguments::SieveScript => { + GetRequestMethod::Sieve(req) => { access_token.assert_is_member(req.account_id)?; self.sieve_script_get(req).await?.into() } - get::RequestArguments::VacationResponse => { + GetRequestMethod::VacationResponse(req) => { access_token.assert_is_member(req.account_id)?; self.vacation_response_get(req).await?.into() } - get::RequestArguments::Principal => self.principal_get(req).await?.into(), - get::RequestArguments::Quota => { + GetRequestMethod::Principal(req) => self.principal_get(req).await?.into(), + GetRequestMethod::Quota(req) => { access_token.assert_is_member(req.account_id)?; self.quota_get(req, access_token).await?.into() } - get::RequestArguments::Blob(arguments) => { + GetRequestMethod::Blob(req) => { access_token.assert_is_member(req.account_id)?; - self.blob_get(req.with_arguments(arguments), access_token) - .await? - .into() + self.blob_get(req, access_token).await?.into() } }, - RequestMethod::Query(mut req) => match req.take_arguments() { - query::RequestArguments::Email(arguments) => { + RequestMethod::Query(mut req) => match req { + QueryRequestMethod::Email(req) => { access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_query(req.with_arguments(arguments), access_token) - .await? - .into() + self.email_query(req, access_token).await?.into() } - query::RequestArguments::Mailbox(arguments) => { + QueryRequestMethod::Mailbox(req) => { access_token.assert_has_access(req.account_id, Collection::Mailbox)?; - self.mailbox_query(req.with_arguments(arguments), access_token) - .await? - .into() + self.mailbox_query(req, access_token).await?.into() } - query::RequestArguments::EmailSubmission => { + QueryRequestMethod::EmailSubmission(req) => { access_token.assert_is_member(req.account_id)?; self.email_submission_query(req).await?.into() } - query::RequestArguments::SieveScript => { + QueryRequestMethod::Sieve(req) => { access_token.assert_is_member(req.account_id)?; self.sieve_script_query(req).await?.into() } - query::RequestArguments::Principal => { + QueryRequestMethod::Principal(req) => { self.principal_query(req, session).await?.into() } - query::RequestArguments::Quota => { + QueryRequestMethod::Quota(req) => { access_token.assert_is_member(req.account_id)?; self.quota_query(req, access_token).await?.into() } }, - RequestMethod::Set(mut req) => match req.take_arguments() { - set::RequestArguments::Email => { + RequestMethod::Set(mut req) => match req { + SetRequestMethod::Email(req) => { access_token.assert_has_access(req.account_id, Collection::Email)?; self.email_set(req, access_token, session).await?.into() } - set::RequestArguments::Mailbox(arguments) => { + SetRequestMethod::Mailbox(req) => { access_token.assert_has_access(req.account_id, Collection::Mailbox)?; - self.mailbox_set(req.with_arguments(arguments), access_token) - .await? - .into() + self.mailbox_set(req, access_token).await?.into() } - set::RequestArguments::Identity => { + SetRequestMethod::Identity(req) => { access_token.assert_is_member(req.account_id)?; self.identity_set(req).await?.into() } - set::RequestArguments::EmailSubmission(arguments) => { + SetRequestMethod::EmailSubmission(req) => { access_token.assert_is_member(req.account_id)?; - self.email_submission_set( - req.with_arguments(arguments), - &session.instance, - next_call, - ) - .await? - .into() - } - set::RequestArguments::PushSubscription => { - self.push_subscription_set(req, access_token).await?.into() - } - set::RequestArguments::SieveScript(arguments) => { - access_token.assert_is_member(req.account_id)?; - - self.sieve_script_set(req.with_arguments(arguments), access_token, session) + self.email_submission_set(req, &session.instance, next_call) .await? .into() } - set::RequestArguments::VacationResponse => { + SetRequestMethod::PushSubscription(req) => { + self.push_subscription_set(req, access_token).await?.into() + } + SetRequestMethod::Sieve(req) => { + access_token.assert_is_member(req.account_id)?; + + self.sieve_script_set(req, access_token, session) + .await? + .into() + } + SetRequestMethod::VacationResponse(req) => { access_token.assert_is_member(req.account_id)?; self.vacation_response_set(req, access_token).await?.into() } }, - RequestMethod::Changes(req) => self.changes(req, access_token).await?.into(), - RequestMethod::Copy(req) => { - access_token - .assert_has_access(req.account_id, Collection::Email)? - .assert_has_access(req.from_account_id, Collection::Email)?; + RequestMethod::Changes(req) => self + .changes(req, method_name.obj, access_token) + .await? + .into_method_response() + .into(), + RequestMethod::Copy(req) => match req { + CopyRequestMethod::Email(req) => { + access_token + .assert_has_access(req.account_id, Collection::Email)? + .assert_has_access(req.from_account_id, Collection::Email)?; - self.email_copy(req, access_token, next_call, session) - .await? - .into() - } + self.email_copy(req, access_token, next_call, session) + .await? + .into() + } + CopyRequestMethod::Blob(req) => { + access_token.assert_is_member(req.account_id)?; + + self.blob_copy(req, access_token).await?.into() + } + }, RequestMethod::ImportEmail(req) => { access_token.assert_has_access(req.account_id, Collection::Email)?; @@ -332,11 +351,6 @@ impl RequestHandler for Server { 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)?; @@ -353,7 +367,7 @@ impl RequestHandler for Server { trc::event!( Jmap(JmapEvent::MethodCall), - Id = method_name, + Id = method_name.as_str(), SpanId = session.session_id, AccountId = access_token.primary_id(), Elapsed = op_start.elapsed(), diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs index f150913c..bcac33d4 100644 --- a/crates/jmap/src/blob/copy.rs +++ b/crates/jmap/src/blob/copy.rs @@ -6,9 +6,11 @@ use super::download::BlobDownload; use common::{Server, auth::AccessToken}; +use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::copy::{CopyBlobRequest, CopyBlobResponse}, + request::MaybeInvalid, }; use std::future::Future; use store::{ @@ -42,6 +44,17 @@ impl BlobCopy for Server { let account_id = request.account_id.document_id(); for blob_id in request.blob_ids { + let blob_id = match blob_id { + MaybeInvalid::Value(blob_id) => blob_id, + MaybeInvalid::Invalid(_) => { + response.not_copied.append( + blob_id, + SetError::invalid_properties().with_description("Invalid blobId."), + ); + continue; + } + }; + if self.has_access_blob(&blob_id, access_token).await? { let mut batch = BatchBuilder::new(); let until = now() + self.core.jmap.upload_tmp_ttl; @@ -56,6 +69,7 @@ impl BlobCopy for Server { .write(batch.build_all()) .await .caused_by(trc::location!())?; + let dest_blob_id = BlobId { hash: blob_id.hash.clone(), class: BlobClass::Reserved { @@ -68,7 +82,7 @@ impl BlobCopy for Server { response.copied.append(blob_id, dest_blob_id); } else { response.not_copied.append( - blob_id, + MaybeInvalid::Value(blob_id), SetError::new(SetErrorType::BlobNotFound).with_description( "blobId does not exist or not enough permissions to access it.", ), diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs index ec7772f9..6fa2c1b4 100644 --- a/crates/jmap/src/blob/get.rs +++ b/crates/jmap/src/blob/get.rs @@ -12,13 +12,10 @@ use jmap_proto::{ get::{GetRequest, GetResponse}, lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse}, }, - object::blob::GetArguments, - types::{ - MaybeUnparsable, - property::{DataProperty, DigestProperty, Property}, - value::{Object, Value}, - }, + object::blob::{Blob, BlobProperty, BlobValue, DataProperty, DigestProperty}, + request::MaybeInvalid, }; +use jmap_tools::{Map, Value}; use mail_builder::encoders::base64::base64_encode; use sha1::{Digest, Sha1}; use sha2::{Sha256, Sha512}; @@ -30,9 +27,9 @@ use utils::map::vec_map::VecMap; pub trait BlobOperations: Sync + Send { fn blob_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn blob_lookup( &self, @@ -43,16 +40,16 @@ pub trait BlobOperations: Sync + Send { impl BlobOperations for Server { async fn blob_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let ids = request - .unwrap_blob_ids(self.core.jmap.get_max_objects)? + .unwrap_ids(self.core.jmap.get_max_objects)? .unwrap_or_default(); let properties = request.unwrap_properties(&[ - Property::Id, - Property::Data(DataProperty::Default), - Property::Size, + BlobProperty::Id, + BlobProperty::Data(DataProperty::Default), + BlobProperty::Size, ]); let mut response = GetResponse { account_id: request.account_id.into(), @@ -70,12 +67,12 @@ impl BlobOperations for Server { 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 mut blob = Map::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); + blob.insert_unchecked(BlobProperty::IsTruncated, true); bytes.len() } else { range_to @@ -85,10 +82,10 @@ impl BlobOperations for Server { 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 { + let value: Value<'static, BlobProperty, BlobValue> = match &property { + BlobProperty::Id => Value::Element(BlobValue::BlobId(blob_id.clone())), + BlobProperty::Size => bytes.len().into(), + BlobProperty::Digest(digest) => match digest { DigestProperty::Sha => { let mut hasher = Sha1::new(); hasher.update(bytes_range); @@ -115,11 +112,11 @@ impl BlobOperations for Server { } } .into(), - Property::Data(data) => match data { + BlobProperty::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); + blob.insert_unchecked(BlobProperty::IsEncodingProblem, true); Value::Null } }, @@ -130,12 +127,12 @@ impl BlobOperations for Server { } DataProperty::Default => match std::str::from_utf8(bytes_range) { Ok(text) => { - property = Property::Data(DataProperty::AsText); + property = BlobProperty::Data(DataProperty::AsText); text.to_string().into() } Err(_) => { - property = Property::Data(DataProperty::AsBase64); - blob.append(Property::IsEncodingProblem, true); + property = BlobProperty::Data(DataProperty::AsBase64); + blob.insert_unchecked(BlobProperty::IsEncodingProblem, true); String::from_utf8( base64_encode(bytes_range).unwrap_or_default(), ) @@ -146,11 +143,11 @@ impl BlobOperations for Server { }, _ => Value::Null, }; - blob.append(property, value); + blob.insert_unchecked(property, value); } // Add result to response - response.list.push(blob); + response.list.push(blob.into()); } else { response.not_found.push(blob_id.into()); } @@ -168,7 +165,7 @@ impl BlobOperations for Server { .type_names .into_iter() .map(|tn| match tn { - MaybeUnparsable::Value(value) => { + MaybeInvalid::Value(value) => { match &value { DataType::Email => { include_email = true; @@ -184,7 +181,7 @@ impl BlobOperations for Server { Ok(value) } - MaybeUnparsable::ParseError(_) => Err(trc::JmapEvent::UnknownDataType.into_err()), + MaybeInvalid::Invalid(_) => Err(trc::JmapEvent::UnknownDataType.into_err()), }) .collect::, _>>()?; let req_account_id = request.account_id.document_id(); @@ -196,7 +193,7 @@ impl BlobOperations for Server { for id in request.ids { match id { - MaybeUnparsable::Value(id) => { + MaybeInvalid::Value(id) => { let mut matched_ids = VecMap::new(); match &id.class { @@ -254,7 +251,7 @@ impl BlobOperations for Server { BlobClass::Reserved { account_id, .. } if *account_id == req_account_id => { } _ => { - response.not_found.push(MaybeUnparsable::Value(id)); + response.not_found.push(MaybeInvalid::Value(id)); continue; } } diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index fca01878..6c28036e 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -14,7 +14,7 @@ use jmap_proto::{ method::upload::{ BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject, }, - request::reference::MaybeReference, + request::reference::MaybeIdReference, }; use std::future::Future; use trc::AddContext; @@ -64,8 +64,8 @@ impl BlobUpload for Server { let bytes = match data_source { DataSourceObject::Id { id, length, offset } => { let id = match id { - MaybeReference::Value(id) => id, - MaybeReference::Reference(reference) => { + MaybeIdReference::Id(id) => id, + MaybeIdReference::Reference(reference) => { if let Some(obj) = response.created.get(&reference) { obj.id.clone() } else { @@ -78,6 +78,14 @@ impl BlobUpload for Server { continue 'outer; } } + MaybeIdReference::Invalid(id) => { + response.not_created.append( + create_id, + SetError::invalid_properties() + .with_description(format!("Invalid blobId {id}.")), + ); + continue 'outer; + } }; if !self.has_access_blob(&id, access_token).await? { @@ -122,6 +130,14 @@ impl BlobUpload for Server { } } DataSourceObject::Value(bytes) => bytes, + DataSourceObject::Null => { + response.not_created.append( + create_id, + SetError::invalid_properties() + .with_description("Invalid DataSourceObject."), + ); + continue 'outer; + } }; if bytes.len() + data.len() < self.core.jmap.upload_max_size { diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index 8b11238c..8dbd8140 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -7,8 +7,11 @@ use crate::api::auth::JmapAuthorization; use common::{Server, auth::AccessToken}; use jmap_proto::{ - method::changes::{ChangesRequest, ChangesResponse, RequestArguments}, - types::{property::Property, state::State}, + method::changes::{ChangesRequest, ChangesResponse}, + object::{JmapObject, NullObject, mailbox::MailboxProperty}, + request::method::MethodObject, + response::ChangesResponseMethod, + types::state::State, }; use std::future::Future; use store::query::log::{Change, Query}; @@ -18,43 +21,51 @@ pub trait ChangesLookup: Sync + Send { fn changes( &self, request: ChangesRequest, + object: MethodObject, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future> + Send; +} + +pub struct IntermediateChangesResponse { + pub response: ChangesResponse, + pub object: MethodObject, + pub only_container_changes: bool, } impl ChangesLookup for Server { async fn changes( &self, request: ChangesRequest, + object: MethodObject, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result { // Map collection and validate ACLs - let (collection, is_container) = match request.arguments { - RequestArguments::Email => { + let (collection, is_container) = match object { + MethodObject::Email => { access_token.assert_has_access(request.account_id, Collection::Email)?; (SyncCollection::Email, false) } - RequestArguments::Mailbox => { + MethodObject::Mailbox => { access_token.assert_has_access(request.account_id, Collection::Mailbox)?; (SyncCollection::Email, true) } - RequestArguments::Thread => { + MethodObject::Thread => { access_token.assert_has_access(request.account_id, Collection::Email)?; (SyncCollection::Thread, true) } - RequestArguments::Identity => { + MethodObject::Identity => { access_token.assert_is_member(request.account_id)?; (SyncCollection::Identity, false) } - RequestArguments::EmailSubmission => { + MethodObject::EmailSubmission => { access_token.assert_is_member(request.account_id)?; (SyncCollection::EmailSubmission, false) } - RequestArguments::Quota => { + _ => { access_token.assert_is_member(request.account_id)?; return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); @@ -68,7 +79,7 @@ impl ChangesLookup for Server { .unwrap_or(usize::MAX), self.core.jmap.changes_max_results.unwrap_or(usize::MAX), ); - let mut response = ChangesResponse { + let mut response: ChangesResponse = ChangesResponse { account_id: request.account_id, old_state: request.since_state.clone(), new_state: State::Initial, @@ -87,7 +98,11 @@ impl ChangesLookup for Server { .changes(account_id, collection.into(), Query::All) .await?; if changelog.changes.is_empty() && changelog.from_change_id == 0 { - return Ok(response); + return Ok(IntermediateChangesResponse { + response, + object, + only_container_changes: false, + }); } (0, changelog) @@ -180,68 +195,62 @@ impl ChangesLookup for Server { } else { State::new_exact(change_id) }; - if is_container - && !response.updated.is_empty() - && !items_changed - && collection == SyncCollection::Email - { - response.updated_properties = vec![ - Property::TotalEmails, - Property::UnreadEmails, - Property::TotalThreads, - Property::UnreadThreads, - ] - .into() - } - Ok(response) + Ok(IntermediateChangesResponse { + only_container_changes: is_container && !response.updated.is_empty() && !items_changed, + response, + object, + }) } } -/*async fn changes( - server: &Server, - account_id: u32, - collection: SyncCollection, - query: Query, - response: &mut ChangesResponse, -) -> trc::Result { - let mut main_changes = server - .store() - .changes(account_id, collection, query) - .await?; - if matches!(collection, Collection::Mailbox) { - let child_changes = server - .store() - .changes(account_id, collection.as_child_update(), query) - .await?; - - if !child_changes.changes.is_empty() { - if child_changes.from_change_id < main_changes.from_change_id { - main_changes.from_change_id = child_changes.from_change_id; - } - if child_changes.to_change_id > main_changes.to_change_id { - main_changes.to_change_id = child_changes.to_change_id; - } - let mut has_child_changes = false; - for change in child_changes.changes { - let id = change.id(); - if !main_changes.changes.iter().any(|c| c.id() == id) { - main_changes.changes.push(change); - has_child_changes = true; +impl IntermediateChangesResponse { + pub fn into_method_response(self) -> ChangesResponseMethod { + match self.object { + MethodObject::Email => ChangesResponseMethod::Email(transmute_response(self.response)), + MethodObject::Mailbox => { + let mut response = transmute_response(self.response); + if self.only_container_changes { + response.updated_properties = vec![ + MailboxProperty::TotalEmails.into(), + MailboxProperty::UnreadEmails.into(), + MailboxProperty::TotalThreads.into(), + MailboxProperty::UnreadThreads.into(), + ] + .into(); } + ChangesResponseMethod::Mailbox(response) } - - if has_child_changes { - response.updated_properties = vec![ - Property::TotalEmails, - Property::UnreadEmails, - Property::TotalThreads, - Property::UnreadThreads, - ] - .into(); + MethodObject::Thread => { + ChangesResponseMethod::Thread(transmute_response(self.response)) } + MethodObject::Identity => { + ChangesResponseMethod::Identity(transmute_response(self.response)) + } + MethodObject::EmailSubmission => { + ChangesResponseMethod::EmailSubmission(transmute_response(self.response)) + } + MethodObject::Core + | MethodObject::Blob + | MethodObject::PushSubscription + | MethodObject::SearchSnippet + | MethodObject::VacationResponse + | MethodObject::SieveScript + | MethodObject::Principal + | MethodObject::Quota => unreachable!(), } } - Ok(main_changes) } -*/ + +fn transmute_response(response: ChangesResponse) -> ChangesResponse { + ChangesResponse { + account_id: response.account_id, + old_state: response.old_state, + new_state: response.new_state, + has_more_changes: response.has_more_changes, + created: response.created, + updated: response.updated, + destroyed: response.destroyed, + updated_properties: None, + } +} diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index cc6176e7..fcfa4d96 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -4,25 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use jmap_proto::method::{ - changes::{self, ChangesRequest}, - query::{self, QueryRequest}, - query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse}, -}; -use std::future::Future; - +use super::get::ChangesLookup; use crate::{ - email::query::EmailQuery, mailbox::query::MailboxQuery, quota::query::QuotaQuery, + email::query::EmailQuery, mailbox::query::MailboxQuery, sieve::query::SieveScriptQuery, submission::query::EmailSubmissionQuery, }; - -use super::get::ChangesLookup; +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::{ + changes::{ChangesRequest, ChangesResponse}, + query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse}, + }, + object::{JmapObject, NullObject}, + request::{QueryChangesRequestMethod, method::MethodObject}, +}; +use std::future::Future; pub trait QueryChanges: Sync + Send { fn query_changes( &self, - request: QueryChangesRequest, + request: QueryChangesRequestMethod, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -30,105 +31,164 @@ pub trait QueryChanges: Sync + Send { impl QueryChanges for Server { async fn query_changes( &self, - request: QueryChangesRequest, + request: QueryChangesRequestMethod, access_token: &AccessToken, ) -> trc::Result { - // Query changes - let changes = self - .changes( - ChangesRequest { - account_id: request.account_id, - since_state: request.since_query_state.clone(), - max_changes: request.max_changes, - arguments: match &request.arguments { - query::RequestArguments::Email(_) => changes::RequestArguments::Email, - query::RequestArguments::Mailbox(_) => changes::RequestArguments::Mailbox, - query::RequestArguments::EmailSubmission => { - changes::RequestArguments::EmailSubmission - } - query::RequestArguments::Quota => changes::RequestArguments::Quota, - _ => { - return Err(trc::JmapEvent::UnknownMethod - .into_err() - .details("Unknown method")); - } - }, - }, - access_token, - ) - .await?; - let calculate_total = request.calculate_total.unwrap_or(false); - let has_changes = changes.has_changes(); - let mut response = QueryChangesResponse { - account_id: request.account_id, - old_query_state: changes.old_state, - new_query_state: changes.new_state, - total: None, - removed: vec![], - added: vec![], - }; + let mut response; + let mut is_mutable = true; + let results; + let changes; + let has_changes; + let up_to_id; - if has_changes || calculate_total { - let query = QueryRequest { - account_id: request.account_id, - filter: request.filter, - sort: request.sort, - position: None, - anchor: None, - anchor_offset: None, - limit: None, - calculate_total: request.calculate_total, - arguments: query::RequestArguments::EmailSubmission, - }; - let is_mutable = query.filter.iter().any(|f| !f.is_immutable()) - || query - .sort - .as_ref() - .is_some_and(|sort| sort.iter().any(|s| !s.is_immutable())); - let results = match request.arguments { - query::RequestArguments::Email(arguments) => { - self.email_query(query.with_arguments(arguments), access_token) - .await? - } - query::RequestArguments::Mailbox(arguments) => { - self.mailbox_query(query.with_arguments(arguments), access_token) - .await? - } - query::RequestArguments::EmailSubmission => { - self.email_submission_query(query).await? - } - query::RequestArguments::Quota => self.quota_query(query, access_token).await?, - _ => unreachable!(), - }; + match request { + QueryChangesRequestMethod::Email(request) => { + // Query changes + changes = self + .changes( + build_changes_request(&request), + MethodObject::Email, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); - if has_changes { - if is_mutable { - for (index, id) in results.ids.into_iter().enumerate() { - if matches!(request.up_to_id, Some(up_to_id) if up_to_id == id) { - break; - } else if changes.created.contains(&id) || changes.updated.contains(&id) { - response.added.push(AddedItem::new(id, index)); - } - } + if !has_changes && !calculate_total { + return Ok(response); + } - response.removed = changes.updated; - } else { - for (index, id) in results.ids.into_iter().enumerate() { - if matches!(request.up_to_id, Some(up_to_id) if up_to_id == id) { - break; - } else if changes.created.contains(&id) { - response.added.push(AddedItem::new(id, index)); - } + up_to_id = request.up_to_id; + is_mutable = request.filter.iter().any(|f| !f.is_immutable()) + || request + .sort + .as_ref() + .is_some_and(|sort| sort.iter().any(|s| !s.is_immutable())); + + results = self.email_query(request.into(), access_token).await?; + } + QueryChangesRequestMethod::Mailbox(request) => { + // Query changes + changes = self + .changes( + build_changes_request(&request), + MethodObject::Mailbox, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self.mailbox_query(request.into(), access_token).await?; + } + QueryChangesRequestMethod::EmailSubmission(request) => { + // Query changes + changes = self + .changes( + build_changes_request(&request), + MethodObject::EmailSubmission, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self.email_submission_query(request.into()).await?; + } + QueryChangesRequestMethod::Sieve(request) => { + // Query changes + changes = self + .changes( + build_changes_request(&request), + MethodObject::SieveScript, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self.sieve_script_query(request.into()).await?; + } + QueryChangesRequestMethod::Principal(_) => { + return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); + } + QueryChangesRequestMethod::Quota(_) => { + return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); + } + } + + if has_changes { + if is_mutable { + for (index, id) in results.ids.into_iter().enumerate() { + if matches!(up_to_id, Some(up_to_id) if up_to_id == id) { + break; + } else if changes.created.contains(&id) || changes.updated.contains(&id) { + response.added.push(AddedItem::new(id, index)); } } - if !changes.destroyed.is_empty() { - response.removed.extend(changes.destroyed); + response.removed = changes.updated; + } else { + for (index, id) in results.ids.into_iter().enumerate() { + if matches!(up_to_id, Some(up_to_id) if up_to_id == id) { + break; + } else if changes.created.contains(&id) { + response.added.push(AddedItem::new(id, index)); + } } } - response.total = results.total; + + if !changes.destroyed.is_empty() { + response.removed.extend(changes.destroyed); + } } + response.total = results.total; Ok(response) } } + +fn build_changes_request(req: &QueryChangesRequest) -> ChangesRequest { + ChangesRequest { + account_id: req.account_id, + since_state: req.since_query_state.clone(), + max_changes: req.max_changes, + } +} + +fn build_query_changes_response( + req: &QueryChangesRequest, + changes: &ChangesResponse, +) -> QueryChangesResponse { + QueryChangesResponse { + account_id: req.account_id, + old_query_state: changes.old_state.clone(), + new_query_state: changes.new_state.clone(), + total: None, + removed: vec![], + added: vec![], + } +} diff --git a/crates/jmap/src/email/body.rs b/crates/jmap/src/email/body.rs index 3d379211..70a0f526 100644 --- a/crates/jmap/src/email/body.rs +++ b/crates/jmap/src/email/body.rs @@ -5,10 +5,8 @@ */ use email::message::metadata::{ArchivedMessageMetadataContents, ArchivedMetadataPartType}; -use jmap_proto::types::{ - property::Property, - value::{Object, Value}, -}; +use jmap_proto::object::email::{EmailProperty, EmailValue}; +use jmap_tools::{Map, Value}; use mail_parser::{ArchivedHeaderValue, HeaderValue, MessagePart, MimeHeaders, PartType}; use types::blob::BlobId; @@ -18,20 +16,20 @@ pub trait ToBodyPart { fn to_body_part( &self, part_id: u32, - properties: &[Property], + properties: &[EmailProperty], raw_message: &[u8], blob_id: &BlobId, - ) -> Value; + ) -> Value<'static, EmailProperty, EmailValue>; } impl ToBodyPart for Vec> { fn to_body_part( &self, part_id: u32, - properties: &[Property], + properties: &[EmailProperty], raw_message: &[u8], blob_id: &BlobId, - ) -> Value { + ) -> Value<'static, EmailProperty, EmailValue> { let mut parts = vec![part_id].into_iter(); let mut parts_stack = Vec::new(); let mut subparts = Vec::with_capacity(1); @@ -41,7 +39,7 @@ impl ToBodyPart for Vec> { .next() .map(|part_id| (part_id, &self[part_id as usize])) { - let mut values = Object::with_capacity(properties.len()); + let mut values = Map::with_capacity(properties.len()); let multipart = if let PartType::Multipart(parts) = &part.body { parts.into() } else { @@ -50,8 +48,8 @@ impl ToBodyPart for Vec> { for property in properties { let value = match property { - Property::PartId if multipart.is_none() => part_id.to_string().into(), - Property::BlobId if multipart.is_none() => { + EmailProperty::PartId if multipart.is_none() => part_id.to_string().into(), + EmailProperty::BlobId if multipart.is_none() => { let base_offset = blob_id.start_offset(); BlobId::new_section( blob_id.hash.clone(), @@ -62,15 +60,15 @@ impl ToBodyPart for Vec> { ) .into() } - Property::Size if multipart.is_none() => match &part.body { + EmailProperty::Size if multipart.is_none() => match &part.body { PartType::Text(text) | PartType::Html(text) => text.len(), PartType::Binary(bin) | PartType::InlineBinary(bin) => bin.len(), PartType::Message(message) => message.root_part().raw_len() as usize, PartType::Multipart(_) => 0, } .into(), - Property::Name => part.attachment_name().into(), - Property::Type => part + EmailProperty::Name => part.attachment_name().map(|v| v.to_string()).into(), + EmailProperty::Type => part .content_type() .map(|ct| { ct.subtype() @@ -84,34 +82,41 @@ impl ToBodyPart for Vec> { _ => None, }) .into(), - Property::Charset => part + EmailProperty::Charset => part .content_type() .and_then(|ct| ct.attribute("charset")) .or(match &part.body { PartType::Text(_) | PartType::Html(_) => Some("us-ascii"), _ => None, }) + .map(|v| v.to_string()) .into(), - Property::Disposition => { - part.content_disposition().map(|cd| cd.ctype()).into() - } - Property::Cid => part.content_id().into(), - Property::Language => match part.content_language() { + EmailProperty::Disposition => part + .content_disposition() + .map(|cd| cd.ctype()) + .map(|v| v.to_string()) + .into(), + EmailProperty::Cid => part.content_id().map(|v| v.to_string()).into(), + EmailProperty::Language => match part.content_language() { HeaderValue::Text(text) => vec![text.to_string()].into(), HeaderValue::TextList(list) => list .iter() .map(|text| text.to_string().into()) - .collect::>() + .collect::>>() .into(), _ => Value::Null, }, - Property::Location => part.content_location().into(), - Property::Header(_) => part.headers.header_to_value(property, raw_message), - Property::Headers => part.headers.headers_to_value(raw_message), - Property::SubParts => continue, + EmailProperty::Location => { + part.content_location().map(|v| v.to_string()).into() + } + EmailProperty::Header(_) => { + part.headers.header_to_value(property, raw_message) + } + EmailProperty::Headers => part.headers.headers_to_value(raw_message), + EmailProperty::SubParts => continue, _ => Value::Null, }; - values.append(property.clone(), value); + values.insert_unchecked(property.clone(), value); } subparts.push(values); @@ -128,7 +133,7 @@ impl ToBodyPart for Vec> { prev_subparts .last_mut() .unwrap() - .append(Property::SubParts, subparts); + .insert_unchecked(EmailProperty::SubParts, subparts); parts = prev_parts; subparts = prev_subparts; } else { @@ -142,10 +147,10 @@ impl ToBodyPart for ArchivedMessageMetadataContents { fn to_body_part( &self, part_id: u32, - properties: &[Property], + properties: &[EmailProperty], raw_message: &[u8], blob_id: &BlobId, - ) -> Value { + ) -> Value<'static, EmailProperty, EmailValue> { let mut parts = vec![part_id].into_iter(); let mut parts_stack = Vec::new(); let mut subparts = Vec::with_capacity(1); @@ -155,7 +160,7 @@ impl ToBodyPart for ArchivedMessageMetadataContents { .next() .map(|part_id| (part_id, &self.parts[part_id as usize])) { - let mut values = Object::with_capacity(properties.len()); + let mut values = Map::with_capacity(properties.len()); let multipart = if let ArchivedMetadataPartType::Multipart(parts) = &part.body { parts.into() } else { @@ -164,8 +169,8 @@ impl ToBodyPart for ArchivedMessageMetadataContents { for property in properties { let value = match property { - Property::PartId if multipart.is_none() => part_id.to_string().into(), - Property::BlobId if multipart.is_none() => { + EmailProperty::PartId if multipart.is_none() => part_id.to_string().into(), + EmailProperty::BlobId if multipart.is_none() => { let base_offset = blob_id.start_offset(); BlobId::new_section( blob_id.hash.clone(), @@ -176,9 +181,9 @@ impl ToBodyPart for ArchivedMessageMetadataContents { ) .into() } - Property::Size if multipart.is_none() => u32::from(part.size).into(), - Property::Name => part.attachment_name().into(), - Property::Type => part + EmailProperty::Size if multipart.is_none() => u32::from(part.size).into(), + EmailProperty::Name => part.attachment_name().map(|v| v.to_string()).into(), + EmailProperty::Type => part .content_type() .map(|ct| { ct.subtype() @@ -194,7 +199,7 @@ impl ToBodyPart for ArchivedMessageMetadataContents { _ => None, }) .into(), - Property::Charset => { + EmailProperty::Charset => { part.content_type() .and_then(|ct| ct.attribute("charset")) .or(match &part.body { @@ -202,28 +207,35 @@ impl ToBodyPart for ArchivedMessageMetadataContents { | ArchivedMetadataPartType::Html => Some("us-ascii"), _ => None, }) + .map(|v| v.to_string()) .into() } - Property::Disposition => { - part.content_disposition().map(|cd| cd.ctype()).into() - } - Property::Cid => part.content_id().into(), - Property::Language => match part.content_language() { + EmailProperty::Disposition => part + .content_disposition() + .map(|cd| cd.ctype()) + .map(|v| v.to_string()) + .into(), + EmailProperty::Cid => part.content_id().map(|v| v.to_string()).into(), + EmailProperty::Language => match part.content_language() { ArchivedHeaderValue::Text(text) => vec![text.to_string()].into(), ArchivedHeaderValue::TextList(list) => list .iter() .map(|text| text.to_string().into()) - .collect::>() + .collect::>>() .into(), _ => Value::Null, }, - Property::Location => part.content_location().into(), - Property::Header(_) => part.headers.header_to_value(property, raw_message), - Property::Headers => part.headers.headers_to_value(raw_message), - Property::SubParts => continue, + EmailProperty::Location => { + part.content_location().map(|v| v.to_string()).into() + } + EmailProperty::Header(_) => { + part.headers.header_to_value(property, raw_message) + } + EmailProperty::Headers => part.headers.headers_to_value(raw_message), + EmailProperty::SubParts => continue, _ => Value::Null, }; - values.append(property.clone(), value); + values.insert_unchecked(property.clone(), value); } subparts.push(values); @@ -243,7 +255,7 @@ impl ToBodyPart for ArchivedMessageMetadataContents { prev_subparts .last_mut() .unwrap() - .append(Property::SubParts, subparts); + .insert_unchecked(EmailProperty::SubParts, subparts); parts = prev_parts; subparts = prev_subparts; } else { diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 80d7bfca..53fe1faf 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -14,18 +14,13 @@ use http_proto::HttpSessionData; use jmap_proto::{ error::set::SetError, method::{ - copy::{CopyRequest, CopyResponse, RequestArguments}, + copy::{CopyRequest, CopyResponse}, set::{self, SetRequest}, }, + object::email::{Email, EmailProperty}, request::{ Call, RequestMethod, method::{MethodFunction, MethodName, MethodObject}, - reference::MaybeReference, - }, - response::references::EvalObjectReferences, - types::{ - property::Property, - value::{MaybePatchValue, Value}, }, }; use std::future::Future; @@ -34,23 +29,23 @@ use types::acl::Acl; use utils::map::vec_map::VecMap; pub trait JmapEmailCopy: Sync + Send { - fn email_copy( + fn email_copy<'x>( &self, - request: CopyRequest, + request: CopyRequest<'x, Email>, access_token: &AccessToken, - next_call: &mut Option>, + next_call: &mut Option>>, session: &HttpSessionData, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl JmapEmailCopy for Server { - async fn email_copy( + async fn email_copy<'x>( &self, - request: CopyRequest, + request: CopyRequest<'x, Email>, access_token: &AccessToken, - next_call: &mut Option>, + next_call: &mut Option>>, session: &HttpSessionData, - ) -> trc::Result { + ) -> trc::Result> { let account_id = request.account_id.document_id(); let from_account_id = request.from_account_id.document_id(); @@ -119,14 +114,14 @@ impl JmapEmailCopy for Server { }; match (property, value) { - (Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => { + (EmailProperty::MailboxIds, MaybePatchValue::Value(Value::Array(ids))) => { mailboxes = ids .into_iter() .filter_map(|id| id.try_unwrap_id()?.document_id().into()) .collect(); } - (Property::MailboxIds, MaybePatchValue::Patch(patch)) => { + (EmailProperty::MailboxIds, MaybePatchValue::Patch(patch)) => { let mut patch = patch.into_iter(); if let Some(id) = patch.next().unwrap().try_unwrap_id() { let document_id = id.document_id(); @@ -140,14 +135,14 @@ impl JmapEmailCopy for Server { } } - (Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => { + (EmailProperty::Keywords, MaybePatchValue::Value(Value::Array(keywords_))) => { keywords = keywords_ .into_iter() .filter_map(|keyword| keyword.try_unwrap_keyword()) .collect(); } - (Property::Keywords, MaybePatchValue::Patch(patch)) => { + (EmailProperty::Keywords, MaybePatchValue::Patch(patch)) => { let mut patch = patch.into_iter(); if let Some(keyword) = patch.next().unwrap().try_unwrap_keyword() { if patch.next().unwrap().try_unwrap_bool().unwrap_or_default() { @@ -159,14 +154,14 @@ impl JmapEmailCopy for Server { } } } - (Property::ReceivedAt, MaybePatchValue::Value(Value::Date(value))) => { + (EmailProperty::ReceivedAt, MaybePatchValue::Value(Value::Date(value))) => { received_at = value.into(); } (property, _) => { response.not_created.append( id, SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Invalid property or value.".to_string()), ); continue 'create; @@ -179,7 +174,7 @@ impl JmapEmailCopy for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(EmailProperty::MailboxIds) .with_description("Message has to belong to at least one mailbox."), ); continue 'create; @@ -191,7 +186,7 @@ impl JmapEmailCopy for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(EmailProperty::MailboxIds) .with_description(format!("mailboxId {mailbox_id} does not exist.")), ); continue 'create; diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs index 82e08e9e..d1e0f1c6 100644 --- a/crates/jmap/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -18,13 +18,11 @@ use email::{ }; use jmap_proto::{ method::get::{GetRequest, GetResponse}, - object::email::GetArguments, - types::{ - date::UTCDate, - property::{HeaderForm, Property}, - value::{Object, Value}, - }, + object::email::{Email, EmailProperty, EmailValue, HeaderForm}, + request::IntoValid, + types::date::UTCDate, }; +use jmap_tools::{Key, Map, Value}; use mail_parser::{ArchivedHeaderName, HeaderValue, core::rkyv::ArchivedGetHeader}; use std::{borrow::Cow, future::Future}; use trc::{AddContext, StoreEvent}; @@ -40,58 +38,62 @@ use types::{ pub trait EmailGet: Sync + Send { fn email_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl EmailGet for Server { async fn email_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::BlobId, - Property::ThreadId, - Property::MailboxIds, - Property::Keywords, - Property::Size, - Property::ReceivedAt, - Property::MessageId, - Property::InReplyTo, - Property::References, - Property::Sender, - Property::From, - Property::To, - Property::Cc, - Property::Bcc, - Property::ReplyTo, - Property::Subject, - Property::SentAt, - Property::HasAttachment, - Property::Preview, - Property::BodyValues, - Property::TextBody, - Property::HtmlBody, - Property::Attachments, + EmailProperty::Id, + EmailProperty::BlobId, + EmailProperty::ThreadId, + EmailProperty::MailboxIds, + EmailProperty::Keywords, + EmailProperty::Size, + EmailProperty::ReceivedAt, + EmailProperty::MessageId, + EmailProperty::InReplyTo, + EmailProperty::References, + EmailProperty::Sender, + EmailProperty::From, + EmailProperty::To, + EmailProperty::Cc, + EmailProperty::Bcc, + EmailProperty::ReplyTo, + EmailProperty::Subject, + EmailProperty::SentAt, + EmailProperty::HasAttachment, + EmailProperty::Preview, + EmailProperty::BodyValues, + EmailProperty::TextBody, + EmailProperty::HtmlBody, + EmailProperty::Attachments, ]); - let body_properties = request.arguments.body_properties.unwrap_or_else(|| { - vec![ - Property::PartId, - Property::BlobId, - Property::Size, - Property::Name, - Property::Type, - Property::Charset, - Property::Disposition, - Property::Cid, - Property::Language, - Property::Location, - ] - }); + let body_properties = request + .arguments + .body_properties + .map(|v| v.into_valid().collect()) + .unwrap_or_else(|| { + vec![ + EmailProperty::PartId, + EmailProperty::BlobId, + EmailProperty::Size, + EmailProperty::Name, + EmailProperty::Type, + EmailProperty::Charset, + EmailProperty::Disposition, + EmailProperty::Cid, + EmailProperty::Language, + EmailProperty::Location, + ] + }); let fetch_text_body_values = request.arguments.fetch_text_body_values.unwrap_or(false); let fetch_html_body_values = request.arguments.fetch_html_body_values.unwrap_or(false); let fetch_all_body_values = request.arguments.fetch_all_body_values.unwrap_or(false); @@ -131,11 +133,11 @@ impl EmailGet for Server { for property in &properties { if matches!( property, - Property::BodyValues - | Property::TextBody - | Property::HtmlBody - | Property::Attachments - | Property::BodyStructure + EmailProperty::BodyValues + | EmailProperty::TextBody + | EmailProperty::HtmlBody + | EmailProperty::Attachments + | EmailProperty::BodyStructure ) { needs_body = true; break; @@ -209,58 +211,68 @@ impl EmailGet for Server { }; // Prepare response - let mut email = Object::with_capacity(properties.len()); + let mut email: Map<'_, EmailProperty, EmailValue> = + Map::with_capacity(properties.len()); let contents = &metadata.contents[0]; let root_part = &contents.parts[0]; for property in &properties { match property { - Property::Id => { - email.append(Property::Id, Id::from(*id)); + EmailProperty::Id => { + email.insert_unchecked(EmailProperty::Id, Id::from(*id)); } - Property::ThreadId => { - email.append(Property::ThreadId, Id::from(id.prefix_id())); + EmailProperty::ThreadId => { + email.insert_unchecked(EmailProperty::ThreadId, Id::from(id.prefix_id())); } - Property::BlobId => { - email.append(Property::BlobId, blob_id.clone()); + EmailProperty::BlobId => { + email.insert_unchecked(EmailProperty::BlobId, blob_id.clone()); } - Property::MailboxIds => { - let mut obj = Object::with_capacity(data.mailboxes.len()); + EmailProperty::MailboxIds => { + let mut obj = Map::with_capacity(data.mailboxes.len()); for id in data.mailboxes.iter() { debug_assert!(id.uid != 0); - obj.append(Property::_T(Id::from(id.mailbox_id).to_string()), true); + obj.insert_unchecked( + EmailProperty::IdValue(Id::from(id.mailbox_id)), + true, + ); } - email.append(property.clone(), Value::Object(obj)); + email.insert_unchecked(property.clone(), Value::Object(obj)); } - Property::Keywords => { - let mut obj = Object::with_capacity(2); + EmailProperty::Keywords => { + let mut obj = Map::with_capacity(2); for keyword in cache.expand_keywords(data) { - obj.append(Property::_T(keyword.to_string()), true); + obj.insert_unchecked(EmailProperty::Keyword(keyword), true); } - email.append(property.clone(), Value::Object(obj)); + email.insert_unchecked(property.clone(), Value::Object(obj)); } - Property::Size => { - email.append(Property::Size, u32::from(metadata.size)); + EmailProperty::Size => { + email.insert_unchecked(EmailProperty::Size, u32::from(metadata.size)); } - Property::ReceivedAt => { - email.append( - Property::ReceivedAt, - Value::Date(UTCDate::from_timestamp( - u64::from(metadata.received_at) as i64 + EmailProperty::ReceivedAt => { + email.insert_unchecked( + EmailProperty::ReceivedAt, + EmailValue::Date(UTCDate::from_timestamp( + u64::from(metadata.received_at) as i64, )), ); } - Property::Preview => { + EmailProperty::Preview => { if !metadata.preview.is_empty() { - email.append(Property::Preview, metadata.preview.to_string()); + email.insert_unchecked( + EmailProperty::Preview, + metadata.preview.to_string(), + ); } } - Property::HasAttachment => { - email.append(Property::HasAttachment, metadata.has_attachments); + EmailProperty::HasAttachment => { + email.insert_unchecked( + EmailProperty::HasAttachment, + metadata.has_attachments, + ); } - Property::Subject => { - email.append( - Property::Subject, + EmailProperty::Subject => { + email.insert_unchecked( + EmailProperty::Subject, root_part .headers .header_value(&ArchivedHeaderName::Subject) @@ -268,9 +280,9 @@ impl EmailGet for Server { .unwrap_or_default(), ); } - Property::SentAt => { - email.append( - Property::SentAt, + EmailProperty::SentAt => { + email.insert_unchecked( + EmailProperty::SentAt, root_part .headers .header_value(&ArchivedHeaderName::Date) @@ -278,15 +290,17 @@ impl EmailGet for Server { .unwrap_or_default(), ); } - Property::MessageId | Property::InReplyTo | Property::References => { - email.append( + EmailProperty::MessageId + | EmailProperty::InReplyTo + | EmailProperty::References => { + email.insert_unchecked( property.clone(), root_part .headers .header_value(&match property { - Property::MessageId => ArchivedHeaderName::MessageId, - Property::InReplyTo => ArchivedHeaderName::InReplyTo, - Property::References => ArchivedHeaderName::References, + EmailProperty::MessageId => ArchivedHeaderName::MessageId, + EmailProperty::InReplyTo => ArchivedHeaderName::InReplyTo, + EmailProperty::References => ArchivedHeaderName::References, _ => unreachable!(), }) .map(|value| { @@ -296,23 +310,23 @@ impl EmailGet for Server { ); } - Property::Sender - | Property::From - | Property::To - | Property::Cc - | Property::Bcc - | Property::ReplyTo => { - email.append( + EmailProperty::Sender + | EmailProperty::From + | EmailProperty::To + | EmailProperty::Cc + | EmailProperty::Bcc + | EmailProperty::ReplyTo => { + email.insert_unchecked( property.clone(), root_part .headers .header_value(&match property { - Property::Sender => ArchivedHeaderName::Sender, - Property::From => ArchivedHeaderName::From, - Property::To => ArchivedHeaderName::To, - Property::Cc => ArchivedHeaderName::Cc, - Property::Bcc => ArchivedHeaderName::Bcc, - Property::ReplyTo => ArchivedHeaderName::ReplyTo, + EmailProperty::Sender => ArchivedHeaderName::Sender, + EmailProperty::From => ArchivedHeaderName::From, + EmailProperty::To => ArchivedHeaderName::To, + EmailProperty::Cc => ArchivedHeaderName::Cc, + EmailProperty::Bcc => ArchivedHeaderName::Bcc, + EmailProperty::ReplyTo => ArchivedHeaderName::ReplyTo, _ => unreachable!(), }) .map(|value| { @@ -321,27 +335,29 @@ impl EmailGet for Server { .unwrap_or_default(), ); } - Property::Header(_) => { - email.append( + EmailProperty::Header(_) => { + email.insert_unchecked( property.clone(), root_part.headers.header_to_value(property, &raw_message), ); } - Property::Headers => { - email.append( - Property::Headers, + EmailProperty::Headers => { + email.insert_unchecked( + EmailProperty::Headers, root_part.headers.headers_to_value(&raw_message), ); } - Property::TextBody | Property::HtmlBody | Property::Attachments => { + EmailProperty::TextBody + | EmailProperty::HtmlBody + | EmailProperty::Attachments => { let list = match property { - Property::TextBody => &contents.text_body, - Property::HtmlBody => &contents.html_body, - Property::Attachments => &contents.attachments, + EmailProperty::TextBody => &contents.text_body, + EmailProperty::HtmlBody => &contents.html_body, + EmailProperty::Attachments => &contents.attachments, _ => unreachable!(), } .iter(); - email.append( + email.insert_unchecked( property.clone(), list.map(|part_id| { contents.to_body_part( @@ -354,14 +370,14 @@ impl EmailGet for Server { .collect::>(), ); } - Property::BodyStructure => { - email.append( - Property::BodyStructure, + EmailProperty::BodyStructure => { + email.insert_unchecked( + EmailProperty::BodyStructure, contents.to_body_part(0, &body_properties, &raw_message, &blob_id), ); } - Property::BodyValues => { - let mut body_values = Object::with_capacity(contents.parts.len()); + EmailProperty::BodyValues => { + let mut body_values = Map::with_capacity(contents.parts.len()); for (part_id, part) in contents.parts.iter().enumerate() { if ((contents.is_html_part(part_id as u16) && (fetch_all_body_values || fetch_html_body_values)) @@ -384,19 +400,19 @@ impl EmailGet for Server { _ => unreachable!(), }; - body_values.append( - Property::_T(part_id.to_string()), - Object::with_capacity(3) - .with_property( - Property::IsEncodingProblem, + body_values.insert_unchecked( + Key::Owned(part_id.to_string()), + Map::with_capacity(3) + .with_key_value( + EmailProperty::IsEncodingProblem, part.is_encoding_problem, ) - .with_property(Property::IsTruncated, is_truncated) - .with_property(Property::Value, value), + .with_key_value(EmailProperty::IsTruncated, is_truncated) + .with_key_value(EmailProperty::Value, value), ); } } - email.append(Property::BodyValues, body_values); + email.insert_unchecked(EmailProperty::BodyValues, body_values); } _ => { @@ -406,7 +422,7 @@ impl EmailGet for Server { } } } - response.list.push(email); + response.list.push(email.into()); } Ok(response) diff --git a/crates/jmap/src/email/headers.rs b/crates/jmap/src/email/headers.rs index fc11cb69..76f9f752 100644 --- a/crates/jmap/src/email/headers.rs +++ b/crates/jmap/src/email/headers.rs @@ -4,12 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - -use jmap_proto::types::{ - property::{HeaderForm, HeaderProperty, Property}, - value::{Object, Value}, +use jmap_proto::{ + object::email::{EmailProperty, EmailValue, HeaderForm, HeaderProperty}, + types::date::UTCDate, }; +use jmap_tools::{Key, Map, Value}; use mail_builder::{ MessageBuilder, headers::{ @@ -22,18 +21,23 @@ use mail_builder::{ }, }; use mail_parser::{ - Addr, ArchivedHeader, ArchivedHeaderValue, Header, HeaderName, HeaderValue, + Addr, ArchivedHeader, ArchivedHeaderValue, DateTime, Group, Header, HeaderName, HeaderValue, parsers::MessageStream, }; +use std::borrow::Cow; use store::rkyv::vec::ArchivedVec; pub trait IntoForm { - fn into_form(self, form: &HeaderForm) -> Value; + fn into_form(self, form: &HeaderForm) -> Value<'static, EmailProperty, EmailValue>; } pub trait HeaderToValue { - fn header_to_value(&self, property: &Property, raw_message: &[u8]) -> Value; - fn headers_to_value(&self, raw_message: &[u8]) -> Value; + fn header_to_value( + &self, + property: &EmailProperty, + raw_message: &[u8], + ) -> Value<'static, EmailProperty, EmailValue>; + fn headers_to_value(&self, raw_message: &[u8]) -> Value<'static, EmailProperty, EmailValue>; } pub trait ValueToHeader<'x> { @@ -43,29 +47,37 @@ pub trait ValueToHeader<'x> { } pub trait BuildHeader: Sized { - fn build_header(self, header: HeaderProperty, value: Value) -> Result; + fn build_header( + self, + header: HeaderProperty, + value: Value<'static, EmailProperty, EmailValue>, + ) -> Result; } impl HeaderToValue for Vec> { - fn header_to_value(&self, property: &Property, raw_message: &[u8]) -> Value { + fn header_to_value( + &self, + property: &EmailProperty, + raw_message: &[u8], + ) -> Value<'static, EmailProperty, EmailValue> { let (header_name, form, all) = match property { - Property::Header(header) => ( + EmailProperty::Header(header) => ( HeaderName::parse(header.header.as_str()) .unwrap_or_else(|| HeaderName::Other(header.header.as_str().into())), header.form, header.all, ), - Property::Sender => (HeaderName::Sender, HeaderForm::Addresses, false), - Property::From => (HeaderName::From, HeaderForm::Addresses, false), - Property::To => (HeaderName::To, HeaderForm::Addresses, false), - Property::Cc => (HeaderName::Cc, HeaderForm::Addresses, false), - Property::Bcc => (HeaderName::Bcc, HeaderForm::Addresses, false), - Property::ReplyTo => (HeaderName::ReplyTo, HeaderForm::Addresses, false), - Property::Subject => (HeaderName::Subject, HeaderForm::Text, false), - Property::MessageId => (HeaderName::MessageId, HeaderForm::MessageIds, false), - Property::InReplyTo => (HeaderName::InReplyTo, HeaderForm::MessageIds, false), - Property::References => (HeaderName::References, HeaderForm::MessageIds, false), - Property::SentAt => (HeaderName::Date, HeaderForm::Date, false), + EmailProperty::Sender => (HeaderName::Sender, HeaderForm::Addresses, false), + EmailProperty::From => (HeaderName::From, HeaderForm::Addresses, false), + EmailProperty::To => (HeaderName::To, HeaderForm::Addresses, false), + EmailProperty::Cc => (HeaderName::Cc, HeaderForm::Addresses, false), + EmailProperty::Bcc => (HeaderName::Bcc, HeaderForm::Addresses, false), + EmailProperty::ReplyTo => (HeaderName::ReplyTo, HeaderForm::Addresses, false), + EmailProperty::Subject => (HeaderName::Subject, HeaderForm::Text, false), + EmailProperty::MessageId => (HeaderName::MessageId, HeaderForm::MessageIds, false), + EmailProperty::InReplyTo => (HeaderName::InReplyTo, HeaderForm::MessageIds, false), + EmailProperty::References => (HeaderName::References, HeaderForm::MessageIds, false), + EmailProperty::SentAt => (HeaderName::Date, HeaderForm::Date, false), _ => return Value::Null, }; @@ -104,18 +116,18 @@ impl HeaderToValue for Vec> { if headers.len() > 1 { headers.reverse(); } - Value::List(headers) + Value::Array(headers) } } - fn headers_to_value(&self, raw_message: &[u8]) -> Value { + fn headers_to_value(&self, raw_message: &[u8]) -> Value<'static, EmailProperty, EmailValue> { let mut headers = Vec::with_capacity(self.len()); for header in self.iter() { headers.push(Value::Object( - Object::with_capacity(2) - .with_property(Property::Name, header.name().to_string()) - .with_property( - Property::Value, + Map::with_capacity(2) + .with_key_value(EmailProperty::Name, header.name().to_string()) + .with_key_value( + EmailProperty::Value, String::from_utf8_lossy( raw_message .get(header.offset_start as usize..header.offset_end as usize) @@ -131,79 +143,77 @@ impl HeaderToValue for Vec> { } impl IntoForm for HeaderValue<'_> { - fn into_form(self, form: &HeaderForm) -> Value { + fn into_form(self, form: &HeaderForm) -> Value<'static, EmailProperty, EmailValue> { match (self, form) { - (HeaderValue::Text(text), HeaderForm::Raw | HeaderForm::Text) => text.into(), + (HeaderValue::Text(text), HeaderForm::Raw | HeaderForm::Text) => { + text.into_owned().into() + } (HeaderValue::TextList(texts), HeaderForm::Raw | HeaderForm::Text) => { texts.join(", ").into() } - (HeaderValue::Text(text), HeaderForm::MessageIds) => Value::List(vec![text.into()]), - (HeaderValue::TextList(texts), HeaderForm::MessageIds) => texts.into(), - (HeaderValue::DateTime(datetime), HeaderForm::Date) => datetime.into(), + (HeaderValue::Text(text), HeaderForm::MessageIds) => { + Value::Array(vec![text.into_owned().into()]) + } + (HeaderValue::TextList(texts), HeaderForm::MessageIds) => { + Value::Array(texts.into_iter().map(|t| t.into_owned().into()).collect()) + } + (HeaderValue::DateTime(datetime), HeaderForm::Date) => from_mail_datetime(datetime), (HeaderValue::Address(mail_parser::Address::List(addrlist)), HeaderForm::URLs) => { - Value::List( + Value::Array( addrlist .into_iter() .filter_map(|addr| match addr { Addr { address: Some(addr), .. - } if addr.contains(':') => Some(addr.into()), + } if addr.contains(':') => Some(addr.into_owned().into()), _ => None, }) .collect(), ) } (HeaderValue::Address(mail_parser::Address::List(addrlist)), HeaderForm::Addresses) => { - addrlist.into() + from_mail_addrlist(addrlist) } ( HeaderValue::Address(mail_parser::Address::Group(grouplist)), HeaderForm::Addresses, - ) => Value::List( - grouplist - .into_iter() - .flat_map(|group| group.addresses) - .map(Value::from) - .collect(), - ), + ) => from_mail_grouplist(grouplist), ( HeaderValue::Address(mail_parser::Address::List(addrlist)), HeaderForm::GroupedAddresses, - ) => Value::List(vec![ - Object::with_capacity(2) - .with_property(Property::Name, Value::Null) - .with_property(Property::Addresses, addrlist) + ) => Value::Array(vec![ + Map::with_capacity(2) + .with_key_value(EmailProperty::Name, Value::Null) + .with_key_value(EmailProperty::Addresses, from_mail_addrlist(addrlist)) .into(), ]), ( HeaderValue::Address(mail_parser::Address::Group(grouplist)), HeaderForm::GroupedAddresses, - ) => grouplist.into(), + ) => from_mail_grouplist(grouplist), _ => Value::Null, } } } -impl<'x> ValueToHeader<'x> for Value { +impl<'x> ValueToHeader<'x> for Value<'static, EmailProperty, EmailValue> { fn try_into_grouped_addresses(self) -> Option> { - let mut obj = self.try_unwrap_object()?; + let mut obj = self.into_object()?; Some(GroupedAddresses { name: obj - .0 - .remove(&Property::Name) - .and_then(|n| n.try_unwrap_string()) + .remove(&Key::Property(EmailProperty::Name)) + .and_then(|n| n.into_string()) .map(|n| n.into()), addresses: obj - .0 - .remove(&Property::Addresses)? + .remove(&Key::Property(EmailProperty::Addresses))? .try_into_address_list()?, }) } fn try_into_address_list(self) -> Option>> { - let list = self.try_unwrap_list()?; + let list = self.into_array()?; let mut addresses = Vec::with_capacity(list.len()); for value in list { addresses.push(Address::Address(value.try_into_address()?)); @@ -212,93 +222,99 @@ impl<'x> ValueToHeader<'x> for Value { } fn try_into_address(self) -> Option> { - let mut obj = self.try_unwrap_object()?; + let mut obj = self.into_object()?; Some(EmailAddress { name: obj - .0 - .remove(&Property::Name) - .and_then(|n| n.try_unwrap_string()) + .remove(&Key::Property(EmailProperty::Name)) + .and_then(|n| n.into_string()) .map(|n| n.into()), - email: obj.0.remove(&Property::Email)?.try_unwrap_string()?.into(), + email: obj + .remove(&Key::Property(EmailProperty::Email))? + .into_string()? + .into(), }) } } impl BuildHeader for MessageBuilder<'_> { - fn build_header(self, header: HeaderProperty, value: Value) -> Result { + fn build_header( + self, + header: HeaderProperty, + value: Value<'static, EmailProperty, EmailValue>, + ) -> Result { Ok(match (&header.form, header.all, value) { - (HeaderForm::Raw, false, Value::Text(value)) => { + (HeaderForm::Raw, false, Value::Str(value)) => { self.header(header.header, Raw::from(value)) } - (HeaderForm::Raw, true, Value::List(value)) => self.headers( + (HeaderForm::Raw, true, Value::Array(value)) => self.headers( header.header, value .into_iter() - .filter_map(|v| Raw::from(v.try_unwrap_string()?).into()), + .filter_map(|v| Raw::from(v.into_string()?).into()), ), - (HeaderForm::Date, false, Value::Date(value)) => { + (HeaderForm::Date, false, Value::Element(EmailValue::Date(value))) => { self.header(header.header, Date::new(value.timestamp())) } - (HeaderForm::Date, true, Value::List(value)) => self.headers( + (HeaderForm::Date, true, Value::Array(value)) => self.headers( header.header, value .into_iter() - .filter_map(|v| Date::new(v.try_unwrap_date()?.timestamp()).into()), + .filter_map(|v| Date::new(unwrap_date(v)?.timestamp()).into()), ), - (HeaderForm::Text, false, Value::Text(value)) => { + (HeaderForm::Text, false, Value::Str(value)) => { self.header(header.header, Text::from(value)) } - (HeaderForm::Text, true, Value::List(value)) => self.headers( + (HeaderForm::Text, true, Value::Array(value)) => self.headers( header.header, value .into_iter() - .filter_map(|v| Text::from(v.try_unwrap_string()?).into()), + .filter_map(|v| Text::from(v.into_string()?).into()), ), - (HeaderForm::URLs, false, Value::List(value)) => self.header( + (HeaderForm::URLs, false, Value::Array(value)) => self.header( header.header, URL { url: value .into_iter() - .filter_map(|v| Cow::from(v.try_unwrap_string()?).into()) + .filter_map(|v| Cow::from(v.into_string()?).into()) .collect(), }, ), - (HeaderForm::URLs, true, Value::List(value)) => self.headers( + (HeaderForm::URLs, true, Value::Array(value)) => self.headers( header.header, value.into_iter().filter_map(|value| { URL { url: value - .try_unwrap_list()? + .into_array()? .into_iter() - .filter_map(|v| Cow::from(v.try_unwrap_string()?).into()) + .filter_map(|v| Cow::from(v.into_string()?).into()) .collect(), } .into() }), ), - (HeaderForm::MessageIds, false, Value::List(value)) => self.header( + (HeaderForm::MessageIds, false, Value::Array(value)) => self.header( header.header, MessageId { id: value .into_iter() - .filter_map(|v| Cow::from(v.try_unwrap_string()?).into()) + .filter_map(|v| Cow::from(v.into_string()?).into()) .collect(), }, ), - (HeaderForm::MessageIds, true, Value::List(value)) => self.headers( + (HeaderForm::MessageIds, true, Value::Array(value)) => self.headers( header.header, value.into_iter().filter_map(|value| { MessageId { id: value - .try_unwrap_list()? + .into_array()? .into_iter() - .filter_map(|v| Cow::from(v.try_unwrap_string()?).into()) + .filter_map(|v| Cow::from(v.into_string()?).into()) .collect(), } .into() }), ), - (HeaderForm::Addresses, false, Value::List(value)) => self.header( + (HeaderForm::Addresses, false, Value::Array(value)) => self.header( header.header, Address::new_list( value @@ -307,13 +323,13 @@ impl BuildHeader for MessageBuilder<'_> { .collect(), ), ), - (HeaderForm::Addresses, true, Value::List(value)) => self.headers( + (HeaderForm::Addresses, true, Value::Array(value)) => self.headers( header.header, value .into_iter() .filter_map(|v| Address::new_list(v.try_into_address_list()?).into()), ), - (HeaderForm::GroupedAddresses, false, Value::List(value)) => self.header( + (HeaderForm::GroupedAddresses, false, Value::Array(value)) => self.header( header.header, Address::new_list( value @@ -322,11 +338,11 @@ impl BuildHeader for MessageBuilder<'_> { .collect(), ), ), - (HeaderForm::GroupedAddresses, true, Value::List(value)) => self.headers( + (HeaderForm::GroupedAddresses, true, Value::Array(value)) => self.headers( header.header, value.into_iter().filter_map(|v| { Address::new_list( - v.try_unwrap_list()? + v.into_array()? .into_iter() .filter_map(|v| Address::Group(v.try_into_grouped_addresses()?).into()) .collect::>(), @@ -342,25 +358,29 @@ impl BuildHeader for MessageBuilder<'_> { } impl HeaderToValue for ArchivedVec> { - fn header_to_value(&self, property: &Property, raw_message: &[u8]) -> Value { + fn header_to_value( + &self, + property: &EmailProperty, + raw_message: &[u8], + ) -> Value<'static, EmailProperty, EmailValue> { let (header_name, form, all) = match property { - Property::Header(header) => ( + EmailProperty::Header(header) => ( HeaderName::parse(header.header.as_str()) .unwrap_or_else(|| HeaderName::Other(header.header.as_str().into())), header.form, header.all, ), - Property::Sender => (HeaderName::Sender, HeaderForm::Addresses, false), - Property::From => (HeaderName::From, HeaderForm::Addresses, false), - Property::To => (HeaderName::To, HeaderForm::Addresses, false), - Property::Cc => (HeaderName::Cc, HeaderForm::Addresses, false), - Property::Bcc => (HeaderName::Bcc, HeaderForm::Addresses, false), - Property::ReplyTo => (HeaderName::ReplyTo, HeaderForm::Addresses, false), - Property::Subject => (HeaderName::Subject, HeaderForm::Text, false), - Property::MessageId => (HeaderName::MessageId, HeaderForm::MessageIds, false), - Property::InReplyTo => (HeaderName::InReplyTo, HeaderForm::MessageIds, false), - Property::References => (HeaderName::References, HeaderForm::MessageIds, false), - Property::SentAt => (HeaderName::Date, HeaderForm::Date, false), + EmailProperty::Sender => (HeaderName::Sender, HeaderForm::Addresses, false), + EmailProperty::From => (HeaderName::From, HeaderForm::Addresses, false), + EmailProperty::To => (HeaderName::To, HeaderForm::Addresses, false), + EmailProperty::Cc => (HeaderName::Cc, HeaderForm::Addresses, false), + EmailProperty::Bcc => (HeaderName::Bcc, HeaderForm::Addresses, false), + EmailProperty::ReplyTo => (HeaderName::ReplyTo, HeaderForm::Addresses, false), + EmailProperty::Subject => (HeaderName::Subject, HeaderForm::Text, false), + EmailProperty::MessageId => (HeaderName::MessageId, HeaderForm::MessageIds, false), + EmailProperty::InReplyTo => (HeaderName::InReplyTo, HeaderForm::MessageIds, false), + EmailProperty::References => (HeaderName::References, HeaderForm::MessageIds, false), + EmailProperty::SentAt => (HeaderName::Date, HeaderForm::Date, false), _ => return Value::Null, }; @@ -402,18 +422,18 @@ impl HeaderToValue for ArchivedVec> { if headers.len() > 1 { headers.reverse(); } - Value::List(headers) + Value::Array(headers) } } - fn headers_to_value(&self, raw_message: &[u8]) -> Value { + fn headers_to_value(&self, raw_message: &[u8]) -> Value<'static, EmailProperty, EmailValue> { let mut headers = Vec::with_capacity(self.len()); for header in self.iter() { headers.push(Value::Object( - Object::with_capacity(2) - .with_property(Property::Name, header.name.to_string()) - .with_property( - Property::Value, + Map::with_capacity(2) + .with_key_value(EmailProperty::Name, header.name.to_string()) + .with_key_value( + EmailProperty::Value, String::from_utf8_lossy( raw_message .get( @@ -444,3 +464,65 @@ impl ByteTrim for &[u8] { &self[..end] } } + +#[inline] +fn unwrap_date(value: Value<'static, EmailProperty, EmailValue>) -> Option { + match value { + Value::Element(EmailValue::Date(date)) => Some(date), + _ => None, + } +} + +fn from_mail_datetime(date: DateTime) -> Value<'static, EmailProperty, EmailValue> { + Value::Element(EmailValue::Date(UTCDate { + year: date.year, + month: date.month, + day: date.day, + hour: date.hour, + minute: date.minute, + second: date.second, + tz_before_gmt: date.tz_before_gmt, + tz_hour: date.tz_hour, + tz_minute: date.tz_minute, + })) +} + +fn from_mail_addr(value: Addr<'_>) -> Value<'static, EmailProperty, EmailValue> { + Value::Object( + Map::with_capacity(2) + .with_key_value(EmailProperty::Name, value.name.map(|v| v.into_owned())) + .with_key_value( + EmailProperty::Email, + value.address.unwrap_or_default().into_owned(), + ), + ) +} + +fn from_mail_group(group: Group<'_>) -> Value<'static, EmailProperty, EmailValue> { + Value::Object( + Map::with_capacity(2) + .with_key_value(EmailProperty::Name, group.name.map(|v| v.into_owned())) + .with_key_value( + EmailProperty::Addresses, + from_mail_addrlist(group.addresses), + ), + ) +} + +fn from_mail_addrlist(addrlist: Vec>) -> Value<'static, EmailProperty, EmailValue> { + Value::Array( + addrlist + .into_iter() + .map(from_mail_addr) + .collect::>>(), + ) +} + +fn from_mail_grouplist(grouplist: Vec>) -> Value<'static, EmailProperty, EmailValue> { + Value::Array( + grouplist + .into_iter() + .map(from_mail_group) + .collect::>>(), + ) +} diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 518b8acf..bea68c2a 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -16,7 +16,7 @@ use http_proto::HttpSessionData; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::import::{ImportEmailRequest, ImportEmailResponse}, - types::{property::Property, state::State}, + types::state::State, }; use mail_parser::MessageParser; use std::future::Future; @@ -89,7 +89,7 @@ impl EmailImport for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description("Message must belong to at least one mailbox."), ); continue; @@ -99,7 +99,7 @@ impl EmailImport for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description(format!( "Mailbox {} does not exist.", Id::from(*mailbox_id) diff --git a/crates/jmap/src/email/mod.rs b/crates/jmap/src/email/mod.rs index ec5dc104..e9e1bb16 100644 --- a/crates/jmap/src/email/mod.rs +++ b/crates/jmap/src/email/mod.rs @@ -5,10 +5,8 @@ */ use email::message::ingest::IngestedEmail; -use jmap_proto::types::{ - property::Property, - value::{Object, Value}, -}; +use jmap_proto::object::email::{EmailProperty, EmailValue}; +use jmap_tools::Map; use types::id::Id; pub mod body; @@ -21,13 +19,13 @@ pub mod query; pub mod set; pub mod snippet; -fn ingested_into_object(email: IngestedEmail) -> Object { - Object::with_capacity(3) - .with_property( - Property::Id, +fn ingested_into_object(email: IngestedEmail) -> Map<'static, EmailProperty, EmailValue> { + Map::with_capacity(3) + .with_key_value( + EmailProperty::Id, Id::from_parts(email.thread_id, email.document_id), ) - .with_property(Property::ThreadId, Id::from(email.thread_id)) - .with_property(Property::BlobId, email.blob_id) - .with_property(Property::Size, email.size) + .with_key_value(EmailProperty::ThreadId, Id::from(email.thread_id)) + .with_key_value(EmailProperty::BlobId, email.blob_id) + .with_key_value(EmailProperty::Size, email.size) } diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs index efe9ef87..182b60d8 100644 --- a/crates/jmap/src/email/parse.rs +++ b/crates/jmap/src/email/parse.rs @@ -8,10 +8,7 @@ use common::{Server, auth::AccessToken}; use email::message::index::PREVIEW_LENGTH; use jmap_proto::{ method::parse::{ParseEmailRequest, ParseEmailResponse}, - types::{ - property::Property, - value::{Object, Value}, - }, + object::email::EmailProperty, }; use mail_parser::{ MessageParser, PartType, decoders::html::html_to_text, parsers::preview::preview_text, @@ -45,40 +42,40 @@ impl EmailParse for Server { } let properties = request.properties.unwrap_or_else(|| { vec![ - Property::BlobId, - Property::Size, - Property::ReceivedAt, - Property::MessageId, - Property::InReplyTo, - Property::References, - Property::Sender, - Property::From, - Property::To, - Property::Cc, - Property::Bcc, - Property::ReplyTo, - Property::Subject, - Property::SentAt, - Property::HasAttachment, - Property::Preview, - Property::BodyValues, - Property::TextBody, - Property::HtmlBody, - Property::Attachments, + EmailProperty::BlobId, + EmailProperty::Size, + EmailProperty::ReceivedAt, + EmailProperty::MessageId, + EmailProperty::InReplyTo, + EmailProperty::References, + EmailProperty::Sender, + EmailProperty::From, + EmailProperty::To, + EmailProperty::Cc, + EmailProperty::Bcc, + EmailProperty::ReplyTo, + EmailProperty::Subject, + EmailProperty::SentAt, + EmailProperty::HasAttachment, + EmailProperty::Preview, + EmailProperty::BodyValues, + EmailProperty::TextBody, + EmailProperty::HtmlBody, + EmailProperty::Attachments, ] }); let body_properties = request.body_properties.unwrap_or_else(|| { vec![ - Property::PartId, - Property::BlobId, - Property::Size, - Property::Name, - Property::Type, - Property::Charset, - Property::Disposition, - Property::Cid, - Property::Language, - Property::Location, + EmailProperty::PartId, + EmailProperty::BlobId, + EmailProperty::Size, + EmailProperty::Name, + EmailProperty::Type, + EmailProperty::Charset, + EmailProperty::Disposition, + EmailProperty::Cid, + EmailProperty::Language, + EmailProperty::Location, ] }); let fetch_text_body_values = request.fetch_text_body_values.unwrap_or(false); @@ -110,19 +107,19 @@ impl EmailParse for Server { }; // Prepare response - let mut email = Object::with_capacity(properties.len()); + let mut email = Map::with_capacity(properties.len()); for property in &properties { match property { - Property::BlobId => { - email.append(Property::BlobId, blob_id.clone()); + EmailProperty::BlobId => { + email.append(EmailProperty::BlobId, blob_id.clone()); } - Property::Size => { - email.append(Property::Size, Value::UnsignedInt(raw_message.len() as u64)); + EmailProperty::Size => { + email.append(EmailProperty::Size, Value::Number(raw_message.len() as u64)); } - Property::HasAttachment => { + EmailProperty::HasAttachment => { email.append( - Property::HasAttachment, + EmailProperty::HasAttachment, Value::Bool(message.parts.iter().enumerate().any(|(part_id, part)| { let part_id = part_id as u32; match &part.body { @@ -136,9 +133,9 @@ impl EmailParse for Server { })), ); } - Property::Preview => { + EmailProperty::Preview => { email.append( - Property::Preview, + EmailProperty::Preview, match message .text_body .first() @@ -159,18 +156,18 @@ impl EmailParse for Server { }, ); } - Property::MessageId - | Property::InReplyTo - | Property::References - | Property::Sender - | Property::From - | Property::To - | Property::Cc - | Property::Bcc - | Property::ReplyTo - | Property::Subject - | Property::SentAt - | Property::Header(_) => { + EmailProperty::MessageId + | EmailProperty::InReplyTo + | EmailProperty::References + | EmailProperty::Sender + | EmailProperty::From + | EmailProperty::To + | EmailProperty::Cc + | EmailProperty::Bcc + | EmailProperty::ReplyTo + | EmailProperty::Subject + | EmailProperty::SentAt + | EmailProperty::Header(_) => { email.append( property.clone(), message.parts[0] @@ -178,17 +175,19 @@ impl EmailParse for Server { .header_to_value(property, &raw_message), ); } - Property::Headers => { + EmailProperty::Headers => { email.append( - Property::Headers, + EmailProperty::Headers, message.parts[0].headers.headers_to_value(&raw_message), ); } - Property::TextBody | Property::HtmlBody | Property::Attachments => { + EmailProperty::TextBody + | EmailProperty::HtmlBody + | EmailProperty::Attachments => { let list = match property { - Property::TextBody => &message.text_body, - Property::HtmlBody => &message.html_body, - Property::Attachments => &message.attachments, + EmailProperty::TextBody => &message.text_body, + EmailProperty::HtmlBody => &message.html_body, + EmailProperty::Attachments => &message.attachments, _ => unreachable!(), } .iter(); @@ -205,16 +204,16 @@ impl EmailParse for Server { .collect::>(), ); } - Property::BodyStructure => { + EmailProperty::BodyStructure => { email.append( - Property::BodyStructure, + EmailProperty::BodyStructure, message .parts .to_body_part(0, &body_properties, &raw_message, &blob_id), ); } - Property::BodyValues => { - let mut body_values = Object::with_capacity(message.parts.len()); + EmailProperty::BodyValues => { + let mut body_values = Map::with_capacity(message.parts.len()); for (part_id, part) in message.parts.iter().enumerate() { let part_id = part_id as u32; if ((message.html_body.contains(&part_id) @@ -226,24 +225,24 @@ impl EmailParse for Server { let (is_truncated, value) = part.body.truncate(max_body_value_bytes); body_values.append( - Property::_T(part_id.to_string()), - Object::with_capacity(3) - .with_property( - Property::IsEncodingProblem, + EmailProperty::_T(part_id.to_string()), + Map::with_capacity(3) + .with_key_value( + EmailProperty::IsEncodingProblem, part.is_encoding_problem, ) - .with_property(Property::IsTruncated, is_truncated) - .with_property(Property::Value, value), + .with_key_value(EmailProperty::IsTruncated, is_truncated) + .with_key_value(EmailProperty::Value, value), ); } } - email.append(Property::BodyValues, body_values); + email.append(EmailProperty::BodyValues, body_values); } - Property::Id - | Property::ThreadId - | Property::Keywords - | Property::MailboxIds - | Property::ReceivedAt => { + EmailProperty::Id + | EmailProperty::ThreadId + | EmailProperty::Keywords + | EmailProperty::MailboxIds + | EmailProperty::ReceivedAt => { email.append(property.clone(), Value::Null); } diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index 3f342dc7..9ba8c726 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -8,8 +8,8 @@ use crate::{JmapMethods, changes::state::MessageCacheState}; use common::{MessageStoreCache, Server, auth::AccessToken}; use email::cache::{MessageCacheFetch, email::MessageCacheAccess}; use jmap_proto::{ - method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, - object::email::QueryArguments, + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::email::{Email, EmailComparator, EmailFilter}, }; use mail_parser::HeaderName; use nlp::language::Language; @@ -27,7 +27,7 @@ use types::{acl::Acl, collection::Collection, field::EmailField, keyword::Keywor pub trait EmailQuery: Sync + Send { fn email_query( &self, - request: QueryRequest, + request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -35,7 +35,7 @@ pub trait EmailQuery: Sync + Send { impl EmailQuery for Server { async fn email_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { let account_id = request.account_id.document_id(); @@ -51,129 +51,135 @@ impl EmailQuery for Server { let mut fts_filters = Vec::with_capacity(filters.len()); for cond in conds { match cond { - Filter::Text(text) => { - fts_filters.push(FtsFilter::Or); - fts_filters.push(FtsFilter::has_text( + Filter::Property(cond) => match cond { + EmailFilter::Text(text) => { + fts_filters.push(FtsFilter::Or); + fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::From), + &text, + Language::None, + )); + fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::To), + &text, + Language::None, + )); + fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::Cc), + &text, + Language::None, + )); + fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::Bcc), + &text, + Language::None, + )); + fts_filters.push(FtsFilter::has_text_detect( + Field::Header(HeaderName::Subject), + &text, + self.core.jmap.default_language, + )); + fts_filters.push(FtsFilter::has_text_detect( + Field::Body, + &text, + self.core.jmap.default_language, + )); + fts_filters.push(FtsFilter::has_text_detect( + Field::Attachment, + text, + self.core.jmap.default_language, + )); + fts_filters.push(FtsFilter::End); + } + EmailFilter::From(text) => fts_filters.push(FtsFilter::has_text( Field::Header(HeaderName::From), - &text, - Language::None, - )); - fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::To), - &text, - Language::None, - )); - fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::Cc), - &text, - Language::None, - )); - fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::Bcc), - &text, - Language::None, - )); - fts_filters.push(FtsFilter::has_text_detect( - Field::Header(HeaderName::Subject), - &text, - self.core.jmap.default_language, - )); - fts_filters.push(FtsFilter::has_text_detect( - Field::Body, - &text, - self.core.jmap.default_language, - )); - fts_filters.push(FtsFilter::has_text_detect( - Field::Attachment, text, - self.core.jmap.default_language, - )); - fts_filters.push(FtsFilter::End); - } - Filter::From(text) => fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::From), - text, - Language::None, - )), - Filter::To(text) => fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::To), - text, - Language::None, - )), - Filter::Cc(text) => fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::Cc), - text, - Language::None, - )), - Filter::Bcc(text) => fts_filters.push(FtsFilter::has_text( - Field::Header(HeaderName::Bcc), - text, - Language::None, - )), - Filter::Subject(text) => fts_filters.push(FtsFilter::has_text_detect( - Field::Header(HeaderName::Subject), - text, - self.core.jmap.default_language, - )), - Filter::Body(text) => fts_filters.push(FtsFilter::has_text_detect( - Field::Body, - text, - self.core.jmap.default_language, - )), - Filter::Header(header) => { - let mut header = header.into_iter(); - let header_name = header.next().ok_or_else(|| { - trc::JmapEvent::InvalidArguments - .into_err() - .details("Header name is missing.".to_string()) - })?; + Language::None, + )), + EmailFilter::To(text) => fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::To), + text, + Language::None, + )), + EmailFilter::Cc(text) => fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::Cc), + text, + Language::None, + )), + EmailFilter::Bcc(text) => fts_filters.push(FtsFilter::has_text( + Field::Header(HeaderName::Bcc), + text, + Language::None, + )), + EmailFilter::Subject(text) => { + fts_filters.push(FtsFilter::has_text_detect( + Field::Header(HeaderName::Subject), + text, + self.core.jmap.default_language, + )) + } + EmailFilter::Body(text) => { + fts_filters.push(FtsFilter::has_text_detect( + Field::Body, + text, + self.core.jmap.default_language, + )) + } + EmailFilter::Header(header) => { + let mut header = header.into_iter(); + let header_name = header.next().ok_or_else(|| { + trc::JmapEvent::InvalidArguments + .into_err() + .details("Header name is missing.".to_string()) + })?; - match HeaderName::parse(header_name) { - Some(HeaderName::Other(header_name)) => { - return Err(trc::JmapEvent::InvalidArguments + match HeaderName::parse(header_name) { + Some(HeaderName::Other(header_name)) => { + return Err(trc::JmapEvent::InvalidArguments .into_err() .details(format!( "Querying header '{header_name}' is not supported.", ))); - } - Some(header_name) => { - if let Some(header_value) = header.next() { - if matches!( - header_name, - HeaderName::MessageId - | HeaderName::InReplyTo - | HeaderName::References - | HeaderName::ResentMessageId - ) { - fts_filters.push(FtsFilter::has_keyword( - Field::Header(header_name), - header_value, - )); + } + Some(header_name) => { + if let Some(header_value) = header.next() { + if matches!( + header_name, + HeaderName::MessageId + | HeaderName::InReplyTo + | HeaderName::References + | HeaderName::ResentMessageId + ) { + fts_filters.push(FtsFilter::has_keyword( + Field::Header(header_name), + header_value, + )); + } else { + fts_filters.push(FtsFilter::has_text( + Field::Header(header_name), + header_value, + Language::None, + )); + } } else { - fts_filters.push(FtsFilter::has_text( - Field::Header(header_name), - header_value, - Language::None, + fts_filters.push(FtsFilter::has_keyword( + Field::Keyword, + header_name.as_str().to_lowercase(), )); } - } else { - fts_filters.push(FtsFilter::has_keyword( - Field::Keyword, - header_name.as_str().to_lowercase(), - )); } + None => (), } - None => (), } - } + other => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(other.to_string())); + } + }, Filter::And | Filter::Or | Filter::Not | Filter::Close => { fts_filters.push(cond.into()); } - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); - } } } filters.push(query::Filter::is_in_set( @@ -183,116 +189,132 @@ impl EmailQuery for Server { } FilterGroup::Store(cond) => { match cond { - Filter::InMailbox(mailbox) => { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cached_messages - .in_mailbox(mailbox.document_id()) - .map(|item| item.document_id), - ))) - } - Filter::InMailboxOtherThan(mailboxes) => { - filters.push(query::Filter::Not); - filters.push(query::Filter::Or); - for mailbox in mailboxes { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cached_messages - .in_mailbox(mailbox.document_id()) - .map(|item| item.document_id), - ))); - } - filters.push(query::Filter::End); - filters.push(query::Filter::End); - } - Filter::Before(date) => filters - .push(query::Filter::lt(EmailField::ReceivedAt, date.serialize())), - Filter::After(date) => filters - .push(query::Filter::gt(EmailField::ReceivedAt, date.serialize())), - Filter::MinSize(size) => { - filters.push(query::Filter::ge(EmailField::Size, size.serialize())) - } - Filter::MaxSize(size) => { - filters.push(query::Filter::lt(EmailField::Size, size.serialize())) - } - Filter::AllInThreadHaveKeyword(keyword) => { - filters.push(query::Filter::is_in_set(thread_keywords( - &cached_messages, - keyword, - true, - ))) - } - Filter::SomeInThreadHaveKeyword(keyword) => { - filters.push(query::Filter::is_in_set(thread_keywords( - &cached_messages, - keyword, - false, - ))) - } - Filter::NoneInThreadHaveKeyword(keyword) => { - filters.push(query::Filter::Not); - filters.push(query::Filter::is_in_set(thread_keywords( - &cached_messages, - keyword, - false, - ))); - filters.push(query::Filter::End); - } - Filter::HasKeyword(keyword) => { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cached_messages - .with_keyword(&keyword) - .map(|item| item.document_id), - ))); - } - Filter::NotKeyword(keyword) => { - filters.push(query::Filter::Not); - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cached_messages - .with_keyword(&keyword) - .map(|item| item.document_id), - ))); - filters.push(query::Filter::End); - } - Filter::HasAttachment(has_attach) => { - if !has_attach { - filters.push(query::Filter::Not); - } - filters - .push(query::Filter::is_in_bitmap(EmailField::HasAttachment, ())); - if !has_attach { - filters.push(query::Filter::End); + Filter::Property(cond) => { + match cond { + EmailFilter::InMailbox(mailbox) => filters.push( + query::Filter::is_in_set(RoaringBitmap::from_iter( + cached_messages + .in_mailbox(mailbox.document_id()) + .map(|item| item.document_id), + )), + ), + EmailFilter::InMailboxOtherThan(mailboxes) => { + filters.push(query::Filter::Not); + filters.push(query::Filter::Or); + for mailbox in mailboxes { + filters.push(query::Filter::is_in_set( + RoaringBitmap::from_iter( + cached_messages + .in_mailbox(mailbox.document_id()) + .map(|item| item.document_id), + ), + )); + } + filters.push(query::Filter::End); + filters.push(query::Filter::End); + } + EmailFilter::Before(date) => filters.push(query::Filter::lt( + EmailField::ReceivedAt, + date.timestamp().serialize(), + )), + EmailFilter::After(date) => filters.push(query::Filter::gt( + EmailField::ReceivedAt, + date.timestamp().serialize(), + )), + EmailFilter::MinSize(size) => filters + .push(query::Filter::ge(EmailField::Size, size.serialize())), + EmailFilter::MaxSize(size) => filters + .push(query::Filter::lt(EmailField::Size, size.serialize())), + EmailFilter::AllInThreadHaveKeyword(keyword) => { + filters.push(query::Filter::is_in_set(thread_keywords( + &cached_messages, + keyword, + true, + ))) + } + EmailFilter::SomeInThreadHaveKeyword(keyword) => { + filters.push(query::Filter::is_in_set(thread_keywords( + &cached_messages, + keyword, + false, + ))) + } + EmailFilter::NoneInThreadHaveKeyword(keyword) => { + filters.push(query::Filter::Not); + filters.push(query::Filter::is_in_set(thread_keywords( + &cached_messages, + keyword, + false, + ))); + filters.push(query::Filter::End); + } + EmailFilter::HasKeyword(keyword) => { + filters.push(query::Filter::is_in_set( + RoaringBitmap::from_iter( + cached_messages + .with_keyword(&keyword) + .map(|item| item.document_id), + ), + )); + } + EmailFilter::NotKeyword(keyword) => { + filters.push(query::Filter::Not); + filters.push(query::Filter::is_in_set( + RoaringBitmap::from_iter( + cached_messages + .with_keyword(&keyword) + .map(|item| item.document_id), + ), + )); + filters.push(query::Filter::End); + } + EmailFilter::HasAttachment(has_attach) => { + if !has_attach { + filters.push(query::Filter::Not); + } + filters.push(query::Filter::is_in_bitmap( + EmailField::HasAttachment, + (), + )); + if !has_attach { + filters.push(query::Filter::End); + } + } + + // Non-standard + EmailFilter::Id(ids) => { + let mut set = RoaringBitmap::new(); + for id in ids { + set.insert(id.document_id()); + } + filters.push(query::Filter::is_in_set(set)); + } + EmailFilter::SentBefore(date) => filters.push(query::Filter::lt( + EmailField::SentAt, + date.timestamp().serialize(), + )), + EmailFilter::SentAfter(date) => filters.push(query::Filter::gt( + EmailField::SentAt, + date.timestamp().serialize(), + )), + EmailFilter::InThread(id) => filters.push( + query::Filter::is_in_set(RoaringBitmap::from_iter( + cached_messages + .in_thread(id.document_id()) + .map(|item| item.document_id), + )), + ), + other => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(other.to_string())); + } } } - // Non-standard - Filter::Id(ids) => { - let mut set = RoaringBitmap::new(); - for id in ids { - set.insert(id.document_id()); - } - filters.push(query::Filter::is_in_set(set)); - } - Filter::SentBefore(date) => { - filters.push(query::Filter::lt(EmailField::SentAt, date.serialize())) - } - Filter::SentAfter(date) => { - filters.push(query::Filter::gt(EmailField::SentAt, date.serialize())) - } - Filter::InThread(id) => { - filters.push(query::Filter::is_in_set(RoaringBitmap::from_iter( - cached_messages - .in_thread(id.document_id()) - .map(|item| item.document_id), - ))) - } Filter::And | Filter::Or | Filter::Not | Filter::Close => { filters.push(cond.into()); } - - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); - } } } } @@ -312,53 +334,45 @@ impl EmailQuery for Server { for comparator in request .sort .and_then(|s| if !s.is_empty() { s.into() } else { None }) - .unwrap_or_else(|| vec![Comparator::descending(SortProperty::ReceivedAt)]) + .unwrap_or_else(|| vec![Comparator::descending(EmailComparator::ReceivedAt)]) { comparators.push(match comparator.property { - SortProperty::ReceivedAt => { + EmailComparator::ReceivedAt => { query::Comparator::field(EmailField::ReceivedAt, comparator.is_ascending) } - SortProperty::Size => { + EmailComparator::Size => { query::Comparator::field(EmailField::Size, comparator.is_ascending) } - SortProperty::From => { + EmailComparator::From => { query::Comparator::field(EmailField::From, comparator.is_ascending) } - SortProperty::To => { + EmailComparator::To => { query::Comparator::field(EmailField::To, comparator.is_ascending) } - SortProperty::Subject => { + EmailComparator::Subject => { query::Comparator::field(EmailField::Subject, comparator.is_ascending) } - SortProperty::SentAt => { + EmailComparator::SentAt => { query::Comparator::field(EmailField::SentAt, comparator.is_ascending) } - SortProperty::HasKeyword => query::Comparator::set( + EmailComparator::HasKeyword(keyword) => query::Comparator::set( RoaringBitmap::from_iter( cached_messages - .with_keyword(&comparator.keyword.unwrap_or(Keyword::Seen)) + .with_keyword(&keyword) .map(|item| item.document_id), ), comparator.is_ascending, ), - SortProperty::AllInThreadHaveKeyword => query::Comparator::set( - thread_keywords( - &cached_messages, - comparator.keyword.unwrap_or(Keyword::Seen), - true, - ), + EmailComparator::AllInThreadHaveKeyword(keyword) => query::Comparator::set( + thread_keywords(&cached_messages, keyword, true), comparator.is_ascending, ), - SortProperty::SomeInThreadHaveKeyword => query::Comparator::set( - thread_keywords( - &cached_messages, - comparator.keyword.unwrap_or(Keyword::Seen), - false, - ), + EmailComparator::SomeInThreadHaveKeyword(keyword) => query::Comparator::set( + thread_keywords(&cached_messages, keyword, false), comparator.is_ascending, ), // Non-standard - SortProperty::Cc => { + EmailComparator::Cc => { query::Comparator::field(EmailField::Cc, comparator.is_ascending) } diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index f138672e..0b4a484c 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -4,8 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, collections::HashMap}; - use super::headers::{BuildHeader, ValueToHeader}; use crate::{ JmapMethods, blob::download::BlobDownload, changes::state::MessageCacheState, @@ -24,13 +22,9 @@ use email::{ use http_proto::HttpSessionData; use jmap_proto::{ error::set::{SetError, SetErrorType}, - method::set::{RequestArguments, SetRequest, SetResponse}, - response::references::EvalObjectReferences, - types::{ - property::Property, - state::State, - value::{MaybePatchValue, SetValue, Value}, - }, + method::set::{SetRequest, SetResponse}, + object::email::Email, + types::state::State, }; use mail_builder::{ MessageBuilder, @@ -42,6 +36,7 @@ use mail_builder::{ }; use mail_parser::MessageParser; use std::future::Future; +use std::{borrow::Cow, collections::HashMap}; use store::{ahash::AHashMap, roaring::RoaringBitmap, write::BatchBuilder}; use trc::AddContext; use types::{ @@ -54,19 +49,19 @@ use types::{ pub trait EmailSet: Sync + Send { fn email_set( &self, - request: SetRequest, + request: SetRequest<'_, Email>, access_token: &AccessToken, session: &HttpSessionData, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl EmailSet for Server { async fn email_set( &self, - mut request: SetRequest, + mut request: SetRequest<'_, Email>, access_token: &AccessToken, session: &HttpSessionData, - ) -> trc::Result { + ) -> trc::Result> { // Prepare response let account_id = request.account_id.document_id(); let cache = self.get_cached_messages(account_id).await?; @@ -127,7 +122,7 @@ impl EmailSet for Server { let mut values = HashMap::with_capacity(obj.0.len()); for (key, value) in obj.0 { if let (Property::_T(id), Value::Object(mut bv)) = (key, value) { - values.insert(id, bv.0.remove(&Property::Value)?.try_unwrap_string()?); + values.insert(id, bv.0.remove(&Property::Value)?.into_string()?); } else { return None; } @@ -149,7 +144,7 @@ impl EmailSet for Server { } }; match (property, value) { - (Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => { + (Property::MailboxIds, MaybePatchValue::Value(Value::Array(ids))) => { mailboxes = ids .into_iter() .filter_map(|id| id.try_unwrap_id()?.document_id().into()) @@ -170,7 +165,7 @@ impl EmailSet for Server { } } - (Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => { + (Property::Keywords, MaybePatchValue::Value(Value::Array(keywords_))) => { keywords = keywords_ .into_iter() .filter_map(|keyword| keyword.try_unwrap_keyword()) @@ -192,14 +187,14 @@ impl EmailSet for Server { ( header @ (Property::MessageId | Property::InReplyTo | Property::References), - MaybePatchValue::Value(Value::List(values)), + MaybePatchValue::Value(Value::Array(values)), ) => { builder = builder.header( header.as_rfc_header(), MessageId { id: values .into_iter() - .filter_map(|value| value.try_unwrap_string()) + .filter_map(|value| value.into_string()) .map(|value| value.into()) .collect(), }, @@ -223,7 +218,7 @@ impl EmailSet for Server { continue 'create; } } - (Property::Subject, MaybePatchValue::Value(Value::Text(value))) => { + (Property::Subject, MaybePatchValue::Value(Value::Str(value))) => { builder = builder.subject(value); } @@ -246,7 +241,7 @@ impl EmailSet for Server { let (values, expected_content_type) = match property { Property::BodyStructure => (vec![value], None), Property::TextBody | Property::HtmlBody if !has_body_structure => { - let values = value.try_unwrap_list().unwrap_or_default(); + let values = value.into_array().unwrap_or_default(); if values.len() <= 1 { ( values, @@ -260,14 +255,14 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Only one part is allowed."), ); continue 'create; } } Property::Attachments if !has_body_structure => { - (value.try_unwrap_list().unwrap_or_default(), None) + (value.into_array().unwrap_or_default(), None) } _ => { response.not_created.append( @@ -298,34 +293,34 @@ impl EmailSet for Server { let mut has_size = false; let mut headers: Vec<(Cow, HeaderType)> = Vec::new(); - if let Some(obj) = value.try_unwrap_object() { + if let Some(obj) = value.into_object() { for (body_property, value) in obj.0 { match (body_property, value) { - (Property::Type, Value::Text(value)) => { + (Property::Type, Value::Str(value)) => { content_type = value.into(); } - (Property::PartId, Value::Text(value)) => { + (Property::PartId, Value::Str(value)) => { part_id = value.into(); } (Property::BlobId, Value::BlobId(value)) => { blob_id = value.into(); } - (Property::Disposition, Value::Text(value)) => { + (Property::Disposition, Value::Str(value)) => { content_disposition = value.into(); } - (Property::Name, Value::Text(value)) => { + (Property::Name, Value::Str(value)) => { name = value.into(); } - (Property::Charset, Value::Text(value)) => { + (Property::Charset, Value::Str(value)) => { charset = value.into(); } - (Property::Language, Value::List(values)) => { + (Property::Language, Value::Array(values)) => { headers.push(( "Content-Language".into(), Text::new( values .into_iter() - .filter_map(|v| v.try_unwrap_string()) + .filter_map(|v| v.into_string()) .fold( String::with_capacity(64), |mut h, v| { @@ -340,19 +335,19 @@ impl EmailSet for Server { .into(), )); } - (Property::Cid, Value::Text(value)) => { + (Property::Cid, Value::Str(value)) => { headers.push(( "Content-ID".into(), MessageId::new(value).into(), )); } - (Property::Location, Value::Text(value)) => { + (Property::Location, Value::Str(value)) => { headers.push(( "Content-Location".into(), Text::new(value).into(), )); } - (Property::Header(header), Value::Text(value)) + (Property::Header(header), Value::Str(value)) if !header.header.eq_ignore_ascii_case( "content-transfer-encoding", ) => @@ -362,13 +357,13 @@ impl EmailSet for Server { Raw::from(value).into(), )); } - (Property::Header(header), Value::List(values)) + (Property::Header(header), Value::Array(values)) if !header.header.eq_ignore_ascii_case( "content-transfer-encoding", ) => { for value in values { - if let Some(value) = value.try_unwrap_string() { + if let Some(value) = value.into_string() { headers.push(( header.header.clone().into(), Raw::from(value).into(), @@ -380,7 +375,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(( + .with_key_value(( property, Property::Headers, )) @@ -393,14 +388,14 @@ impl EmailSet for Server { (Property::Size, _) => { has_size = true; } - (Property::SubParts, Value::List(values)) => { + (Property::SubParts, Value::Array(values)) => { subparts = values.into(); } (body_property, value) if value != Value::Null => { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, body_property)) + .with_key_value((property, body_property)) .with_description("Cannot set property."), ); continue 'create; @@ -419,7 +414,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, Property::Type)) + .with_key_value((property, Property::Type)) .with_description("Multiparts can only be set with bodyStructure."), ); continue 'create; @@ -431,7 +426,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, Property::Type)) + .with_key_value((property, Property::Type)) .with_description(format!( "Expected one body part of type \"{}\"", expected_content_type.unwrap() @@ -465,7 +460,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, Property::Size)) + .with_key_value((property, Property::Size)) .with_description( "Cannot specify \"size\" when providing a \"partId\".", ), @@ -498,7 +493,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, Property::Charset)) + .with_key_value((property, Property::Charset)) .with_description( "Cannot specify a character set when providing a \"partId\".", ), @@ -571,7 +566,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property((property, Property::PartId)) + .with_key_value((property, Property::PartId)) .with_description(format!( "Missing body value for partId {part_id:?}" )), @@ -596,7 +591,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description(format!( "Message exceeds maximum size of {} bytes.", self.core.jmap.mail_attachments_max_size @@ -664,7 +659,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description("Message has to belong to at least one mailbox."), ); continue 'create; @@ -676,7 +671,7 @@ impl EmailSet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description(format!("mailboxId {mailbox_id} does not exist.")), ); continue 'create; @@ -791,7 +786,7 @@ impl EmailSet for Server { } }; match (property, value) { - (Property::MailboxIds, MaybePatchValue::Value(Value::List(ids))) => { + (Property::MailboxIds, MaybePatchValue::Value(Value::Array(ids))) => { new_data.set_mailboxes( ids.into_iter() .filter_map(|id| { @@ -811,7 +806,7 @@ impl EmailSet for Server { } } } - (Property::Keywords, MaybePatchValue::Value(Value::List(keywords_))) => { + (Property::Keywords, MaybePatchValue::Value(Value::Array(keywords_))) => { new_data.set_keywords( keywords_ .into_iter() @@ -880,7 +875,7 @@ impl EmailSet for Server { response.not_updated.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description("Message has to belong to at least one mailbox."), ); continue 'update; @@ -907,7 +902,7 @@ impl EmailSet for Server { response.not_updated.append( id, SetError::invalid_properties() - .with_property(Property::MailboxIds) + .with_key_value(Property::MailboxIds) .with_description(format!( "mailboxId {} does not exist.", mailbox_id.mailbox_id diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index d03b1b4d..f5d89cd9 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -4,16 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::changes::state::StateManager; use common::{Server, storage::index::ObjectIndexBuilder}; use directory::QueryParams; use email::identity::{ArchivedEmailAddress, Identity}; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - property::Property, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::identity::{self, IdentityProperty, IdentityValue}, }; +use jmap_tools::{Map, Value}; +use std::future::Future; use store::{ rkyv::{option::ArchivedOption, vec::ArchivedVec}, roaring::RoaringBitmap, @@ -23,15 +23,11 @@ use trc::AddContext; use types::collection::{Collection, SyncCollection}; use utils::sanitize_email; -use crate::changes::state::StateManager; - -use std::future::Future; - pub trait IdentityGet: Sync + Send { fn identity_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; fn identity_get_or_create( &self, @@ -42,18 +38,18 @@ pub trait IdentityGet: Sync + Send { impl IdentityGet for Server { async fn identity_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::Name, - Property::Email, - Property::ReplyTo, - Property::Bcc, - Property::TextSignature, - Property::HtmlSignature, - Property::MayDelete, + IdentityProperty::Id, + IdentityProperty::Name, + IdentityProperty::Email, + IdentityProperty::ReplyTo, + IdentityProperty::Bcc, + IdentityProperty::TextSignature, + IdentityProperty::HtmlSignature, + IdentityProperty::MayDelete, ]); let account_id = request.account_id.document_id(); let identity_ids = self.identity_get_or_create(account_id).await?; @@ -95,39 +91,50 @@ impl IdentityGet for Server { let identity = _identity .unarchive::() .caused_by(trc::location!())?; - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { match property { - Property::Id => { - result.append(Property::Id, Value::Id(id)); + IdentityProperty::Id => { + result.insert_unchecked(IdentityProperty::Id, IdentityValue::Id(id)); } - Property::MayDelete => { - result.append(Property::MayDelete, Value::Bool(true)); + IdentityProperty::MayDelete => { + result.insert_unchecked(IdentityProperty::MayDelete, Value::Bool(true)); } - Property::Name => { - result.append(Property::Name, identity.name.to_string()); + IdentityProperty::Name => { + result.insert_unchecked(IdentityProperty::Name, identity.name.to_string()); } - Property::Email => { - result.append(Property::Email, identity.email.to_string()); + IdentityProperty::Email => { + result + .insert_unchecked(IdentityProperty::Email, identity.email.to_string()); } - Property::TextSignature => { - result.append(Property::TextSignature, identity.text_signature.to_string()); + IdentityProperty::TextSignature => { + result.insert_unchecked( + IdentityProperty::TextSignature, + identity.text_signature.to_string(), + ); } - Property::HtmlSignature => { - result.append(Property::HtmlSignature, identity.html_signature.to_string()); + IdentityProperty::HtmlSignature => { + result.insert_unchecked( + IdentityProperty::HtmlSignature, + identity.html_signature.to_string(), + ); } - Property::Bcc => { - result.append(Property::Bcc, email_to_value(&identity.bcc)); + IdentityProperty::Bcc => { + result + .insert_unchecked(IdentityProperty::Bcc, email_to_value(&identity.bcc)); } - Property::ReplyTo => { - result.append(Property::ReplyTo, email_to_value(&identity.reply_to)); + IdentityProperty::ReplyTo => { + result.insert_unchecked( + IdentityProperty::ReplyTo, + email_to_value(&identity.reply_to), + ); } property => { - result.append(property.clone(), Value::Null); + result.insert_unchecked(property.clone(), Value::Null); } } } - response.list.push(result); + response.list.push(result.into()); } Ok(response) @@ -200,16 +207,18 @@ impl IdentityGet for Server { } } -fn email_to_value(email: &ArchivedOption>) -> Value { +fn email_to_value( + email: &ArchivedOption>, +) -> Value<'static, IdentityProperty, IdentityValue> { if let ArchivedOption::Some(email) = email { - Value::List( + Value::Array( email .iter() .map(|email| { Value::Object( - Object::with_capacity(2) - .with_property(Property::Name, &email.name) - .with_property(Property::Email, &email.email), + Map::with_capacity(2) + .with_key_value(IdentityProperty::Name, &email.name) + .with_key_value(IdentityProperty::Email, &email.email), ) }) .collect(), diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 30430607..caefea3c 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -9,13 +9,9 @@ use directory::QueryParams; use email::identity::{EmailAddress, Identity}; use jmap_proto::{ error::set::SetError, - method::set::{RequestArguments, SetRequest, SetResponse}, - response::references::EvalObjectReferences, - types::{ - property::Property, - state::State, - value::{MaybePatchValue, Value}, - }, + method::set::{SetRequest, SetResponse}, + object::identity, + types::state::State, }; use std::future::Future; use store::write::BatchBuilder; @@ -29,15 +25,15 @@ use utils::sanitize_email; pub trait IdentitySet: Sync + Send { fn identity_set( &self, - request: SetRequest, - ) -> impl Future> + Send; + request: SetRequest<'_, identity::Identity>, + ) -> impl Future>> + Send; } impl IdentitySet for Server { async fn identity_set( &self, - mut request: SetRequest, - ) -> trc::Result { + mut request: SetRequest<'_, identity::Identity>, + ) -> trc::Result> { let account_id = request.account_id.document_id(); let identity_ids = self .get_document_ids(account_id, Collection::Identity) @@ -71,7 +67,7 @@ impl IdentitySet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::Email) + .with_key_value(Property::Email) .with_description( "E-mail address not configured for this account.".to_string(), ), @@ -82,7 +78,7 @@ impl IdentitySet for Server { response.not_created.append( id, SetError::invalid_properties() - .with_property(Property::Email) + .with_key_value(Property::Email) .with_description("Missing e-mail address."), ); continue 'create; @@ -194,29 +190,29 @@ fn validate_identity_value( is_create: bool, ) -> Result<(), SetError> { match (property, value) { - (Property::Name, MaybePatchValue::Value(Value::Text(value))) if value.len() < 255 => { + (Property::Name, MaybePatchValue::Value(Value::Str(value))) if value.len() < 255 => { identity.name = value; } - (Property::Email, MaybePatchValue::Value(Value::Text(value))) + (Property::Email, MaybePatchValue::Value(Value::Str(value))) if is_create && value.len() < 255 => { identity.email = sanitize_email(&value).ok_or_else(|| { SetError::invalid_properties() - .with_property(Property::Email) + .with_key_value(Property::Email) .with_description("Invalid e-mail address.") })?; } - (Property::TextSignature, MaybePatchValue::Value(Value::Text(value))) + (Property::TextSignature, MaybePatchValue::Value(Value::Str(value))) if value.len() < 2048 => { identity.text_signature = value; } - (Property::HtmlSignature, MaybePatchValue::Value(Value::Text(value))) + (Property::HtmlSignature, MaybePatchValue::Value(Value::Str(value))) if value.len() < 2048 => { identity.html_signature = value; } - (Property::ReplyTo | Property::Bcc, MaybePatchValue::Value(Value::List(value))) => { + (Property::ReplyTo | Property::Bcc, MaybePatchValue::Value(Value::Array(value))) => { let mut addresses = Vec::with_capacity(value.len()); for addr in value { let mut address = EmailAddress { @@ -227,11 +223,11 @@ fn validate_identity_value( if let Value::Object(obj) = addr { for (key, value) in obj.0 { match (key, value) { - (Property::Email, Value::Text(value)) if value.len() < 255 => { + (Property::Email, Value::Str(value)) if value.len() < 255 => { is_valid = true; address.email = value; } - (Property::Name, Value::Text(value)) if value.len() < 255 => { + (Property::Name, Value::Str(value)) if value.len() < 255 => { address.name = Some(value); } (Property::Name, Value::Null) => (), @@ -247,7 +243,7 @@ fn validate_identity_value( addresses.push(address); } else { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Invalid e-mail address object.")); } } @@ -275,7 +271,7 @@ fn validate_identity_value( (Property::Bcc, MaybePatchValue::Value(Value::Null)) => identity.bcc = None, (property, _) => { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Field could not be set.")); } } diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index eb14a24d..b4a19b5b 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -12,6 +12,7 @@ use jmap_proto::{ query::{QueryRequest, QueryResponse}, set::{SetRequest, SetResponse}, }, + object::JmapObject, types::state::State, }; use std::{fmt::Display, future::Future}; @@ -39,11 +40,11 @@ pub mod vacation; pub mod websocket; impl JmapMethods for Server { - async fn prepare_set_response( + async fn prepare_set_response( &self, - request: &SetRequest, + request: &SetRequest<'_, T>, asserted_state: State, - ) -> trc::Result { + ) -> trc::Result> { Ok( SetResponse::from_request(request, self.core.jmap.set_max_objects)? .with_state(asserted_state), @@ -86,7 +87,7 @@ impl JmapMethods for Server { }) } - async fn build_query_response( + async fn build_query_response( &'_ self, result_set: &ResultSet, query_state: State, @@ -162,11 +163,11 @@ impl JmapMethods for Server { } pub trait JmapMethods: Sync + Send { - fn prepare_set_response( + fn prepare_set_response( &self, request: &SetRequest, asserted_state: State, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn filter( &self, @@ -182,7 +183,7 @@ pub trait JmapMethods: Sync + Send { filters: Vec>, ) -> impl Future> + Send; - fn build_query_response( + fn build_query_response( &'_ self, result_set: &ResultSet, query_state: State, diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index 99539588..38ae2ae3 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -7,45 +7,43 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use email::cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess}; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - property::Property, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::mailbox::{Mailbox, MailboxProperty, MailboxValue}, }; +use jmap_tools::{Map, Value}; use std::future::Future; use store::ahash::AHashSet; -use types::{acl::Acl, keyword::Keyword}; +use types::{acl::Acl, keyword::Keyword, special_use::SpecialUse}; -use crate::api::auth::JmapAcl; +use crate::api::acl::JmapRights; pub trait MailboxGet: Sync + Send { fn mailbox_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl MailboxGet for Server { async fn mailbox_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::Name, - Property::ParentId, - Property::Role, - Property::SortOrder, - Property::IsSubscribed, - Property::TotalEmails, - Property::UnreadEmails, - Property::TotalThreads, - Property::UnreadThreads, - Property::MyRights, + MailboxProperty::Id, + MailboxProperty::Name, + MailboxProperty::ParentId, + MailboxProperty::Role, + MailboxProperty::SortOrder, + MailboxProperty::IsSubscribed, + MailboxProperty::TotalEmails, + MailboxProperty::UnreadEmails, + MailboxProperty::TotalThreads, + MailboxProperty::UnreadThreads, + MailboxProperty::MyRights, ]); let account_id = request.account_id.document_id(); let cache = self.get_cached_messages(account_id).await?; @@ -89,106 +87,78 @@ impl MailboxGet for Server { continue; }; - let mut mailbox = Object::with_capacity(properties.len()); + let mut mailbox = Map::with_capacity(properties.len()); for property in &properties { let value = match property { - Property::Id => Value::Id(id), - Property::Name => Value::Text(cached_mailbox.name.to_string()), - Property::Role => { - if let Some(role) = cached_mailbox.role.as_str() { - Value::Text(role.to_string()) - } else { - Value::Null - } + MailboxProperty::Id => Value::Element(MailboxValue::Id(id)), + MailboxProperty::Name => Value::Str(cached_mailbox.name.to_string().into()), + MailboxProperty::Role => match cached_mailbox.role { + SpecialUse::None => Value::Null, + role => Value::Element(MailboxValue::Role(role)), + }, + MailboxProperty::SortOrder => { + Value::Number(cached_mailbox.sort_order().unwrap_or_default().into()) } - Property::SortOrder => { - Value::from(cached_mailbox.sort_order().unwrap_or_default()) - } - Property::ParentId => { + MailboxProperty::ParentId => { if let Some(parent_id) = cached_mailbox.parent_id() { - Value::Id((parent_id).into()) + Value::Element(MailboxValue::Id(parent_id.into())) } else { Value::Null } } - Property::TotalEmails => { - Value::UnsignedInt(cache.in_mailbox(document_id).count() as u64) + MailboxProperty::TotalEmails => { + Value::Number(cache.in_mailbox(document_id).count().into()) } - Property::UnreadEmails => Value::UnsignedInt( + MailboxProperty::UnreadEmails => Value::Number( cache .in_mailbox_without_keyword(document_id, &Keyword::Seen) - .count() as u64, + .count() + .into(), ), - Property::TotalThreads => Value::UnsignedInt( + MailboxProperty::TotalThreads => Value::Number( cache .in_mailbox(document_id) .map(|m| m.thread_id) .collect::>() - .len() as u64, + .len() + .into(), ), - Property::UnreadThreads => Value::UnsignedInt( + MailboxProperty::UnreadThreads => Value::Number( cache .in_mailbox_without_keyword(document_id, &Keyword::Seen) .map(|m| m.thread_id) .collect::>() - .len() as u64, + .len() + .into(), ), - Property::MyRights => { + MailboxProperty::MyRights => { if access_token.is_shared(account_id) { - let acl = cached_mailbox.acls.as_slice().effective_acl(access_token); - Object::with_capacity(9) - .with_property(Property::MayReadItems, acl.contains(Acl::ReadItems)) - .with_property(Property::MayAddItems, acl.contains(Acl::AddItems)) - .with_property( - Property::MayRemoveItems, - acl.contains(Acl::RemoveItems), - ) - .with_property(Property::MaySetSeen, acl.contains(Acl::ModifyItems)) - .with_property( - Property::MaySetKeywords, - acl.contains(Acl::ModifyItems), - ) - .with_property( - Property::MayCreateChild, - acl.contains(Acl::CreateChild), - ) - .with_property(Property::MayRename, acl.contains(Acl::Modify)) - .with_property(Property::MayDelete, acl.contains(Acl::Delete)) - .with_property(Property::MaySubmit, acl.contains(Acl::Submit)) - .into() + JmapRights::rights::( + cached_mailbox.acls.as_slice().effective_acl(access_token), + ) } else { - Object::with_capacity(9) - .with_property(Property::MayReadItems, true) - .with_property(Property::MayAddItems, true) - .with_property(Property::MayRemoveItems, true) - .with_property(Property::MaySetSeen, true) - .with_property(Property::MaySetKeywords, true) - .with_property(Property::MayCreateChild, true) - .with_property(Property::MayRename, true) - .with_property(Property::MayDelete, true) - .with_property(Property::MaySubmit, true) - .into() + JmapRights::all_rights::() } } - Property::IsSubscribed => Value::Bool( + MailboxProperty::IsSubscribed => Value::Bool( cached_mailbox .subscribers .contains(&access_token.primary_id()), ), - Property::Acl => { - self.acl_get(&cached_mailbox.acls, access_token, account_id) - .await - } - + MailboxProperty::ShareWith => JmapRights::share_with::( + account_id, + access_token, + &cached_mailbox.acls, + ), _ => Value::Null, }; - mailbox.append(property.clone(), value); + mailbox.insert_unchecked(property.clone(), value); } // Add result to response - response.list.push(mailbox); + response.list.push(mailbox.into()); } Ok(response) } diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 47d486d8..3dec1749 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -5,11 +5,11 @@ */ use crate::{JmapMethods, changes::state::MessageCacheState}; -use common::{Server, auth::AccessToken, config::jmap::settings::SpecialUse}; +use common::{Server, auth::AccessToken}; use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess}; use jmap_proto::{ - method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, - object::mailbox::QueryArguments, + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::mailbox::{Mailbox, MailboxComparator, MailboxFilter}, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -19,12 +19,12 @@ use store::{ query::{self}, roaring::RoaringBitmap, }; -use types::{acl::Acl, collection::Collection}; +use types::{acl::Acl, collection::Collection, special_use::SpecialUse}; pub trait MailboxQuery: Sync + Send { fn mailbox_query( &self, - request: QueryRequest, + request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -32,7 +32,7 @@ pub trait MailboxQuery: Sync + Send { impl MailboxQuery for Server { async fn mailbox_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { let account_id = request.account_id.document_id(); @@ -43,106 +43,111 @@ impl MailboxQuery for Server { for cond in std::mem::take(&mut request.filter) { match cond { - Filter::ParentId(parent_id) => { - let parent_id = parent_id.map(|id| id.document_id()).unwrap_or(u32::MAX); - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| mailbox.parent_id == parent_id) - .map(|m| m.document_id) - .collect::(), - )); - } - Filter::Name(name) => { - #[cfg(feature = "test_mode")] - { - // Used for concurrent requests tests - if name == "__sleep" { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + Filter::Property(cond) => { + match cond { + MailboxFilter::ParentId(parent_id) => { + let parent_id = + parent_id.map(|id| id.document_id()).unwrap_or(u32::MAX); + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| mailbox.parent_id == parent_id) + .map(|m| m.document_id) + .collect::(), + )); + } + MailboxFilter::Name(name) => { + #[cfg(feature = "test_mode")] + { + // Used for concurrent requests tests + if name == "__sleep" { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + let name = name.to_lowercase(); + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| mailbox.name.to_lowercase().contains(&name)) + .map(|m| m.document_id) + .collect::(), + )); + } + MailboxFilter::Role(role) => { + if let Some(role) = role { + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| mailbox.role == role) + .map(|m| m.document_id) + .collect::(), + )); + } else { + filters.push(query::Filter::Not); + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| matches!(mailbox.role, SpecialUse::None)) + .map(|m| m.document_id) + .collect::(), + )); + filters.push(query::Filter::End); + } + } + MailboxFilter::HasAnyRole(has_role) => { + if !has_role { + filters.push(query::Filter::Not); + } + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| !matches!(mailbox.role, SpecialUse::None)) + .map(|m| m.document_id) + .collect::(), + )); + if !has_role { + filters.push(query::Filter::End); + } + } + MailboxFilter::IsSubscribed(is_subscribed) => { + if !is_subscribed { + filters.push(query::Filter::Not); + } + filters.push(query::Filter::is_in_set( + mailboxes + .mailboxes + .items + .iter() + .filter(|mailbox| { + mailbox.subscribers.contains(&access_token.primary_id) + }) + .map(|m| m.document_id) + .collect::(), + )); + if !is_subscribed { + filters.push(query::Filter::End); + } + } + MailboxFilter::_T(other) => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(other)); } } - let name = name.to_lowercase(); - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| mailbox.name.to_lowercase().contains(&name)) - .map(|m| m.document_id) - .collect::(), - )); - } - Filter::Role(role) => { - if let Some(role) = role { - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| mailbox.role.as_str().is_some_and(|r| r == role)) - .map(|m| m.document_id) - .collect::(), - )); - } else { - filters.push(query::Filter::Not); - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| matches!(mailbox.role, SpecialUse::None)) - .map(|m| m.document_id) - .collect::(), - )); - filters.push(query::Filter::End); - } - } - Filter::HasAnyRole(has_role) => { - if !has_role { - filters.push(query::Filter::Not); - } - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| !matches!(mailbox.role, SpecialUse::None)) - .map(|m| m.document_id) - .collect::(), - )); - if !has_role { - filters.push(query::Filter::End); - } - } - Filter::IsSubscribed(is_subscribed) => { - if !is_subscribed { - filters.push(query::Filter::Not); - } - filters.push(query::Filter::is_in_set( - mailboxes - .mailboxes - .items - .iter() - .filter(|mailbox| { - mailbox.subscribers.contains(&access_token.primary_id) - }) - .map(|m| m.document_id) - .collect::(), - )); - if !is_subscribed { - filters.push(query::Filter::End); - } - } - Filter::And | Filter::Or | Filter::Not | Filter::Close => { - filters.push(cond.into()); } - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); + Filter::And | Filter::Or | Filter::Not | Filter::Close => { + filters.push(cond.into()); } } } @@ -212,10 +217,10 @@ impl MailboxQuery for Server { for comparator in request .sort .filter(|s| !s.is_empty()) - .unwrap_or_else(|| vec![Comparator::ascending(SortProperty::ParentId)]) + .unwrap_or_else(|| vec![Comparator::ascending(MailboxComparator::ParentId)]) { comparators.push(match comparator.property { - SortProperty::Name => { + MailboxComparator::Name => { let sorted_list = mailboxes .mailboxes .items @@ -228,7 +233,7 @@ impl MailboxQuery for Server { comparator.is_ascending, ) } - SortProperty::SortOrder => { + MailboxComparator::SortOrder => { let sorted_list = mailboxes .mailboxes .items @@ -241,7 +246,7 @@ impl MailboxQuery for Server { comparator.is_ascending, ) } - SortProperty::ParentId => { + MailboxComparator::ParentId => { let sorted_list = mailboxes .mailboxes .items @@ -260,10 +265,8 @@ impl MailboxQuery for Server { ) } - other => { - return Err(trc::JmapEvent::UnsupportedSort - .into_err() - .details(other.to_string())); + MailboxComparator::_T(other) => { + return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other)); } }); } diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 379c15ef..4cf7e5cb 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -6,8 +6,7 @@ use crate::{JmapMethods, api::auth::JmapAcl, changes::state::MessageCacheState}; use common::{ - Server, auth::AccessToken, config::jmap::settings::SpecialUse, sharing::EffectiveAcl, - storage::index::ObjectIndexBuilder, + Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, }; #[allow(unused_imports)] use email::mailbox::{INBOX_ID, JUNK_ID, TRASH_ID, UidMailbox}; @@ -21,13 +20,8 @@ use email::{ use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, - object::mailbox::SetArguments, - response::references::EvalObjectReferences, - types::{ - property::Property, - state::State, - value::{MaybePatchValue, Object, SetValue, Value}, - }, + object::mailbox, + types::state::State, }; use std::future::Future; use store::{ @@ -42,7 +36,7 @@ pub struct SetContext<'x> { account_id: u32, access_token: &'x AccessToken, is_shared: bool, - response: SetResponse, + response: SetResponse, mailbox_ids: RoaringBitmap, will_destroy: Vec, } @@ -50,9 +44,9 @@ pub struct SetContext<'x> { pub trait MailboxSet: Sync + Send { fn mailbox_set( &self, - request: SetRequest, + request: SetRequest<'_, mailbox::Mailbox>, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn mailbox_set_item( &self, @@ -66,9 +60,9 @@ impl MailboxSet for Server { #[allow(clippy::blocks_in_conditions)] async fn mailbox_set( &self, - mut request: SetRequest, + mut request: SetRequest<'_, mailbox::Mailbox>, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { // Prepare response let account_id = request.account_id.document_id(); let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false); @@ -313,13 +307,13 @@ impl MailboxSet for Server { } }; match (&property, value) { - (Property::Name, MaybePatchValue::Value(Value::Text(value))) => { + (Property::Name, MaybePatchValue::Value(Value::Str(value))) => { let value = value.trim(); if !value.is_empty() && value.len() < self.core.jmap.mailbox_name_max_len { changes.name = value.into(); } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Name) + .with_key_value(Property::Name) .with_description( if !value.is_empty() { "Mailbox name is too long." @@ -354,20 +348,20 @@ impl MailboxSet for Server { changes.subscribers.retain(|id| *id != account_id); } } - (Property::Role, MaybePatchValue::Value(Value::Text(value))) => { + (Property::Role, MaybePatchValue::Value(Value::Str(value))) => { let role = value.trim(); if let Ok(role) = SpecialUse::parse_value(role) { changes.role = role; } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Role) + .with_key_value(Property::Role) .with_description(format!("Invalid role {role:?}.")))); } } (Property::Role, MaybePatchValue::Value(Value::Null)) => { changes.role = SpecialUse::None; } - (Property::SortOrder, MaybePatchValue::Value(Value::UnsignedInt(value))) => { + (Property::SortOrder, MaybePatchValue::Value(Value::Number(value))) => { changes.sort_order = Some(value as u32); } (Property::Acl, value) => { @@ -389,7 +383,7 @@ impl MailboxSet for Server { _ => { return Ok(Err(SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Invalid property or value.".to_string()))); } } @@ -408,7 +402,7 @@ impl MailboxSet for Server { for depth in 0..self.core.jmap.mailbox_max_depth { if mailbox_parent_id == current_mailbox_id { return Ok(Err(SetError::invalid_properties() - .with_property(Property::ParentId) + .with_key_value(Property::ParentId) .with_description("Mailbox cannot be a parent of itself."))); } else if mailbox_parent_id == 0 { if depth == 0 && ctx.is_shared { @@ -446,14 +440,14 @@ impl MailboxSet for Server { break; } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::ParentId) + .with_key_value(Property::ParentId) .with_description("Mailbox parent does not exist."))); } } if !success { return Ok(Err(SetError::invalid_properties() - .with_property(Property::ParentId) + .with_key_value(Property::ParentId) .with_description( "Mailbox parent-child relationship is too deep.", ))); @@ -471,7 +465,7 @@ impl MailboxSet for Server { && cached_mailboxes.mailbox_by_role(&changes.role).is_some() { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Role) + .with_key_value(Property::Role) .with_description(format!( "A mailbox with role '{}' already exists.", changes.role.as_str().unwrap_or_default() @@ -483,7 +477,7 @@ impl MailboxSet for Server { *document_id == INBOX_ID || *document_id == TRASH_ID }) { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Role) + .with_key_value(Property::Role) .with_description( "You are not allowed to change the role of Inbox or Trash folders.", ))); @@ -503,7 +497,7 @@ impl MailboxSet for Server { }) { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Name) + .with_key_value(Property::Name) .with_description(format!( "A mailbox with name '{}' already exists.", changes.name @@ -511,7 +505,7 @@ impl MailboxSet for Server { } } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Name) + .with_key_value(Property::Name) .with_description("Mailbox name cannot be empty."))); } diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 88cd43c7..2053b822 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -7,37 +7,35 @@ use common::Server; use directory::QueryParams; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - property::Property, - state::State, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::principal::{Principal, PrincipalProperty, PrincipalValue}, + types::state::State, }; +use jmap_tools::{Map, Value}; use std::future::Future; use types::collection::Collection; pub trait PrincipalGet: Sync + Send { fn principal_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; } impl PrincipalGet for Server { async fn principal_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::Type, - Property::Name, - Property::Description, - Property::Email, - //Property::Timezone, - //Property::Capabilities, + PrincipalProperty::Id, + PrincipalProperty::Type, + PrincipalProperty::Name, + PrincipalProperty::Description, + PrincipalProperty::Email, + //PrincipalProperty::Timezone, + //PrincipalProperty::Capabilities, ]); let principal_ids = self .get_document_ids(u32::MAX, Collection::Principal) @@ -74,27 +72,29 @@ impl PrincipalGet for Server { continue; }; - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { let value = match property { - Property::Id => Value::Id(id), - Property::Type => Value::Text(principal.typ().to_jmap().to_string()), - Property::Name => Value::Text(principal.name().to_string()), - Property::Description => principal + PrincipalProperty::Id => Value::Element(PrincipalValue::Id(id)), + PrincipalProperty::Type => { + Value::Str(principal.typ().to_jmap().to_string().into()) + } + PrincipalProperty::Name => Value::Str(principal.name().to_string().into()), + PrincipalProperty::Description => principal .description() - .map(|v| Value::Text(v.to_string())) + .map(|v| Value::Str(v.to_string().into())) .unwrap_or(Value::Null), - Property::Email => principal + PrincipalProperty::Email => principal .emails .first() - .map(|email| Value::Text(email.to_string())) + .map(|email| Value::Str(email.to_string().into())) .unwrap_or(Value::Null), _ => Value::Null, }; - result.append(property.clone(), value); + result.insert_unchecked(property.clone(), value); } - response.list.push(result); + response.list.push(result.into()); } Ok(response) diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index c69dd630..738aee68 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -9,7 +9,8 @@ use common::Server; use directory::QueryParams; use http_proto::HttpSessionData; use jmap_proto::{ - method::query::{Filter, QueryRequest, QueryResponse, RequestArguments}, + method::query::{Filter, QueryRequest, QueryResponse}, + object::principal::{Principal, PrincipalFilter}, types::state::State, }; use std::future::Future; @@ -19,7 +20,7 @@ use types::collection::Collection; pub trait PrincipalQuery: Sync + Send { fn principal_query( &self, - request: QueryRequest, + request: QueryRequest, session: &HttpSessionData, ) -> impl Future> + Send; } @@ -27,7 +28,7 @@ pub trait PrincipalQuery: Sync + Send { impl PrincipalQuery for Server { async fn principal_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, session: &HttpSessionData, ) -> trc::Result { let account_id = request.account_id.document_id(); @@ -40,45 +41,54 @@ impl PrincipalQuery for Server { for cond in std::mem::take(&mut request.filter) { match cond { - Filter::Name(name) => { - if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::name(name.as_str()).with_return_member_of(false)) - .await? - { - if is_set || result_set.results.contains(principal.id()) { - result_set.results = - RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(); + Filter::Property(cond) => match cond { + PrincipalFilter::Name(name) => { + if let Some(principal) = self + .core + .storage + .directory + .query(QueryParams::name(name.as_str()).with_return_member_of(false)) + .await? + { + if is_set || result_set.results.contains(principal.id()) { + result_set.results = + RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(); + } else { + result_set.results = RoaringBitmap::new(); + } } else { result_set.results = RoaringBitmap::new(); } - } else { - result_set.results = RoaringBitmap::new(); - } - is_set = false; - } - Filter::Email(email) => { - let mut ids = RoaringBitmap::new(); - if let Some(id) = self - .email_to_id(&self.core.storage.directory, &email, session.session_id) - .await? - { - ids.insert(id); - } - if is_set { - result_set.results = ids; is_set = false; - } else { - result_set.results &= ids; } - } - Filter::Type(_) => {} - other => { + PrincipalFilter::Email(email) => { + let mut ids = RoaringBitmap::new(); + if let Some(id) = self + .email_to_id(&self.core.storage.directory, &email, session.session_id) + .await? + { + ids.insert(id); + } + if is_set { + result_set.results = ids; + is_set = false; + } else { + result_set.results &= ids; + } + } + PrincipalFilter::_T(other) => { + return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other)); + } + other => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(other.to_string())); + } + }, + Filter::And | Filter::Or | Filter::Not | Filter::Close => { return Err(trc::JmapEvent::UnsupportedFilter .into_err() - .details(other.to_string())); + .details("Logical operators are not supported")); } } } diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs index 8ed46cb0..f4dcc077 100644 --- a/crates/jmap/src/push/get.rs +++ b/crates/jmap/src/push/get.rs @@ -10,13 +10,11 @@ use common::{ ipc::{EncryptionKeys, PushSubscription, StateEvent, UpdateSubscription}, }; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - date::UTCDate, - property::Property, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::push_subscription::{self, PushSubscriptionProperty, PushSubscriptionValue}, + types::date::UTCDate, }; +use jmap_tools::{Map, Value}; use std::future::Future; use store::{ BitmapKey, ValueKey, @@ -29,9 +27,9 @@ use utils::map::bitmap::Bitmap; pub trait PushSubscriptionFetch: Sync + Send { fn push_subscription_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn fetch_push_subscriptions( &self, @@ -44,16 +42,16 @@ pub trait PushSubscriptionFetch: Sync + Send { impl PushSubscriptionFetch for Server { async fn push_subscription_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::DeviceClientId, - Property::VerificationCode, - Property::Expires, - Property::Types, + PushSubscriptionProperty::Id, + PushSubscriptionProperty::DeviceClientId, + PushSubscriptionProperty::VerificationCode, + PushSubscriptionProperty::Expires, + PushSubscriptionProperty::Types, ]); let account_id = access_token.primary_id(); let push_ids = self @@ -95,48 +93,49 @@ impl PushSubscriptionFetch for Server { let push = push_ .unarchive::() .caused_by(trc::location!())?; - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { match property { - Property::Id => { - result.append(Property::Id, Value::Id(id)); + PushSubscriptionProperty::Id => { + result.insert_unchecked(PushSubscriptionProperty::Id, id); } - Property::Url | Property::Keys | Property::Value => { + PushSubscriptionProperty::Url | PushSubscriptionProperty::Keys => { return Err(trc::JmapEvent::Forbidden.into_err().details( "The 'url' and 'keys' properties are not readable".to_string(), )); } - Property::DeviceClientId => { - result.append( - Property::DeviceClientId, - Value::from(&push.device_client_id), + PushSubscriptionProperty::DeviceClientId => { + result.insert_unchecked( + PushSubscriptionProperty::DeviceClientId, + &push.device_client_id, ); } - Property::Types => { + PushSubscriptionProperty::Types => { let mut types = Vec::new(); for typ in Bitmap::from(&push.types).into_iter() { - types.push(Value::Text(typ.to_string())); + types.push(Value::Element(PushSubscriptionValue::Types(typ))); } - result.append(Property::Types, Value::List(types)); + result + .insert_unchecked(PushSubscriptionProperty::Types, Value::Array(types)); } - Property::Expires => { + PushSubscriptionProperty::Expires => { if push.expires > 0 { - result.append( - Property::Expires, - Value::Date( + result.insert_unchecked( + PushSubscriptionProperty::Expires, + Value::Element(PushSubscriptionValue::Date( UTCDate::from_timestamp(u64::from(push.expires) as i64), - ), + )), ); } else { - result.append(Property::Expires, Value::Null); + result.insert_unchecked(PushSubscriptionProperty::Expires, Value::Null); } } property => { - result.append(property.clone(), Value::Null); + result.insert_unchecked(property.clone(), Value::Null); } } } - response.list.push(result); + response.list.push(result.into()); } Ok(response) diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index 89d3eb44..af97a11e 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -10,13 +10,9 @@ use common::{Server, auth::AccessToken}; use email::push::{Keys, PushSubscription}; use jmap_proto::{ error::set::SetError, - method::set::{RequestArguments, SetRequest, SetResponse}, - response::references::EvalObjectReferences, - types::{ - date::UTCDate, - property::Property, - value::{MaybePatchValue, Object, Value}, - }, + method::set::{SetRequest, SetResponse}, + object::push_subscription, + types::date::UTCDate, }; use rand::distr::Alphanumeric; use std::future::Future; @@ -35,17 +31,17 @@ const VERIFICATION_CODE_LEN: usize = 32; pub trait PushSubscriptionSet: Sync + Send { fn push_subscription_set( &self, - request: SetRequest, + request: SetRequest<'_, push_subscription::PushSubscription>, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl PushSubscriptionSet for Server { async fn push_subscription_set( &self, - mut request: SetRequest, + mut request: SetRequest<'_, push_subscription::PushSubscription>, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let account_id = access_token.primary_id(); let push_ids = self .get_document_ids(account_id, Collection::PushSubscription) @@ -118,10 +114,10 @@ impl PushSubscriptionSet for Server { .commit_point(); 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), + Map::with_capacity(1) + .with_key_value(Property::Id, Value::Id(document_id.into())) + .with_key_value(Property::Keys, Value::Null) + .with_key_value(Property::Expires, expires), ); } @@ -209,12 +205,12 @@ fn validate_push_value( is_create: bool, ) -> Result<(), SetError> { match (property, value) { - (Property::DeviceClientId, MaybePatchValue::Value(Value::Text(value))) + (Property::DeviceClientId, MaybePatchValue::Value(Value::Str(value))) if is_create && value.len() < 255 => { push.device_client_id = value; } - (Property::Url, MaybePatchValue::Value(Value::Text(value))) + (Property::Url, MaybePatchValue::Value(Value::Str(value))) if is_create && value.len() < 512 && value.starts_with("https://") => { push.url = value; @@ -235,7 +231,7 @@ fn validate_push_value( push.keys = Some(Keys { auth, p256dh }); } else { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Failed to decode keys.")); } } @@ -251,7 +247,7 @@ fn validate_push_value( (Property::Expires, MaybePatchValue::Value(Value::Null)) => { push.expires = now() + EXPIRES_MAX as u64; } - (Property::Types, MaybePatchValue::Value(Value::List(value))) => { + (Property::Types, MaybePatchValue::Value(Value::Array(value))) => { push.types.clear(); for item in value { @@ -262,17 +258,17 @@ fn validate_push_value( push.types.insert(dt); } else { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Invalid data type.")); } } } - (Property::VerificationCode, MaybePatchValue::Value(Value::Text(value))) if !is_create => { + (Property::VerificationCode, MaybePatchValue::Value(Value::Str(value))) if !is_create => { if push.verification_code == value { push.verified = true; } else { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Verification code does not match.".to_string())); } } @@ -285,7 +281,7 @@ fn validate_push_value( (Property::VerificationCode, MaybePatchValue::Value(Value::Null)) => {} (property, _) => { return Err(SetError::invalid_properties() - .with_property(property.clone()) + .with_key_value(property.clone()) .with_description("Field could not be set.")); } } diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs index 0fa17a85..abed45ef 100644 --- a/crates/jmap/src/quota/get.rs +++ b/crates/jmap/src/quota/get.rs @@ -6,42 +6,40 @@ use common::{Server, auth::AccessToken}; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - property::Property, - state::State, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::quota::{Quota, QuotaProperty, QuotaValue}, + types::state::State, }; +use jmap_tools::{Map, Value}; use std::future::Future; use types::{id::Id, type_state::DataType}; pub trait QuotaGet: Sync + Send { fn quota_get( &self, - request: GetRequest, + request: GetRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl QuotaGet for Server { async fn quota_get( &self, - mut request: GetRequest, + mut request: GetRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.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, + QuotaProperty::Id, + QuotaProperty::ResourceType, + QuotaProperty::Used, + QuotaProperty::WarnLimit, + QuotaProperty::SoftLimit, + QuotaProperty::HardLimit, + QuotaProperty::Scope, + QuotaProperty::Name, + QuotaProperty::Description, + QuotaProperty::Types, ]); let account_id = request.account_id.document_id(); let quota_ids = if access_token.quota > 0 { @@ -69,31 +67,31 @@ impl QuotaGet for Server { continue; } - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::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.to_string().into(), - Property::Description => access_token + QuotaProperty::Id => Value::Element(id.into()), + QuotaProperty::ResourceType => "octets".to_string().into(), + QuotaProperty::Used => (self.get_used_quota(account_id).await? as u64).into(), + QuotaProperty::HardLimit => access_token.quota.into(), + QuotaProperty::Scope => "account".to_string().into(), + QuotaProperty::Name => access_token.name.to_string().into(), + QuotaProperty::Description => access_token .description .as_ref() .map(|s| s.to_string()) .into(), - Property::Types => vec![ - Value::Text(DataType::Email.to_string()), - Value::Text(DataType::SieveScript.to_string()), + QuotaProperty::Types => vec![ + Value::Element(QuotaValue::Types(DataType::Email)), + Value::Element(QuotaValue::Types(DataType::SieveScript)), ] .into(), _ => Value::Null, }; - result.append(property.clone(), value); + result.insert_unchecked(property.clone(), value); } - response.list.push(result); + response.list.push(result.into()); } Ok(response) diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs index 856821fb..bdf5f0bc 100644 --- a/crates/jmap/src/quota/query.rs +++ b/crates/jmap/src/quota/query.rs @@ -6,7 +6,8 @@ use common::{Server, auth::AccessToken}; use jmap_proto::{ - method::query::{QueryRequest, QueryResponse, RequestArguments}, + method::query::{QueryRequest, QueryResponse}, + object::quota::Quota, types::state::State, }; use std::future::Future; @@ -15,7 +16,7 @@ use types::id::Id; pub trait QuotaQuery: Sync + Send { fn quota_query( &self, - request: QueryRequest, + request: QueryRequest, access_token: &AccessToken, ) -> impl Future> + Send; } @@ -23,7 +24,7 @@ pub trait QuotaQuery: Sync + Send { impl QuotaQuery for Server { async fn quota_query( &self, - request: QueryRequest, + request: QueryRequest, access_token: &AccessToken, ) -> trc::Result { Ok(QueryResponse { @@ -39,71 +40,5 @@ impl QuotaQuery for Server { 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(trc::JmapEvent::UnsupportedFilter.into_err().details(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(trc::JmapEvent::UnsupportedSort.into_err().details(other.to_string())), - }); - } - - // Sort results - self.sort(result_set, comparators, paginate, response).await - } else { - Ok(response) - }*/ } } diff --git a/crates/jmap/src/quota/set.rs b/crates/jmap/src/quota/set.rs deleted file mode 100644 index cc7f39c4..00000000 --- a/crates/jmap/src/quota/set.rs +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use jmap_proto::{ - object::index::{IndexAs, IndexProperty}, - types::property::Property, -}; -use std::future::Future; - -pub trait QuotaSet: Sync + Send { - fn quota_set( - &self, - account_id: u32, - quota: &AccessToken, - ) -> impl Future> + Send; -} - -impl QuotaSet for Server { - async fn quota_set(&self, account_id: u32, quota: &AccessToken) -> trc::Result {} -} diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs index 3c5c0a65..d7deaecc 100644 --- a/crates/jmap/src/sieve/get.rs +++ b/crates/jmap/src/sieve/get.rs @@ -8,12 +8,10 @@ use crate::changes::state::StateManager; use common::Server; use email::sieve::SieveScript; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - property::Property, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::sieve::{Sieve, SieveProperty, SieveValue}, }; +use jmap_tools::{Map, Value}; use std::future::Future; use trc::AddContext; use types::{ @@ -24,21 +22,21 @@ use types::{ pub trait SieveScriptGet: Sync + Send { fn sieve_script_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; } impl SieveScriptGet for Server { async fn sieve_script_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::Name, - Property::BlobId, - Property::IsActive, + SieveProperty::Id, + SieveProperty::Name, + SieveProperty::BlobId, + SieveProperty::IsActive, ]); let account_id = request.account_id.document_id(); let push_ids = self @@ -83,19 +81,19 @@ impl SieveScriptGet for Server { let sieve = sieve_ .unarchive::() .caused_by(trc::location!())?; - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { match property { - Property::Id => { - result.append(Property::Id, Value::Id(id)); + SieveProperty::Id => { + result.insert_unchecked(SieveProperty::Id, id); } - Property::Name => { - result.append(Property::Name, Value::from(&sieve.name)); + SieveProperty::Name => { + result.insert_unchecked(SieveProperty::Name, &sieve.name); } - Property::IsActive => { - result.append(Property::IsActive, Value::Bool(sieve.is_active)); + SieveProperty::IsActive => { + result.insert_unchecked(SieveProperty::IsActive, sieve.is_active); } - Property::BlobId => { + SieveProperty::BlobId => { let blob_id = BlobId { hash: (&sieve.blob_hash).into(), class: BlobClass::Linked { @@ -110,14 +108,14 @@ impl SieveScriptGet for Server { .into(), }; - result.append(Property::BlobId, Value::BlobId(blob_id)); - } - property => { - result.append(property.clone(), Value::Null); + result.insert_unchecked( + SieveProperty::BlobId, + Value::Element(SieveValue::BlobId(blob_id)), + ); } } } - response.list.push(result); + response.list.push(result.into()); } Ok(response) diff --git a/crates/jmap/src/sieve/query.rs b/crates/jmap/src/sieve/query.rs index 79f159a0..903ac166 100644 --- a/crates/jmap/src/sieve/query.rs +++ b/crates/jmap/src/sieve/query.rs @@ -6,8 +6,9 @@ use crate::{JmapMethods, changes::state::StateManager}; use common::Server; -use jmap_proto::method::query::{ - Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty, +use jmap_proto::{ + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::sieve::{Sieve, SieveComparator, SieveFilter}, }; use std::future::Future; use store::query::{self}; @@ -19,35 +20,36 @@ use types::{ pub trait SieveScriptQuery: Sync + Send { fn sieve_script_query( &self, - request: QueryRequest, + request: QueryRequest, ) -> impl Future> + Send; } impl SieveScriptQuery for Server { async fn sieve_script_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, ) -> trc::Result { 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(name) => { - filters.push(query::Filter::contains(SieveField::Name, &name)) - } - Filter::IsActive(is_active) => filters.push(query::Filter::eq( - SieveField::IsActive, - vec![is_active as u8], - )), + Filter::Property(cond) => match cond { + SieveFilter::Name(name) => { + filters.push(query::Filter::contains(SieveField::Name, &name)) + } + SieveFilter::IsActive(is_active) => filters.push(query::Filter::eq( + SieveField::IsActive, + vec![is_active as u8], + )), + SieveFilter::_T(other) => { + return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other)); + } + }, + Filter::And | Filter::Or | Filter::Not | Filter::Close => { filters.push(cond.into()); } - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); - } } } @@ -70,19 +72,17 @@ impl SieveScriptQuery for Server { for comparator in request .sort .and_then(|s| if !s.is_empty() { s.into() } else { None }) - .unwrap_or_else(|| vec![Comparator::descending(SortProperty::Name)]) + .unwrap_or_else(|| vec![Comparator::descending(SieveComparator::Name)]) { comparators.push(match comparator.property { - SortProperty::Name => { + SieveComparator::Name => { query::Comparator::field(SieveField::Name, comparator.is_ascending) } - SortProperty::IsActive => { + SieveComparator::IsActive => { query::Comparator::field(SieveField::IsActive, comparator.is_ascending) } - other => { - return Err(trc::JmapEvent::UnsupportedSort - .into_err() - .details(other.to_string())); + SieveComparator::_T(other) => { + return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other)); } }); } diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 93fe11f7..2eeca874 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -17,14 +17,8 @@ use http_proto::HttpSessionData; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, - object::sieve::SetArguments, - request::reference::MaybeReference, - response::references::EvalObjectReferences, - types::{ - property::Property, - state::State, - value::{MaybePatchValue, Object, SetValue, Value}, - }, + object::sieve::Sieve, + types::state::State, }; use rand::distr::Alphanumeric; use sieve::compiler::ErrorType; @@ -46,16 +40,16 @@ use types::{ pub struct SetContext<'x> { resource_token: ResourceToken, access_token: &'x AccessToken, - response: SetResponse, + response: SetResponse, } pub trait SieveScriptSet: Sync + Send { fn sieve_script_set( &self, - request: SetRequest, + request: SetRequest<'_, Sieve>, access_token: &AccessToken, session: &HttpSessionData, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; #[allow(clippy::type_complexity)] fn sieve_set_item<'x>( @@ -138,9 +132,9 @@ impl SieveScriptSet for Server { // Add result with updated blobId ctx.response.created.insert( id, - Object::with_capacity(1) - .with_property(Property::Id, Value::Id(document_id.into())) - .with_property( + Map::with_capacity(1) + .with_key_value(Property::Id, Value::Id(document_id.into())) + .with_key_value( Property::BlobId, BlobId { hash: blob_hash, @@ -243,7 +237,7 @@ impl SieveScriptSet for Server { ctx.response.updated.append( id, blob_id.map(|blob_id| { - Object::with_capacity(1).with_property(Property::BlobId, blob_id) + Map::with_capacity(1).with_key_value(Property::BlobId, blob_id) }), ); } @@ -330,7 +324,7 @@ impl SieveScriptSet for Server { if let Some(obj) = obj { return Some(obj); } else { - *obj = Some(Object::with_capacity(1)); + *obj = Some(Map::with_capacity(1)); return obj.as_mut().unwrap().into(); } } @@ -399,14 +393,14 @@ impl SieveScriptSet for Server { } }; match (&property, value) { - (Property::Name, MaybePatchValue::Value(Value::Text(value))) => { + (Property::Name, MaybePatchValue::Value(Value::Str(value))) => { if value.len() > self.core.jmap.sieve_max_script_name { return Ok(Err(SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Script name is too long."))); } else if value.eq_ignore_ascii_case("vacation") { return Ok(Err(SetError::forbidden() - .with_property(property) + .with_key_value(property) .with_description( "The 'vacation' name is reserved, please use a different name.", ))); @@ -442,7 +436,7 @@ impl SieveScriptSet for Server { } _ => { return Ok(Err(SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Invalid property or value.".to_string()))); } } @@ -506,7 +500,7 @@ impl SieveScriptSet for Server { } } else { return Ok(Err(SetError::new(SetErrorType::BlobNotFound) - .with_property(Property::BlobId) + .with_key_value(Property::BlobId) .with_description("Blob does not exist."))); } } else { @@ -514,7 +508,7 @@ impl SieveScriptSet for Server { } } else if update.is_none() { return Ok(Err(SetError::invalid_properties() - .with_property(Property::BlobId) + .with_key_value(Property::BlobId) .with_description("Missing blobId."))); } else { None diff --git a/crates/jmap/src/sieve/validate.rs b/crates/jmap/src/sieve/validate.rs index 9bd0be78..84b33c25 100644 --- a/crates/jmap/src/sieve/validate.rs +++ b/crates/jmap/src/sieve/validate.rs @@ -4,15 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::blob::download::BlobDownload; use common::{Server, auth::AccessToken}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse}, + request::MaybeInvalid, }; use std::future::Future; -use crate::blob::download::BlobDownload; - pub trait SieveScriptValidate: Sync + Send { fn sieve_script_validate( &self, @@ -29,16 +29,21 @@ impl SieveScriptValidate for Server { ) -> trc::Result { Ok(ValidateSieveScriptResponse { account_id: request.account_id, - error: match self - .blob_download(&request.blob_id, access_token) - .await? - .map(|bytes| self.core.sieve.untrusted_compiler.compile(&bytes)) - { - Some(Ok(_)) => None, - Some(Err(err)) => SetError::new(SetErrorType::InvalidScript) - .with_description(err.to_string()) - .into(), - None => SetError::new(SetErrorType::BlobNotFound).into(), + error: match request.blob_id { + MaybeInvalid::Value(blob_id) => { + match self + .blob_download(&blob_id, access_token) + .await? + .map(|bytes| self.core.sieve.untrusted_compiler.compile(&bytes)) + { + Some(Ok(_)) => None, + Some(Err(err)) => SetError::new(SetErrorType::InvalidScript) + .with_description(err.to_string()) + .into(), + None => SetError::new(SetErrorType::BlobNotFound).into(), + } + } + MaybeInvalid::Invalid(_) => SetError::new(SetErrorType::BlobNotFound).into(), }, }) } diff --git a/crates/jmap/src/submission/get.rs b/crates/jmap/src/submission/get.rs index 00d69cc8..e84ccdf8 100644 --- a/crates/jmap/src/submission/get.rs +++ b/crates/jmap/src/submission/get.rs @@ -4,18 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::changes::state::StateManager; use common::Server; use email::submission::{ - ArchivedAddress, ArchivedEnvelope, Delivered, DeliveryStatus, EmailSubmission, UndoStatus, + ArchivedAddress, ArchivedEnvelope, ArchivedUndoStatus, Delivered, DeliveryStatus, + EmailSubmission, }; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{ - date::UTCDate, - property::Property, - value::{Object, Value}, - }, + method::get::{GetRequest, GetResponse}, + object::email_submission::{self, Displayed, EmailSubmissionProperty, EmailSubmissionValue}, + types::date::UTCDate, }; +use jmap_tools::{Key, Map, Value}; use smtp::queue::{ArchivedError, ArchivedErrorDetails, ArchivedStatus, Message, spool::SmtpSpool}; use smtp_proto::ArchivedResponse; use std::future::Future; @@ -27,32 +27,30 @@ use types::{ }; use utils::map::vec_map::VecMap; -use crate::changes::state::StateManager; - pub trait EmailSubmissionGet: Sync + Send { fn email_submission_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; } impl EmailSubmissionGet for Server { async fn email_submission_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let properties = request.unwrap_properties(&[ - Property::Id, - Property::EmailId, - Property::IdentityId, - Property::ThreadId, - Property::Envelope, - Property::SendAt, - Property::UndoStatus, - Property::DeliveryStatus, - Property::DsnBlobIds, - Property::MdnBlobIds, + EmailSubmissionProperty::Id, + EmailSubmissionProperty::EmailId, + EmailSubmissionProperty::IdentityId, + EmailSubmissionProperty::ThreadId, + EmailSubmissionProperty::Envelope, + EmailSubmissionProperty::SendAt, + EmailSubmissionProperty::UndoStatus, + EmailSubmissionProperty::DeliveryStatus, + EmailSubmissionProperty::DsnBlobIds, + EmailSubmissionProperty::MdnBlobIds, ]); let account_id = request.account_id.document_id(); let email_submission_ids = self @@ -140,82 +138,124 @@ impl EmailSubmissionGet for Server { is_pending = true; } - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { let value = match property { - Property::Id => Value::Id(id), - Property::DeliveryStatus => { - let mut status = Object::with_capacity(delivery_status.len()); + EmailSubmissionProperty::Id => Value::Element(id.into()), + EmailSubmissionProperty::DeliveryStatus => { + let mut status = Map::with_capacity(delivery_status.len()); for (rcpt, delivery_status) in std::mem::take(&mut delivery_status) { - status.set( - Property::_T(rcpt), - Object::with_capacity(3) - .with_property( - Property::Delivered, - delivery_status.delivered.as_str().to_string(), + status.insert_unchecked( + Key::Owned(rcpt), + Map::with_capacity(3) + .with_key_value( + EmailSubmissionProperty::Delivered, + EmailSubmissionValue::Delivered( + match delivery_status.delivered { + Delivered::Queued => { + email_submission::Delivered::Queued + } + Delivered::Yes => email_submission::Delivered::Yes, + Delivered::No => email_submission::Delivered::No, + Delivered::Unknown => { + email_submission::Delivered::Unknown + } + }, + ), ) - .with_property(Property::SmtpReply, delivery_status.smtp_reply) - .with_property(Property::Displayed, "unknown"), + .with_key_value( + EmailSubmissionProperty::SmtpReply, + delivery_status.smtp_reply, + ) + .with_key_value( + EmailSubmissionProperty::Displayed, + Value::Element(EmailSubmissionValue::Displayed( + Displayed::Unknown, + )), + ), ); } Value::Object(status) } - Property::UndoStatus => Value::Text( - { - if is_pending { - UndoStatus::Pending.as_str() - } else { - submission.undo_status.as_str() + EmailSubmissionProperty::UndoStatus => { + Value::Element(EmailSubmissionValue::UndoStatus(if is_pending { + email_submission::UndoStatus::Pending + } else { + match submission.undo_status { + ArchivedUndoStatus::Pending => { + email_submission::UndoStatus::Pending + } + ArchivedUndoStatus::Final => email_submission::UndoStatus::Final, + ArchivedUndoStatus::Canceled => { + email_submission::UndoStatus::Canceled + } } - } - .to_string(), - ), - Property::EmailId => Value::Id(Id::from_parts( - u32::from(submission.thread_id), - u32::from(submission.email_id), - )), - Property::IdentityId => Value::Id(Id::from(u32::from(submission.identity_id))), - Property::ThreadId => Value::Id(Id::from(u32::from(submission.thread_id))), - Property::Envelope => build_envelope(&submission.envelope), - Property::SendAt => { - Value::Date(UTCDate::from_timestamp(u64::from(submission.send_at) as i64)) + })) + } + EmailSubmissionProperty::EmailId => Value::Element( + Id::from_parts( + u32::from(submission.thread_id), + u32::from(submission.email_id), + ) + .into(), + ), + EmailSubmissionProperty::IdentityId => { + Value::Element(Id::from(u32::from(submission.identity_id)).into()) + } + EmailSubmissionProperty::ThreadId => { + Value::Element(Id::from(u32::from(submission.thread_id)).into()) + } + EmailSubmissionProperty::Envelope => build_envelope(&submission.envelope), + EmailSubmissionProperty::SendAt => Value::Element(EmailSubmissionValue::Date( + UTCDate::from_timestamp(u64::from(submission.send_at) as i64), + )), + EmailSubmissionProperty::MdnBlobIds | EmailSubmissionProperty::DsnBlobIds => { + Value::Array(vec![]) } - Property::MdnBlobIds | Property::DsnBlobIds => Value::List(vec![]), _ => Value::Null, }; - result.append(property.clone(), value); + result.insert_unchecked(property.clone(), value); } - response.list.push(result); + response.list.push(result.into()); } Ok(response) } } -fn build_envelope(envelope: &ArchivedEnvelope) -> Value { - Object::with_capacity(2) - .with_property(Property::MailFrom, build_address(&envelope.mail_from)) - .with_property( - Property::RcptTo, - Value::List(envelope.rcpt_to.iter().map(build_address).collect()), +fn build_envelope( + envelope: &ArchivedEnvelope, +) -> Value<'static, EmailSubmissionProperty, EmailSubmissionValue> { + Map::with_capacity(2) + .with_key_value( + EmailSubmissionProperty::MailFrom, + build_address(&envelope.mail_from), + ) + .with_key_value( + EmailSubmissionProperty::RcptTo, + Value::Array(envelope.rcpt_to.iter().map(build_address).collect()), ) .into() } -fn build_address(envelope: &ArchivedAddress) -> Value { - Object::with_capacity(2) - .with_property(Property::Email, Value::Text(envelope.email.to_string())) - .with_property( - Property::Parameters, +fn build_address( + envelope: &ArchivedAddress, +) -> Value<'static, EmailSubmissionProperty, EmailSubmissionValue> { + Map::with_capacity(2) + .with_key_value( + EmailSubmissionProperty::Email, + Value::Str(envelope.email.to_string().into()), + ) + .with_key_value( + EmailSubmissionProperty::Parameters, if let ArchivedOption::Some(params) = &envelope.parameters { - Value::Object(Object( + Value::Object(Map::from_iter( params .iter() - .map(|(k, v)| (Property::_T(k.to_string()), v.into())) - .collect(), + .map(|(k, v)| (Key::Owned(k.to_string()), v.into())), )) } else { Value::Null diff --git a/crates/jmap/src/submission/query.rs b/crates/jmap/src/submission/query.rs index 90f96cc3..6be0681c 100644 --- a/crates/jmap/src/submission/query.rs +++ b/crates/jmap/src/submission/query.rs @@ -7,8 +7,9 @@ use crate::{JmapMethods, changes::state::StateManager}; use common::Server; use email::submission::UndoStatus; -use jmap_proto::method::query::{ - Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty, +use jmap_proto::{ + method::query::{Comparator, Filter, QueryRequest, QueryResponse}, + object::email_submission::{self, EmailSubmissionComparator, EmailSubmissionFilter}, }; use std::future::Future; use store::{ @@ -23,73 +24,80 @@ use types::{ pub trait EmailSubmissionQuery: Sync + Send { fn email_submission_query( &self, - request: QueryRequest, + request: QueryRequest, ) -> impl Future> + Send; } impl EmailSubmissionQuery for Server { async fn email_submission_query( &self, - mut request: QueryRequest, + mut request: QueryRequest, ) -> trc::Result { 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::IdentityIds(ids) => { - filters.push(query::Filter::Or); - for id in ids { - filters.push(query::Filter::eq( - EmailSubmissionField::IdentityId, - id.document_id().serialize(), - )); + Filter::Property(cond) => match cond { + EmailSubmissionFilter::IdentityIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq( + EmailSubmissionField::IdentityId, + id.document_id().serialize(), + )); + } + filters.push(query::Filter::End); } - filters.push(query::Filter::End); - } - Filter::EmailIds(ids) => { - filters.push(query::Filter::Or); - for id in ids { - filters.push(query::Filter::eq( - EmailSubmissionField::EmailId, - id.id().serialize(), - )); + EmailSubmissionFilter::EmailIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq( + EmailSubmissionField::EmailId, + id.id().serialize(), + )); + } + filters.push(query::Filter::End); } - filters.push(query::Filter::End); - } - Filter::ThreadIds(ids) => { - filters.push(query::Filter::Or); - for id in ids { - filters.push(query::Filter::eq( - EmailSubmissionField::ThreadId, - id.document_id().serialize(), - )); + EmailSubmissionFilter::ThreadIds(ids) => { + filters.push(query::Filter::Or); + for id in ids { + filters.push(query::Filter::eq( + EmailSubmissionField::ThreadId, + id.document_id().serialize(), + )); + } + filters.push(query::Filter::End); } - filters.push(query::Filter::End); - } - Filter::UndoStatus(undo_status) => filters.push(query::Filter::eq( - EmailSubmissionField::UndoStatus, - UndoStatus::parse(&undo_status) - .unwrap_or(UndoStatus::Pending) - .as_index() - .serialize(), - )), - Filter::Before(before) => filters.push(query::Filter::lt( - EmailSubmissionField::SendAt, - (before.timestamp() as u64).serialize(), - )), - Filter::After(after) => filters.push(query::Filter::gt( - EmailSubmissionField::SendAt, - (after.timestamp() as u64).serialize(), - )), + EmailSubmissionFilter::UndoStatus(undo_status) => { + filters.push(query::Filter::eq( + EmailSubmissionField::UndoStatus, + match undo_status { + email_submission::UndoStatus::Pending => UndoStatus::Pending, + email_submission::UndoStatus::Final => UndoStatus::Final, + email_submission::UndoStatus::Canceled => UndoStatus::Canceled, + } + .as_index() + .serialize(), + )) + } + EmailSubmissionFilter::Before(before) => filters.push(query::Filter::lt( + EmailSubmissionField::SendAt, + (before.timestamp() as u64).serialize(), + )), + EmailSubmissionFilter::After(after) => filters.push(query::Filter::gt( + EmailSubmissionField::SendAt, + (after.timestamp() as u64).serialize(), + )), + + EmailSubmissionFilter::_T(other) => { + return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other)); + } + }, + Filter::And | Filter::Or | Filter::Not | Filter::Close => { filters.push(cond.into()); } - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); - } } } @@ -112,25 +120,23 @@ impl EmailSubmissionQuery for Server { for comparator in request .sort .and_then(|s| if !s.is_empty() { s.into() } else { None }) - .unwrap_or_else(|| vec![Comparator::descending(SortProperty::SentAt)]) + .unwrap_or_else(|| vec![Comparator::descending(EmailSubmissionComparator::SentAt)]) { comparators.push(match comparator.property { - SortProperty::EmailId => query::Comparator::field( + EmailSubmissionComparator::EmailId => query::Comparator::field( EmailSubmissionField::EmailId, comparator.is_ascending, ), - SortProperty::ThreadId => query::Comparator::field( + EmailSubmissionComparator::ThreadId => query::Comparator::field( EmailSubmissionField::ThreadId, comparator.is_ascending, ), - SortProperty::SentAt => query::Comparator::field( + EmailSubmissionComparator::SentAt => query::Comparator::field( EmailSubmissionField::SendAt, comparator.is_ascending, ), - other => { - return Err(trc::JmapEvent::UnsupportedSort - .into_err() - .details(other.to_string())); + EmailSubmissionComparator::_T(other) => { + return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other)); } }); } diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 636cc917..df2a7e92 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -19,18 +19,12 @@ use email::{ use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{self, SetRequest, SetResponse}, - object::email_submission::SetArguments, + object::email_submission, request::{ Call, RequestMethod, method::{MethodFunction, MethodName, MethodObject}, - reference::MaybeReference, - }, - response::references::EvalObjectReferences, - types::{ - property::Property, - state::State, - value::{MaybePatchValue, Object, SetValue, Value}, }, + types::state::State, }; use mail_parser::{ArchivedHeaderName, ArchivedHeaderValue}; use smtp::{ @@ -46,29 +40,29 @@ use types::{blob_hash::BlobHash, collection::Collection, field::EmailField, id:: use utils::{map::vec_map::VecMap, sanitize_email}; pub trait EmailSubmissionSet: Sync + Send { - fn email_submission_set( + fn email_submission_set<'x>( &self, - request: SetRequest, + request: SetRequest<'x, email_submission::EmailSubmission>, instance: &Arc, next_call: &mut Option>, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn send_message( &self, account_id: u32, - response: &SetResponse, + response: &SetResponse, instance: &Arc, object: Object, ) -> impl Future>> + Send; } impl EmailSubmissionSet for Server { - async fn email_submission_set( + async fn email_submission_set<'x>( &self, - mut request: SetRequest, + mut request: SetRequest<'x, email_submission::EmailSubmission>, instance: &Arc, - next_call: &mut Option>, - ) -> trc::Result { + next_call: &mut Option>>, + ) -> trc::Result> { let account_id = request.account_id.document_id(); let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy(); @@ -144,7 +138,7 @@ impl EmailSubmissionSet for Server { }; if let ( Property::UndoStatus, - MaybePatchValue::Value(Value::Text(undo_status_)), + MaybePatchValue::Value(Value::Str(undo_status_)), Some(queue_id_), ) = (&property, value, submission.inner.queue_id) { @@ -154,7 +148,7 @@ impl EmailSubmissionSet for Server { response.not_updated.append( id, SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Field could not be set."), ); continue 'update; @@ -197,7 +191,7 @@ impl EmailSubmissionSet for Server { response.not_updated.append( id, SetError::invalid_properties() - .with_property(Property::UndoStatus) + .with_key_value(Property::UndoStatus) .with_description("Email submissions can only be cancelled."), ); } @@ -309,7 +303,7 @@ impl EmailSubmissionSet for Server { async fn send_message( &self, account_id: u32, - response: &SetResponse, + response: &SetResponse, instance: &Arc, object: Object, ) -> trc::Result> { @@ -360,7 +354,7 @@ impl EmailSubmissionSet for Server { } Err(err) => { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description(format!( "Failed to parse mailFrom parameters: {err}." )))); @@ -371,7 +365,7 @@ impl EmailSubmissionSet for Server { return Ok(Err(err)); } }, - (Property::RcptTo, Value::List(value)) => { + (Property::RcptTo, Value::Array(value)) => { for addr in value { match parse_envelope_address(addr) { Ok((addr, params, smtp_params)) => { @@ -400,7 +394,7 @@ impl EmailSubmissionSet for Server { } Err(err) => { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description(format!( "Failed to parse rcptTo parameters: {err}." )))); @@ -415,7 +409,7 @@ impl EmailSubmissionSet for Server { } _ => { return Ok(Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description(format!( "Invalid object property {property}." )))); @@ -426,10 +420,10 @@ impl EmailSubmissionSet for Server { (Property::Envelope, MaybePatchValue::Value(Value::Null)) => { continue; } - (Property::UndoStatus, MaybePatchValue::Value(Value::Text(_))) => continue, + (Property::UndoStatus, MaybePatchValue::Value(Value::Str(_))) => continue, _ => { return Ok(Err(SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Field could not be set."))); } } @@ -456,7 +450,7 @@ impl EmailSubmissionSet for Server { .to_string() } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::IdentityId) + .with_key_value(Property::IdentityId) .with_description("Identity not found."))); }; @@ -493,7 +487,7 @@ impl EmailSubmissionSet for Server { metadata } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::EmailId) + .with_key_value(Property::EmailId) .with_description("Email not found."))); }; let metadata = metadata_ @@ -566,7 +560,7 @@ impl EmailSubmissionSet for Server { message } else { return Ok(Err(SetError::invalid_properties() - .with_property(Property::EmailId) + .with_key_value(Property::EmailId) .with_description("Blob for email not found."))); }; @@ -691,7 +685,7 @@ fn parse_envelope_address( SetError, > { if let Value::Object(mut envelope) = envelope { - if let Some(Value::Text(addr)) = envelope.0.remove(&Property::Email) { + if let Some(Value::Str(addr)) = envelope.0.remove(&Property::Email) { if let Some(addr) = sanitize_email(&addr) { if let Some(Value::Object(params)) = envelope.0.remove(&Property::Parameters) { let mut params_text = String::new(); @@ -705,7 +699,7 @@ fn parse_envelope_address( params_text.push(' '); } params_text.push_str(&k); - if let Value::Text(v) = v { + if let Value::Str(v) = v { params_text.push('='); params_text.push_str(&v); params_list.append(k, Some(v)); @@ -722,17 +716,17 @@ fn parse_envelope_address( } } else { Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description(format!("Invalid e-mail address {addr:?}."))) } } else { Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description("Missing e-mail address field.")) } } else { Err(SetError::invalid_properties() - .with_property(Property::Envelope) + .with_key_value(Property::Envelope) .with_description("Invalid envelope object.")) } } diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs index 481cbdd5..171e4edc 100644 --- a/crates/jmap/src/thread/get.rs +++ b/crates/jmap/src/thread/get.rs @@ -8,9 +8,11 @@ use crate::changes::state::StateManager; use common::Server; use email::cache::MessageCacheFetch; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - types::{property::Property, value::Object}, + method::get::{GetRequest, GetResponse}, + object::thread::{Thread, ThreadProperty, ThreadValue}, + request::MaybeInvalid, }; +use jmap_tools::Map; use std::future::Future; use store::{ ahash::AHashMap, @@ -27,15 +29,15 @@ use types::{ pub trait ThreadGet: Sync + Send { fn thread_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; } impl ThreadGet for Server { async fn thread_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let account_id = request.account_id.document_id(); let mut thread_map: AHashMap = AHashMap::with_capacity(32); for item in &self @@ -61,9 +63,10 @@ impl ThreadGet for Server { .map(Into::into) .collect() }; - let add_email_ids = request - .properties - .is_none_or(|p| p.unwrap().contains(&Property::EmailIds)); + let add_email_ids = request.properties.is_none_or(|p| { + p.unwrap() + .contains(&MaybeInvalid::Value(ThreadProperty::EmailIds)) + }); let mut response = GetResponse { account_id: request.account_id.into(), state: self @@ -77,11 +80,12 @@ impl ThreadGet for Server { for id in ids { let thread_id = id.document_id(); if let Some(document_ids) = thread_map.remove(&thread_id) { - let mut thread = Object::with_capacity(2).with_property(Property::Id, id); + let mut thread: Map<'_, ThreadProperty, ThreadValue> = + Map::with_capacity(2).with_key_value(ThreadProperty::Id, id); if add_email_ids { let doc_count = document_ids.len() as usize; - thread.append( - Property::EmailIds, + thread.insert_unchecked( + ThreadProperty::EmailIds, self.core .storage .data @@ -98,7 +102,7 @@ impl ThreadGet for Server { .collect::>(), ); } - response.list.push(thread); + response.list.push(thread.into()); } else { response.not_found.push(id.into()); } diff --git a/crates/jmap/src/vacation/get.rs b/crates/jmap/src/vacation/get.rs index 7cce5000..45353597 100644 --- a/crates/jmap/src/vacation/get.rs +++ b/crates/jmap/src/vacation/get.rs @@ -4,18 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{JmapMethods, changes::state::StateManager}; use common::Server; use email::sieve::SieveScript; use jmap_proto::{ - method::get::{GetRequest, GetResponse, RequestArguments}, - request::reference::MaybeReference, - types::{ - any_id::AnyId, - date::UTCDate, - property::Property, - value::{Object, Value}, + method::get::{GetRequest, GetResponse}, + object::vacation_response::{ + VacationResponse, VacationResponseProperty, VacationResponseValue, }, + request::reference::MaybeResultReference, + types::date::UTCDate, }; +use jmap_tools::{Map, Value}; use std::future::Future; use store::query::Filter; use trc::AddContext; @@ -25,13 +25,11 @@ use types::{ id::Id, }; -use crate::{JmapMethods, changes::state::StateManager}; - pub trait VacationResponseGet: Sync + Send { fn vacation_response_get( &self, - request: GetRequest, - ) -> impl Future> + Send; + request: GetRequest, + ) -> impl Future>> + Send; fn get_vacation_sieve_script_id( &self, @@ -42,17 +40,17 @@ pub trait VacationResponseGet: Sync + Send { impl VacationResponseGet for Server { async fn vacation_response_get( &self, - mut request: GetRequest, - ) -> trc::Result { + mut request: GetRequest, + ) -> trc::Result> { let account_id = request.account_id.document_id(); let properties = request.unwrap_properties(&[ - Property::Id, - Property::IsEnabled, - Property::FromDate, - Property::ToDate, - Property::Subject, - Property::TextBody, - Property::HtmlBody, + VacationResponseProperty::Id, + VacationResponseProperty::IsEnabled, + VacationResponseProperty::FromDate, + VacationResponseProperty::ToDate, + VacationResponseProperty::Subject, + VacationResponseProperty::TextBody, + VacationResponseProperty::HtmlBody, ]); let mut response = GetResponse { account_id: request.account_id.into(), @@ -64,11 +62,11 @@ impl VacationResponseGet for Server { not_found: vec![], }; - let do_get = if let Some(MaybeReference::Value(ids)) = request.ids { + let do_get = if let Some(MaybeResultReference::Value(ids)) = request.ids { let mut do_get = false; for id in ids { match id.try_unwrap() { - Some(AnyId::Id(id)) if id.is_singleton() => { + Some(id) if id.is_singleton() => { do_get = true; } Some(id) => { @@ -91,63 +89,66 @@ impl VacationResponseGet for Server { .unarchive::() .caused_by(trc::location!())?; let vacation = sieve.vacation_response.as_ref(); - let mut result = Object::with_capacity(properties.len()); + let mut result = Map::with_capacity(properties.len()); for property in &properties { match property { - Property::Id => { - result.append(Property::Id, Value::Id(Id::singleton())); + VacationResponseProperty::Id => { + result.insert_unchecked( + VacationResponseProperty::Id, + Id::singleton(), + ); } - Property::IsEnabled => { - result.append(Property::IsEnabled, sieve.is_active); + VacationResponseProperty::IsEnabled => { + result.insert_unchecked( + VacationResponseProperty::IsEnabled, + sieve.is_active, + ); } - Property::FromDate => { - result.append( - Property::FromDate, + VacationResponseProperty::FromDate => { + result.insert_unchecked( + VacationResponseProperty::FromDate, vacation.and_then(|r| { r.from_date .as_ref() .map(u64::from) .map(UTCDate::from) - .map(Value::Date) + .map(|v| Value::Element(VacationResponseValue::Date(v))) }), ); } - Property::ToDate => { - result.append( - Property::ToDate, + VacationResponseProperty::ToDate => { + result.insert_unchecked( + VacationResponseProperty::ToDate, vacation.and_then(|r| { r.to_date .as_ref() .map(u64::from) .map(UTCDate::from) - .map(Value::Date) + .map(|v| Value::Element(VacationResponseValue::Date(v))) }), ); } - Property::Subject => { - result.append( - Property::Subject, - vacation.and_then(|r| r.subject.as_ref().map(Value::from)), + VacationResponseProperty::Subject => { + result.insert_unchecked( + VacationResponseProperty::Subject, + vacation.and_then(|r| r.subject.as_ref()), ); } - Property::TextBody => { - result.append( - Property::TextBody, - vacation.and_then(|r| r.text_body.as_ref().map(Value::from)), + VacationResponseProperty::TextBody => { + result.insert_unchecked( + VacationResponseProperty::TextBody, + vacation.and_then(|r| r.text_body.as_ref()), ); } - Property::HtmlBody => { - result.append( - Property::HtmlBody, - vacation.and_then(|r| r.html_body.as_ref().map(Value::from)), + VacationResponseProperty::HtmlBody => { + result.insert_unchecked( + VacationResponseProperty::HtmlBody, + vacation.and_then(|r| r.html_body.as_ref()), ); } - property => { - result.append(property.clone(), Value::Null); - } } } - response.list.push(result); + response.list.push(result.into()); } else { response.not_found.push(Id::singleton().into()); } diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index acbe2efa..523e9ccf 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -12,13 +12,9 @@ use email::sieve::{ }; use jmap_proto::{ error::set::{SetError, SetErrorType}, - method::set::{RequestArguments, SetRequest, SetResponse}, - response::references::EvalObjectReferences, - types::{ - date::UTCDate, - property::Property, - value::{MaybePatchValue, Object, Value}, - }, + method::set::{SetRequest, SetResponse}, + object::vacation_response, + types::date::UTCDate, }; use mail_builder::MessageBuilder; use mail_parser::decoders::html::html_to_text; @@ -37,9 +33,9 @@ use types::{ pub trait VacationResponseSet: Sync + Send { fn vacation_response_set( &self, - request: SetRequest, + request: SetRequest<'_, vacation_response::VacationResponse>, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn build_script(&self, obj: &mut SieveScript) -> trc::Result>; } @@ -47,9 +43,9 @@ pub trait VacationResponseSet: Sync + Send { impl VacationResponseSet for Server { async fn vacation_response_set( &self, - mut request: SetRequest, + mut request: SetRequest<'_, vacation_response::VacationResponse>, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { let account_id = request.account_id.document_id(); let mut response = self .prepare_set_response( @@ -177,19 +173,19 @@ impl VacationResponseSet for Server { } }; match (&property, value) { - (Property::Subject, MaybePatchValue::Value(Value::Text(value))) + (Property::Subject, MaybePatchValue::Value(Value::Str(value))) if value.len() < 512 => { build_script = true; vacation.subject = Some(value); } - (Property::HtmlBody, MaybePatchValue::Value(Value::Text(value))) + (Property::HtmlBody, MaybePatchValue::Value(Value::Str(value))) if value.len() < 2048 => { build_script = true; vacation.html_body = Some(value); } - (Property::TextBody, MaybePatchValue::Value(Value::Text(value))) + (Property::TextBody, MaybePatchValue::Value(Value::Str(value))) if value.len() < 2048 => { build_script = true; @@ -244,7 +240,7 @@ impl VacationResponseSet for Server { response, create_id, SetError::invalid_properties() - .with_property(property) + .with_key_value(property) .with_description("Field could not be set."), )); } @@ -310,7 +306,7 @@ impl VacationResponseSet for Server { if let Some(create_id) = create_id { response.created.insert( create_id, - Object::with_capacity(1).with_property(Property::Id, Id::singleton()), + Map::with_capacity(1).with_key_value(Property::Id, Id::singleton()), ); } else { response.updated.append(Id::singleton(), None); diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index b8f32367..f83111d5 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -7,6 +7,7 @@ resolver = "2" [dependencies] utils = { path = "../utils" } trc = { path = "../trc" } +jmap-tools = { path = "/Users/me/code/jmap-tool" } hashify = "0.2" serde = { version = "1.0", features = ["derive"]} rkyv = { version = "0.8.10", features = ["little_endian"] } diff --git a/crates/types/src/blob.rs b/crates/types/src/blob.rs index 4d1367da..5fbd3f9f 100644 --- a/crates/types/src/blob.rs +++ b/crates/types/src/blob.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use jmap_tools::{Element, Property, Value}; use std::{borrow::Borrow, str::FromStr, time::SystemTime}; use utils::codec::{ base32_custom::{Base32Reader, Base32Writer}, @@ -240,3 +241,9 @@ impl std::fmt::Display for BlobId { f.write_str(&writer.finalize()) } } + +impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { + fn from(id: BlobId) -> Self { + Value::Element(E::from(id)) + } +} diff --git a/crates/types/src/id.rs b/crates/types/src/id.rs index 7167988f..568841b9 100644 --- a/crates/types/src/id.rs +++ b/crates/types/src/id.rs @@ -5,6 +5,7 @@ */ use crate::DocumentId; +use jmap_tools::{Element, Property, Value}; use std::{ops::Deref, str::FromStr}; use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE}; @@ -205,6 +206,12 @@ impl std::fmt::Display for Id { } } +impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { + fn from(id: Id) -> Self { + Value::Element(E::from(id)) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/types/src/keyword.rs b/crates/types/src/keyword.rs index 0e747b8a..4f56cb33 100644 --- a/crates/types/src/keyword.rs +++ b/crates/types/src/keyword.rs @@ -6,6 +6,8 @@ use std::fmt::Display; +use jmap_tools::{Element, Property, Value}; + pub const SEEN: usize = 0; pub const DRAFT: usize = 1; pub const FLAGGED: usize = 2; @@ -261,3 +263,9 @@ impl<'de> serde::Deserialize<'de> for Keyword { Ok(Keyword::parse(<&str>::deserialize(deserializer)?)) } } + +impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { + fn from(id: Keyword) -> Self { + Value::Element(E::from(id)) + } +} diff --git a/crates/types/src/special_use.rs b/crates/types/src/special_use.rs index 7f6ad63a..82c82fc1 100644 --- a/crates/types/src/special_use.rs +++ b/crates/types/src/special_use.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use jmap_tools::{Element, Property, Value}; use utils::config::utils::ParseValue; #[derive( @@ -98,3 +99,9 @@ impl ParseValue for SpecialUse { SpecialUse::parse(value).ok_or_else(|| format!("Unknown folder role {:?}", value)) } } + +impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { + fn from(id: SpecialUse) -> Self { + Value::Element(E::from(id)) + } +} diff --git a/crates/types/src/type_state.rs b/crates/types/src/type_state.rs index d5b621af..d02627ac 100644 --- a/crates/types/src/type_state.rs +++ b/crates/types/src/type_state.rs @@ -5,6 +5,7 @@ */ use crate::collection::SyncCollection; +use jmap_tools::{Element, Property, Value}; use serde::Serialize; use std::{fmt::Display, str::FromStr}; use utils::map::bitmap::{Bitmap, BitmapItem}; @@ -222,3 +223,9 @@ impl<'de> serde::Deserialize<'de> for DataType { .ok_or_else(|| serde::de::Error::custom("invalid JMAP data type")) } } + +impl<'x, P: Property, E: Element + From> From for Value<'x, P, E> { + fn from(id: DataType) -> Self { + Value::Element(E::from(id)) + } +}