diff --git a/Cargo.lock b/Cargo.lock index b3e6ffbf..8aa8a36d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,6 +1014,7 @@ dependencies = [ "hashify", "mail-builder", "mail-parser", + "rkyv 0.8.10", "serde", ] @@ -1670,7 +1671,7 @@ checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" [[package]] name = "dav" -version = "0.11.5" +version = "0.11.7" dependencies = [ "common", "dav-proto", @@ -1689,7 +1690,7 @@ dependencies = [ [[package]] name = "dav-proto" -version = "0.1.0" +version = "0.11.7" dependencies = [ "calcard", "hashify", @@ -2738,7 +2739,7 @@ dependencies = [ [[package]] name = "groupware" -version = "0.11.5" +version = "0.11.7" dependencies = [ "calcard", "common", @@ -3027,7 +3028,7 @@ dependencies = [ [[package]] name = "http" -version = "0.11.5" +version = "0.11.7" dependencies = [ "async-stream", "base64 0.22.1", @@ -3135,7 +3136,7 @@ dependencies = [ [[package]] name = "http_proto" -version = "0.11.5" +version = "0.11.7" dependencies = [ "common", "form_urlencoded", @@ -4181,7 +4182,7 @@ dependencies = [ "directory", "email", "groupware", - "http 0.11.5", + "http 0.11.7", "imap", "jemallocator", "jmap", @@ -6639,7 +6640,7 @@ dependencies = [ [[package]] name = "services" -version = "0.11.5" +version = "0.11.7" dependencies = [ "aes-gcm", "aes-gcm-siv", @@ -7206,7 +7207,7 @@ dependencies = [ "flate2", "form_urlencoded", "futures", - "http 0.11.5", + "http 0.11.7", "http-body-util", "http_proto", "hyper 1.6.0", diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 71b212ad..4f304bdc 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -125,9 +125,9 @@ impl Caches { MB_10, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), - files: Cache::from_config( + dav: Cache::from_config( config, - "file", + "dav", MB_10, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index dc00ea1a..5d1df925 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -146,7 +146,7 @@ pub struct Caches { pub account: Cache>, pub mailbox: Cache>, pub threads: Cache>, - pub files: Cache>, + pub dav: Cache>, pub bayes: CacheWithTtl, @@ -250,15 +250,21 @@ pub struct Threads { pub struct NameWrapper(pub String); +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct DavResourceId { + pub account_id: u32, + pub collection: u8, +} + #[derive(Debug, Default)] -pub struct Files { - pub files: IdBimap, +pub struct DavResources { + pub files: IdBimap, pub size: u64, pub modseq: Option, } #[derive(Debug, Default)] -pub struct FileItem { +pub struct DavResource { pub document_id: u32, pub parent_id: Option, pub name: String, @@ -296,6 +302,12 @@ impl CacheItemWeight for MailboxId { } } +impl CacheItemWeight for DavResourceId { + fn weight(&self) -> u64 { + std::mem::size_of::() as u64 + } +} + impl CacheItemWeight for Threads { fn weight(&self) -> u64 { ((self.threads.len() + 2) * std::mem::size_of::()) as u64 @@ -320,7 +332,7 @@ impl CacheItemWeight for HttpAuthCache { } } -impl CacheItemWeight for Files { +impl CacheItemWeight for DavResources { fn weight(&self) -> u64 { self.size } @@ -441,7 +453,7 @@ impl Default for Caches { account: Cache::new(1024, 10 * 1024 * 1024), mailbox: Cache::new(1024, 10 * 1024 * 1024), threads: Cache::new(1024, 10 * 1024 * 1024), - files: Cache::new(1024, 10 * 1024 * 1024), + dav: Cache::new(1024, 10 * 1024 * 1024), bayes: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_rbl: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_txt: CacheWithTtl::new(1024, 10 * 1024 * 1024), @@ -493,8 +505,8 @@ pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec { } } -impl Files { - pub fn subtree(&self, search_path: &str) -> impl Iterator { +impl DavResources { + pub fn subtree(&self, search_path: &str) -> impl Iterator { let prefix = format!("{search_path}/"); self.files .iter() @@ -505,7 +517,7 @@ impl Files { &self, search_path: &str, depth: usize, - ) -> impl Iterator { + ) -> impl Iterator { let prefix = format!("{search_path}/"); self.files.iter().filter(move |item| { item.name @@ -515,7 +527,7 @@ impl Files { }) } - pub fn tree_with_depth(&self, depth: usize) -> impl Iterator { + pub fn tree_with_depth(&self, depth: usize) -> impl Iterator { self.files.iter().filter(move |item| { item.name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth }) @@ -530,7 +542,7 @@ impl Files { } } -impl IdBimapItem for FileItem { +impl IdBimapItem for DavResource { fn id(&self) -> &u32 { &self.document_id } @@ -540,21 +552,21 @@ impl IdBimapItem for FileItem { } } -impl std::hash::Hash for FileItem { +impl std::hash::Hash for DavResource { fn hash(&self, state: &mut H) { self.document_id.hash(state); } } -impl PartialEq for FileItem { +impl PartialEq for DavResource { fn eq(&self, other: &Self) -> bool { self.document_id == other.document_id } } -impl Eq for FileItem {} +impl Eq for DavResource {} -impl std::borrow::Borrow for FileItem { +impl std::borrow::Borrow for DavResource { fn borrow(&self) -> &u32 { &self.document_id } diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 182105b2..2c9e640c 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -1,17 +1,17 @@ [package] name = "dav-proto" -version = "0.1.0" +version = "0.11.7" edition = "2021" [dependencies] hashify = "0.2.6" quick-xml = "0.37.2" -calcard = { path = "/Users/me/code/calcard" } +calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } mail-parser = "0.10.2" hyper = "1.6.0" rkyv = { version = "0.8.10", features = ["little_endian"] } [dev-dependencies] -calcard = { path = "/Users/me/code/calcard", features = ["serde"] } +calcard = { path = "/Users/me/code/calcard", features = ["serde", "rkyv"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs index 13f55c4b..4b8756c9 100644 --- a/crates/dav-proto/src/requests/report.rs +++ b/crates/dav-proto/src/requests/report.rs @@ -40,7 +40,7 @@ impl DavParser for Report { NamedElement { ns: Namespace::CardDav, element: Element::AddressbookQuery, - } => AddressbookQuery::parse(stream).map(Report::Addressbook), + } => AddressbookQuery::parse(stream).map(Report::AddressbookQuery), NamedElement { ns: Namespace::CardDav, element: Element::AddressbookMultiget, diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs index 7bb63b7a..65c3ddb6 100644 --- a/crates/dav-proto/src/schema/request.rs +++ b/crates/dav-proto/src/schema/request.rs @@ -58,18 +58,23 @@ pub struct LockInfo { #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(test, serde(tag = "type"))] pub enum Report { - Addressbook(AddressbookQuery), + AddressbookQuery(AddressbookQuery), AddressbookMultiGet(MultiGet), CalendarQuery(CalendarQuery), CalendarMultiGet(MultiGet), FreeBusyQuery(FreeBusyQuery), SyncCollection(SyncCollection), + ExpandProperty(ExpandProperty), AclPrincipalPropSet(AclPrincipalPropSet), PrincipalMatch(PrincipalMatch), PrincipalPropertySearch(PrincipalPropertySearch), PrincipalSearchPropertySet, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct ExpandProperty {} + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct AddressbookQuery { diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index d8076158..02c467bd 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav" -version = "0.11.5" +version = "0.11.7" edition = "2024" resolver = "2" diff --git a/crates/dav/src/card/acl.rs b/crates/dav/src/card/acl.rs new file mode 100644 index 00000000..dc7edea8 --- /dev/null +++ b/crates/dav/src/card/acl.rs @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::Acl}; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardAclRequestHandler: Sync + Send { + fn handle_card_acl_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: Acl, + ) -> impl Future> + Send; +} + +impl CardAclRequestHandler for Server { + async fn handle_card_acl_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: Acl, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/copy_move.rs b/crates/dav/src/card/copy_move.rs new file mode 100644 index 00000000..46632d9f --- /dev/null +++ b/crates/dav/src/card/copy_move.rs @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::RequestHeaders; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardCopyMoveRequestHandler: Sync + Send { + fn handle_card_copy_move_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + is_move: bool, + ) -> impl Future> + Send; +} + +impl CardCopyMoveRequestHandler for Server { + async fn handle_card_copy_move_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + is_move: bool, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/delete.rs b/crates/dav/src/card/delete.rs new file mode 100644 index 00000000..6ca1d6a5 --- /dev/null +++ b/crates/dav/src/card/delete.rs @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::RequestHeaders; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardDeleteRequestHandler: Sync + Send { + fn handle_card_delete_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + ) -> impl Future> + Send; +} + +impl CardDeleteRequestHandler for Server { + async fn handle_card_delete_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/get.rs b/crates/dav/src/card/get.rs new file mode 100644 index 00000000..6ca0caf5 --- /dev/null +++ b/crates/dav/src/card/get.rs @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::MultiGet}; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardGetRequestHandler: Sync + Send { + fn handle_card_get_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + is_head: bool, + ) -> impl Future> + Send; + + fn handle_card_multiget_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: MultiGet, + ) -> impl Future> + Send; +} + +impl CardGetRequestHandler for Server { + async fn handle_card_get_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + is_head: bool, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } + + async fn handle_card_multiget_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: MultiGet, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/mkcol.rs b/crates/dav/src/card/mkcol.rs new file mode 100644 index 00000000..3c6a8ee7 --- /dev/null +++ b/crates/dav/src/card/mkcol.rs @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::MkCol}; +use http_proto::HttpResponse; +use hyper::StatusCode; + +use crate::{DavError, common::uri::DavUriResource}; + +pub(crate) trait CardMkColRequestHandler: Sync + Send { + fn handle_card_mkcol_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: Option, + ) -> impl Future> + Send; +} + +impl CardMkColRequestHandler for Server { + async fn handle_card_mkcol_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: Option, + ) -> crate::Result { + // Validate URI + let resource = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + if resource.resource.is_none_or(|r| r.contains('/')) + || !access_token.is_member(resource.account_id) + { + return Err(DavError::Code(StatusCode::FORBIDDEN)); + } + + todo!() + } +} diff --git a/crates/dav/src/card/mod.rs b/crates/dav/src/card/mod.rs index c8a832f8..8624d509 100644 --- a/crates/dav/src/card/mod.rs +++ b/crates/dav/src/card/mod.rs @@ -3,3 +3,13 @@ * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ + +pub mod acl; +pub mod copy_move; +pub mod delete; +pub mod get; +pub mod mkcol; +pub mod propfind; +pub mod proppatch; +pub mod query; +pub mod update; diff --git a/crates/dav/src/card/propfind.rs b/crates/dav/src/card/propfind.rs new file mode 100644 index 00000000..012e6893 --- /dev/null +++ b/crates/dav/src/card/propfind.rs @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::PropFind}; +use http_proto::HttpResponse; + +use crate::common::{DavQuery, uri::DavUriResource}; + +pub(crate) trait CardPropFindRequestHandler: Sync + Send { + fn handle_card_propfind_request( + &self, + access_token: &AccessToken, + query: DavQuery<'_>, + ) -> impl Future> + Send; +} + +impl CardPropFindRequestHandler for Server { + async fn handle_card_propfind_request( + &self, + access_token: &AccessToken, + query: DavQuery<'_>, + ) -> crate::Result { + // Validate URI + + todo!() + } +} diff --git a/crates/dav/src/card/proppatch.rs b/crates/dav/src/card/proppatch.rs new file mode 100644 index 00000000..60ced889 --- /dev/null +++ b/crates/dav/src/card/proppatch.rs @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::PropertyUpdate}; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardPropPatchRequestHandler: Sync + Send { + fn handle_card_proppatch_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: PropertyUpdate, + ) -> impl Future> + Send; +} + +impl CardPropPatchRequestHandler for Server { + async fn handle_card_proppatch_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: PropertyUpdate, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs new file mode 100644 index 00000000..e24f8cd0 --- /dev/null +++ b/crates/dav/src/card/query.rs @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{RequestHeaders, schema::request::AddressbookQuery}; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardQueryRequestHandler: Sync + Send { + fn handle_card_query_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: AddressbookQuery, + ) -> impl Future> + Send; +} + +impl CardQueryRequestHandler for Server { + async fn handle_card_query_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: AddressbookQuery, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/card/update.rs b/crates/dav/src/card/update.rs new file mode 100644 index 00000000..b8e53ab1 --- /dev/null +++ b/crates/dav/src/card/update.rs @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::RequestHeaders; +use http_proto::HttpResponse; + +use crate::common::uri::DavUriResource; + +pub(crate) trait CardUpdateRequestHandler: Sync + Send { + fn handle_card_update_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + bytes: Vec, + is_patch: bool, + ) -> impl Future> + Send; +} + +impl CardUpdateRequestHandler for Server { + async fn handle_card_update_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + bytes: Vec, + is_patch: bool, + ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + + todo!() + } +} diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 87b29638..b1833c14 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -28,6 +28,7 @@ use trc::AddContext; use crate::{ DavErrorCondition, + card::propfind::CardPropFindRequestHandler, common::uri::DavUriResource, file::propfind::HandleFilePropFindRequest, principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, @@ -86,7 +87,7 @@ impl PropFindRequestHandler for Server { access_token, DavQuery::propfind( UriResource::new_owned( - Collection::FileNode, + resource.collection, account_id, resource.resource, ), @@ -97,7 +98,21 @@ impl PropFindRequestHandler for Server { .await } Collection::Calendar => todo!(), - Collection::AddressBook => todo!(), + Collection::AddressBook => { + self.handle_card_propfind_request( + access_token, + DavQuery::propfind( + UriResource::new_owned( + resource.collection, + account_id, + resource.resource, + ), + request, + headers, + ), + ) + .await + } Collection::Principal => { let mut response = MultiStatus::new(Vec::with_capacity(16)); diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index b1f0f08f..9ba977eb 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -9,7 +9,7 @@ use std::fmt::Display; use common::{Server, auth::AccessToken}; use directory::backend::internal::manage::ManageDirectory; -use groupware::file::hierarchy::FileHierarchy; +use groupware::hierarchy::DavHierarchy; use http_proto::request::decode_path_element; use hyper::StatusCode; use jmap_proto::types::collection::Collection; @@ -103,35 +103,28 @@ impl DavUriResource for Server { } async fn map_uri_resource(&self, uri: OwnedUri<'_>) -> trc::Result> { - let todo = "map cal, card"; - - let resource = if let Some(resource) = uri.resource { - resource - } else { - return Ok(None); - }; - - let document_id = match uri.collection { - Collection::FileNode => self - .fetch_file_hierarchy(uri.account_id) + if let Some(resource) = uri.resource { + if let Some(resource) = self + .fetch_dav_hierarchy(uri.account_id, uri.collection) .await .caused_by(trc::location!())? .files .by_name(resource) - .map(|f| f.document_id), - Collection::Calendar => todo!(), - Collection::CalendarEvent => todo!(), - Collection::AddressBook => todo!(), - Collection::ContactCard => todo!(), - _ => None, - }; - - if let Some(document_id) = document_id { - Ok(Some(DocumentUri { - collection: uri.collection, - account_id: uri.account_id, - resource: document_id, - })) + { + Ok(Some(DocumentUri { + collection: if resource.is_container || uri.collection == Collection::FileNode { + uri.collection + } else if uri.collection == Collection::Calendar { + Collection::CalendarEvent + } else { + Collection::ContactCard + }, + account_id: uri.account_id, + resource: resource.document_id, + })) + } else { + Ok(None) + } } else { Ok(None) } diff --git a/crates/dav/src/file/acl.rs b/crates/dav/src/file/acl.rs index 39200899..cd44f7c8 100644 --- a/crates/dav/src/file/acl.rs +++ b/crates/dav/src/file/acl.rs @@ -6,7 +6,10 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; -use groupware::file::{ArchivedFileNode, FileNode, hierarchy::FileHierarchy}; +use groupware::{ + file::{ArchivedFileNode, FileNode}, + hierarchy::DavHierarchy, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; @@ -51,7 +54,7 @@ impl FileAclRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index e2f4f45c..f7590d75 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -6,9 +6,9 @@ use std::sync::Arc; -use common::{Files, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::{Depth, RequestHeaders}; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{ @@ -31,7 +31,7 @@ use crate::{ file::{DavFileResource, FileItemId, insert_file_node, update_file_node}, }; -use super::{FromFileItem, delete::delete_files, delete_file_node}; +use super::{FromDavResource, delete::delete_files, delete_file_node}; pub(crate) trait FileCopyMoveRequestHandler: Sync + Send { fn handle_file_copy_move_request( @@ -56,7 +56,7 @@ impl FileCopyMoveRequestHandler for Server { .into_owned_uri()?; let from_account_id = from_resource_.account_id; let from_files = self - .fetch_file_hierarchy(from_account_id) + .fetch_dav_hierarchy(from_account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let from_resource = from_files.map_resource::(&from_resource_)?; @@ -113,7 +113,7 @@ impl FileCopyMoveRequestHandler for Server { let to_files = if to_account_id == from_account_id { from_files.clone() } else { - self.fetch_file_hierarchy(to_account_id) + self.fetch_dav_hierarchy(to_account_id, Collection::FileNode) .await .caused_by(trc::location!())? }; @@ -130,7 +130,7 @@ impl FileCopyMoveRequestHandler for Server { if let Some(mut existing_destination) = to_files .files .by_name(destination_resource_name) - .map(Destination::from_file_item) + .map(Destination::from_dav_resource) { if !headers.overwrite_fail { existing_destination.account_id = to_account_id; @@ -350,8 +350,8 @@ impl Default for Destination { async fn move_container( server: &Server, access_token: &AccessToken, - from_files: Arc, - to_files: Arc, + from_files: Arc, + to_files: Arc, from_resource: UriResource, destination: Destination, depth: Depth, @@ -413,7 +413,7 @@ async fn move_container( async fn copy_container( server: &Server, access_token: &AccessToken, - from_files: Arc, + from_files: Arc, from_resource: UriResource, mut destination: Destination, depth: Depth, @@ -846,8 +846,8 @@ async fn rename_item( Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag)) } -impl FromFileItem for Destination { - fn from_file_item(item: &common::FileItem) -> Self { +impl FromDavResource for Destination { + fn from_dav_resource(item: &common::DavResource) -> Self { Destination { account_id: u32::MAX, document_id: Some(item.document_id), diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 4631dfb5..62fded71 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -6,7 +6,7 @@ use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::RequestHeaders; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{ @@ -50,7 +50,7 @@ impl FileDeleteRequestHandler for Server { .filter(|r| !r.is_empty()) .ok_or(DavError::Code(StatusCode::FORBIDDEN))?; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index 4d3a579a..1c29dbc3 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -6,7 +6,7 @@ use common::{Server, auth::AccessToken}; use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime}; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; @@ -46,7 +46,7 @@ impl FileGetRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 98e1a692..917c7188 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -9,7 +9,7 @@ use dav_proto::{ RequestHeaders, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, }; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, type_state::DataType}; @@ -51,7 +51,7 @@ impl FileMkColRequestHandler for Server { .into_owned_uri()?; let account_id = resource_.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_parent_resource(&resource_)?; diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index bc8e58ab..605d4a76 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -4,7 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{FileItem, Files, Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; +use common::{ + DavResource, DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder, +}; use groupware::file::{ArchivedFileNode, FileNode}; use hyper::StatusCode; use jmap_proto::types::{collection::Collection, type_state::DataType}; @@ -31,8 +33,8 @@ pub mod propfind; pub mod proppatch; pub mod update; -pub(crate) trait FromFileItem { - fn from_file_item(item: &FileItem) -> Self; +pub(crate) trait FromDavResource { + fn from_dav_resource(item: &DavResource) -> Self; } pub(crate) struct FileItemId { @@ -42,22 +44,23 @@ pub(crate) struct FileItemId { } pub(crate) trait DavFileResource { - fn map_resource( + fn map_resource( &self, resource: &OwnedUri<'_>, ) -> crate::Result>; - fn map_parent<'x, T: FromFileItem>(&self, resource: &'x str) -> Option<(Option, &'x str)>; + fn map_parent<'x, T: FromDavResource>(&self, resource: &'x str) + -> Option<(Option, &'x str)>; #[allow(clippy::type_complexity)] - fn map_parent_resource<'x, T: FromFileItem>( + fn map_parent_resource<'x, T: FromDavResource>( &self, resource: &OwnedUri<'x>, ) -> crate::Result, &'x str)>>; } -impl DavFileResource for Files { - fn map_resource( +impl DavFileResource for DavResources { + fn map_resource( &self, resource: &OwnedUri<'_>, ) -> crate::Result> { @@ -67,15 +70,18 @@ impl DavFileResource for Files { .map(|r| UriResource { collection: resource.collection, account_id: resource.account_id, - resource: T::from_file_item(r), + resource: T::from_dav_resource(r), }) .ok_or(DavError::Code(StatusCode::NOT_FOUND)) } - fn map_parent<'x, T: FromFileItem>(&self, resource: &'x str) -> Option<(Option, &'x str)> { + fn map_parent<'x, T: FromDavResource>( + &self, + resource: &'x str, + ) -> Option<(Option, &'x str)> { let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') { ( - Some(self.files.by_name(parent).map(T::from_file_item)?), + Some(self.files.by_name(parent).map(T::from_dav_resource)?), child, ) } else { @@ -85,7 +91,7 @@ impl DavFileResource for Files { Some((parent, child)) } - fn map_parent_resource<'x, T: FromFileItem>( + fn map_parent_resource<'x, T: FromDavResource>( &self, resource: &OwnedUri<'x>, ) -> crate::Result, &'x str)>> { @@ -107,14 +113,14 @@ impl DavFileResource for Files { } } -impl FromFileItem for u32 { - fn from_file_item(item: &FileItem) -> Self { +impl FromDavResource for u32 { + fn from_dav_resource(item: &DavResource) -> Self { item.document_id } } -impl FromFileItem for FileItemId { - fn from_file_item(item: &FileItem) -> Self { +impl FromDavResource for FileItemId { + fn from_dav_resource(item: &DavResource) -> Self { FileItemId { document_id: item.document_id, parent_id: item.parent_id, diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 2f169356..299701a9 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{FileItem, Server, auth::AccessToken}; +use common::{Server, auth::AccessToken}; use dav_proto::schema::{ property::{ DavProperty, DavValue, Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedLock, @@ -16,7 +16,7 @@ use dav_proto::schema::{ SupportedPrivilege, }, }; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; @@ -58,7 +58,7 @@ impl HandleFilePropFindRequest for Server { ) -> crate::Result { let account_id = query.resource.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; @@ -657,11 +657,11 @@ static ALL_PROPS: [DavProperty; 17] = [ struct Paths<'x> { min: u32, max: u32, - items: AHashMap, + items: AHashMap, } impl<'x> Paths<'x> { - pub fn new(iter: impl Iterator) -> Self { + pub fn new(iter: impl Iterator) -> Self { let mut paths = Paths { min: u32::MAX, max: 0, diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index ec7c483e..bbc68be9 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -13,7 +13,7 @@ use dav_proto::{ response::{BaseCondition, MultiStatus, PropStat, Response}, }, }; -use groupware::file::{FileNode, hierarchy::FileHierarchy}; +use groupware::{file::FileNode, hierarchy::DavHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; @@ -64,7 +64,7 @@ impl FilePropPatchRequestHandler for Server { let uri = headers.uri; let account_id = resource_.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource = files.map_resource(&resource_)?; diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index 927d1bc4..cd2a76e7 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -6,7 +6,10 @@ use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime}; -use groupware::file::{FileNode, FileProperties, hierarchy::FileHierarchy}; +use groupware::{ + file::{FileNode, FileProperties}, + hierarchy::DavHierarchy, +}; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::{ @@ -58,7 +61,7 @@ impl FileUpdateRequestHandler for Server { .into_owned_uri()?; let account_id = resource.account_id; let files = self - .fetch_file_hierarchy(account_id) + .fetch_dav_hierarchy(account_id, Collection::FileNode) .await .caused_by(trc::location!())?; let resource_name = resource diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 96d3e833..80c75f39 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -25,6 +25,13 @@ use hyper::{StatusCode, header}; use crate::{ DavError, DavMethod, DavResource, + card::{ + acl::CardAclRequestHandler, copy_move::CardCopyMoveRequestHandler, + delete::CardDeleteRequestHandler, get::CardGetRequestHandler, + mkcol::CardMkColRequestHandler, propfind::CardPropFindRequestHandler, + proppatch::CardPropPatchRequestHandler, query::CardQueryRequestHandler, + update::CardUpdateRequestHandler, + }, common::{ DavQuery, acl::DavAclHandler, @@ -88,38 +95,46 @@ impl DavRequestDispatcher for Server { ) .await } - DavMethod::PROPPATCH => match resource { - DavResource::Card => todo!(), - DavResource::Cal => todo!(), - DavResource::File => { - self.handle_file_proppatch_request( - &access_token, - headers, - PropertyUpdate::parse(&mut Tokenizer::new(&body))?, - ) - .await + DavMethod::PROPPATCH => { + let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?; + match resource { + DavResource::Card => { + self.handle_card_proppatch_request(&access_token, headers, request) + .await + } + DavResource::Cal => todo!(), + DavResource::File => { + self.handle_file_proppatch_request(&access_token, headers, request) + .await + } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - }, - DavMethod::MKCOL => match resource { - DavResource::Card => todo!(), - DavResource::Cal => todo!(), - DavResource::File => { - self.handle_file_mkcol_request( - &access_token, - headers, - if !body.is_empty() { - Some(MkCol::parse(&mut Tokenizer::new(&body))?) - } else { - None - }, - ) - .await + } + DavMethod::MKCOL => { + let request = if !body.is_empty() { + Some(MkCol::parse(&mut Tokenizer::new(&body))?) + } else { + None + }; + + match resource { + DavResource::Card => { + self.handle_card_mkcol_request(&access_token, headers, request) + .await + } + DavResource::Cal => todo!(), + DavResource::File => { + self.handle_file_mkcol_request(&access_token, headers, request) + .await + } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - }, + } DavMethod::GET => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_get_request(&access_token, headers, false) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_get_request(&access_token, headers, false) @@ -128,7 +143,10 @@ impl DavRequestDispatcher for Server { DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::HEAD => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_get_request(&access_token, headers, true) + .await + } DavResource::Cal => todo!(), DavResource::File => { #[cfg(debug_assertions)] @@ -158,7 +176,10 @@ impl DavRequestDispatcher for Server { } match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_delete_request(&access_token, headers) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_delete_request(&access_token, headers) @@ -168,7 +189,10 @@ impl DavRequestDispatcher for Server { } } DavMethod::PUT | DavMethod::POST => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_update_request(&access_token, headers, body, false) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_update_request(&access_token, headers, body, false) @@ -177,7 +201,10 @@ impl DavRequestDispatcher for Server { DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::PATCH => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_update_request(&access_token, headers, body, true) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_update_request(&access_token, headers, body, true) @@ -186,7 +213,10 @@ impl DavRequestDispatcher for Server { DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::COPY => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_copy_move_request(&access_token, headers, false) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, false) @@ -195,7 +225,10 @@ impl DavRequestDispatcher for Server { DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::MOVE => match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_copy_move_request(&access_token, headers, false) + .await + } DavResource::Cal => todo!(), DavResource::File => { self.handle_file_copy_move_request(&access_token, headers, true) @@ -204,9 +237,8 @@ impl DavRequestDispatcher for Server { DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::LOCK => match resource { - DavResource::Card => todo!(), - DavResource::Cal => todo!(), - DavResource::File => { + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + _ => { self.handle_lock_request( &access_token, headers, @@ -218,40 +250,42 @@ impl DavRequestDispatcher for Server { ) .await } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), }, DavMethod::UNLOCK => { self.handle_lock_request(&access_token, headers, LockRequest::Unlock) .await } - DavMethod::ACL => match resource { - DavResource::Card => todo!(), - DavResource::Cal => todo!(), - DavResource::File => { - self.handle_file_acl_request( - &access_token, - headers, - Acl::parse(&mut Tokenizer::new(&body))?, - ) - .await + DavMethod::ACL => { + let request = Acl::parse(&mut Tokenizer::new(&body))?; + match resource { + DavResource::Card => { + self.handle_card_acl_request(&access_token, headers, request) + .await + } + DavResource::Cal => todo!(), + DavResource::File => { + self.handle_file_acl_request(&access_token, headers, request) + .await + } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), } - DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), - }, + } DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { Report::SyncCollection(sync_collection) => { let uri = self .validate_uri(&access_token, headers.uri) .await .and_then(|d| d.into_owned_uri())?; + let request = DavQuery::changes(uri, sync_collection, headers); match resource { - DavResource::Card => todo!(), + DavResource::Card => { + self.handle_card_propfind_request(&access_token, request) + .await + } DavResource::Cal => todo!(), DavResource::File => { - self.handle_file_propfind_request( - &access_token, - DavQuery::changes(uri, sync_collection, headers), - ) - .await + self.handle_file_propfind_request(&access_token, request) + .await } DavResource::Principal => { Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) @@ -287,11 +321,18 @@ impl DavRequestDispatcher for Server { Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) } } - Report::Addressbook(report) => todo!(), - Report::AddressbookMultiGet(report) => todo!(), + Report::AddressbookQuery(report) => { + self.handle_card_query_request(&access_token, headers, report) + .await + } + Report::AddressbookMultiGet(report) => { + self.handle_card_multiget_request(&access_token, headers, report) + .await + } Report::CalendarQuery(report) => todo!(), Report::CalendarMultiGet(report) => todo!(), Report::FreeBusyQuery(report) => todo!(), + Report::ExpandProperty(report) => todo!(), }, DavMethod::OPTIONS => unreachable!(), } diff --git a/crates/email/src/message/copy.rs b/crates/email/src/message/copy.rs index b32dcc8e..d96be1a2 100644 --- a/crates/email/src/message/copy.rs +++ b/crates/email/src/message/copy.rs @@ -108,7 +108,7 @@ impl EmailCopy for Server { | HeaderName::ResentMessageId => { header.value.visit_text(|id| { if !id.is_empty() && id.len() < MAX_ID_LENGTH { - references.push(id); + references.push(id.as_bytes()); } }); } @@ -128,7 +128,7 @@ impl EmailCopy for Server { // Obtain threadId let thread_id = self - .find_or_merge_thread(account_id, subject, &references) + .find_or_merge_thread(account_id, subject, references, None) .await .caused_by(trc::location!())?; diff --git a/crates/email/src/message/index.rs b/crates/email/src/message/index.rs index 1ceccb00..eed9b8fd 100644 --- a/crates/email/src/message/index.rs +++ b/crates/email/src/message/index.rs @@ -116,13 +116,9 @@ impl MessageMetadata { // Add ids to inverted index if id.len() < MAX_ID_LENGTH { if set { - batch - .index(Property::MessageId, id.serialize()) - .index(Property::References, id.serialize()); + batch.index(Property::References, encode_message_id(id)); } else { - batch - .unindex(Property::MessageId, id.serialize()) - .unindex(Property::References, id.serialize()); + batch.unindex(Property::References, encode_message_id(id)); } } }); @@ -229,6 +225,13 @@ impl MessageMetadata { } } +fn encode_message_id(message_id: &str) -> Vec { + let mut msg_id = Vec::with_capacity(message_id.len() + 1); + msg_id.extend_from_slice(message_id.as_bytes()); + msg_id.push(0); + msg_id +} + impl ArchivedMessageMetadata { #[inline(always)] pub fn root_part(&self) -> &ArchivedMessageMetadataPart { @@ -307,13 +310,9 @@ impl ArchivedMessageMetadata { // Add ids to inverted index if id.len() < MAX_ID_LENGTH { if set { - batch - .index(Property::MessageId, id.serialize()) - .index(Property::References, id.serialize()); + batch.index(Property::References, encode_message_id(id)); } else { - batch - .unindex(Property::MessageId, id.serialize()) - .unindex(Property::References, id.serialize()); + batch.unindex(Property::References, encode_message_id(id)); } } }); diff --git a/crates/email/src/message/ingest.rs b/crates/email/src/message/ingest.rs index b9dd6129..80790982 100644 --- a/crates/email/src/message/ingest.rs +++ b/crates/email/src/message/ingest.rs @@ -6,7 +6,6 @@ use std::{ borrow::Cow, - collections::BTreeSet, fmt::Write, time::{Duration, Instant}, }; @@ -37,7 +36,6 @@ use std::future::Future; use store::{ BlobClass, IndexKey, IndexKeyPrefix, IterateParams, U32_LEN, ahash::AHashMap, - query::Filter, roaring::RoaringBitmap, write::{ AlignedBytes, Archive, AssignedIds, BatchBuilder, MaybeDynamicValue, SerializeWithId, @@ -106,7 +104,8 @@ pub trait EmailIngest: Sync + Send { &self, account_id: u32, thread_name: &str, - references: &[&str], + references: Vec<&[u8]>, + skip_duplicate: Option<(&[u8], u32)>, ) -> impl Future> + Send; fn assign_imap_uid( &self, @@ -258,7 +257,7 @@ impl EmailIngest for Server { } // Obtain message references and thread name - let mut message_id = String::new(); + let mut message_id = None; let thread_id = { let mut references = Vec::with_capacity(5); let mut subject = ""; @@ -266,10 +265,11 @@ impl EmailIngest for Server { match &header.name { HeaderName::MessageId => header.value.visit_text(|id| { if !id.is_empty() && id.len() < MAX_ID_LENGTH { - if message_id.is_empty() { - message_id = id.to_string(); + // Used by find_or_merge_thread to skip duplicates + if params.source.is_smtp() && message_id.is_none() { + message_id = references.len().into(); } - references.push(id); + references.push(id.as_bytes()); } }), HeaderName::InReplyTo @@ -277,7 +277,7 @@ impl EmailIngest for Server { | HeaderName::ResentMessageId => { header.value.visit_text(|id| { if !id.is_empty() && id.len() < MAX_ID_LENGTH { - references.push(id); + references.push(id.as_bytes()); } }); } @@ -295,29 +295,19 @@ impl EmailIngest for Server { } } - // Check for duplicates - if params.source.is_smtp() - && !message_id.is_empty() - && !self - .core - .storage - .data - .filter( - account_id, - Collection::Email, - vec![ - Filter::eq(Property::MessageId, message_id.as_str().serialize()), - Filter::is_in_bitmap( - Property::MailboxIds, - params.mailbox_ids.first().copied().unwrap_or(INBOX_ID), - ), - ], - ) - .await - .caused_by(trc::location!())? - .results - .is_empty() - { + let skip_duplicate = message_id.map(|idx| { + ( + references[idx], + params.mailbox_ids.first().copied().unwrap_or(INBOX_ID), + ) + }); + let thread_id = self + .find_or_merge_thread(account_id, subject, references, skip_duplicate) + .await?; + if thread_id != u32::MAX { + thread_id + } else { + // Duplicate message trc::event!( MessageIngest(MessageIngestEvent::Duplicate), SpanId = params.session_id, @@ -333,9 +323,6 @@ impl EmailIngest for Server { size: 0, }); } - - self.find_or_merge_thread(account_id, subject, &references) - .await? }; // Add additional headers to message @@ -586,7 +573,8 @@ impl EmailIngest for Server { &self, account_id: u32, thread_name: &str, - references: &[&str], + mut references: Vec<&[u8]>, + skip_duplicate: Option<(&[u8], u32)>, ) -> trc::Result { if references.is_empty() { return self.create_thread_id(account_id).await; @@ -599,10 +587,9 @@ impl EmailIngest for Server { "!" } .serialize(); - let references = references - .iter() - .map(|r| r.as_bytes()) - .collect::>(); + + // Sort references ascending + references.sort_unstable(); loop { // Find messages with a matching subject @@ -642,12 +629,15 @@ impl EmailIngest for Server { ) .await .caused_by(trc::location!())?; + + // No matching subjects were found, skip early if subj_results.is_empty() { return self.create_thread_id(account_id).await; } // Find messages with matching references let mut results = RoaringBitmap::new(); + let mut found_message_id = Vec::new(); self.store() .iterate( IterateParams::new( @@ -670,13 +660,27 @@ impl EmailIngest for Server { .ascending(), |key, _| { let id_pos = key.len() - U32_LEN; - let value = key.get(IndexKeyPrefix::len()..id_pos).ok_or_else(|| { - trc::Error::corrupted_key(key, None, trc::location!()) - })?; + let mut value = + key.get(IndexKeyPrefix::len()..id_pos).ok_or_else(|| { + trc::Error::corrupted_key(key, None, trc::location!()) + })?; let document_id = key.deserialize_be_u32(id_pos)?; - if subj_results.contains(document_id) && references.contains(value) { + if let Some(message_id) = value.strip_suffix(&[0]) { + value = message_id; + if skip_duplicate.is_some_and(|(message_id, _)| message_id == value) { + found_message_id.push(document_id); + } + } + + if subj_results.contains(document_id) + && references.binary_search(&value).is_ok() + { results.insert(document_id); + + if subj_results.len() == results.len() { + return Ok(false); + } } Ok(true) @@ -684,10 +688,30 @@ impl EmailIngest for Server { ) .await .caused_by(trc::location!())?; + + // No matching messages if results.is_empty() { return self.create_thread_id(account_id).await; } + // Skip duplicate messages + if !found_message_id.is_empty() { + if let Some(ids) = self + .get_tag( + account_id, + Collection::Email, + Property::MailboxIds, + skip_duplicate.unwrap().1, + ) + .await + .caused_by(trc::location!())? + { + if found_message_id.iter().any(|id| ids.contains(*id)) { + return Ok(u32::MAX); + } + } + } + // Find the most common threadId let mut thread_counts = AHashMap::::with_capacity(16); let mut thread_id = u32::MAX; diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index eaaa33d2..ebfb6a04 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "groupware" -version = "0.11.5" +version = "0.11.7" edition = "2024" resolver = "2" @@ -12,7 +12,7 @@ jmap_proto = { path = "../jmap-proto" } trc = { path = "../trc" } directory = { path = "../directory" } dav-proto = { path = "../dav-proto" } -calcard = { path = "/Users/me/code/calcard" } +calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } hashify = "0.2" rkyv = { version = "0.8.10", features = ["little_endian"] } percent-encoding = "2.3.1" diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index f2e2419b..058336b7 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -28,12 +28,15 @@ pub struct CalendarPreferences { pub is_subscribed: bool, pub is_default: bool, pub is_visible: bool, - /*pub include_in_availability: IncludeInAvailability, + pub include_in_availability: IncludeInAvailability, pub default_alerts_with_time: VecMap, pub default_alerts_without_time: VecMap, - pub time_zone: Timezone,*/ + pub time_zone: Timezone, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] pub struct CalendarEvent { pub name: Option, pub event: ICalendar, @@ -47,15 +50,24 @@ pub struct CalendarEvent { pub is_draft: bool, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] pub enum Timezone { IANA(String), Custom(ICalendar), + #[default] Default, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +#[rkyv(derive(Debug))] pub enum IncludeInAvailability { All, Attending, + #[default] None, } diff --git a/crates/groupware/src/contact/index.rs b/crates/groupware/src/contact/index.rs new file mode 100644 index 00000000..3ac559c5 --- /dev/null +++ b/crates/groupware/src/contact/index.rs @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject}; +use jmap_proto::types::{property::Property, value::AclGrant}; + +use super::{AddressBook, ArchivedAddressBook, ContactCard}; + +impl IndexableObject for AddressBook { + fn index_values(&self) -> impl Iterator> { + [ + IndexValue::Text { + field: Property::Name.into(), + value: self.name.as_str().into(), + }, + IndexValue::Acl { + value: (&self.acls).into(), + }, + IndexValue::Quota { + used: self.dead_properties.size() as u32 + + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + + self.description.as_ref().map_or(0, |n| n.len() as u32) + + self.name.len() as u32, + }, + ] + .into_iter() + } +} + +impl IndexableObject for &ArchivedAddressBook { + fn index_values(&self) -> impl Iterator> { + [ + IndexValue::Text { + field: Property::Name.into(), + value: self.name.as_str().into(), + }, + IndexValue::Acl { + value: self + .acls + .iter() + .map(AclGrant::from) + .collect::>() + .into(), + }, + IndexValue::Quota { + used: self.dead_properties.size() as u32 + + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + + self.description.as_ref().map_or(0, |n| n.len() as u32) + + self.name.len() as u32, + }, + ] + .into_iter() + } +} + +impl IndexableAndSerializableObject for AddressBook {} + +impl IndexableObject for ContactCard { + fn index_values(&self) -> impl Iterator> { + [ + IndexValue::Text { + field: Property::Name.into(), + value: self.name.as_str().into(), + }, + IndexValue::U32List { + field: Property::ParentId.into(), + value: self.addressbook_ids.as_slice().into(), + }, + IndexValue::Quota { + used: self.dead_properties.size() as u32 + + self.display_name.as_ref().map_or(0, |n| n.len() as u32) + + self.name.len() as u32 + + self.size, + }, + ] + .into_iter() + } +} diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index a4f4bee3..1d8daeaa 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +pub mod index; + use calcard::vcard::VCard; +use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; use store::{SERIALIZE_OBJ_15_V1, SerializedVersion}; @@ -19,6 +22,7 @@ pub struct AddressBook { pub sort_order: u32, pub is_default: bool, pub subscribers: Vec, + pub dead_properties: DeadProperty, pub acls: Vec, } @@ -29,13 +33,19 @@ pub enum AddressBookRight { Delete, } +#[derive( + rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, +)] +#[rkyv(derive(Debug))] pub struct ContactCard { - pub name: Option, + pub name: String, pub display_name: Option, pub addressbook_ids: Vec, pub card: VCard, + pub dead_properties: DeadProperty, pub created: u64, pub updated: u64, + pub size: u32, } impl TryFrom for AddressBookRight { diff --git a/crates/groupware/src/file/hierarchy.rs b/crates/groupware/src/file/hierarchy.rs deleted file mode 100644 index 37506e5e..00000000 --- a/crates/groupware/src/file/hierarchy.rs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::sync::Arc; - -use common::{FileItem, Files, Server}; -use jmap_proto::types::collection::Collection; -use trc::AddContext; -use utils::bimap::IdBimap; - -use crate::file::FileNode; - -pub trait FileHierarchy: Sync + Send { - fn fetch_file_hierarchy( - &self, - account_id: u32, - ) -> impl Future>> + Send; -} - -impl FileHierarchy for Server { - async fn fetch_file_hierarchy(&self, account_id: u32) -> trc::Result> { - let change_id = self - .store() - .get_last_change_id(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())?; - if let Some(files) = self - .inner - .cache - .files - .get(&account_id) - .filter(|x| x.modseq == change_id) - { - Ok(files) - } else { - let mut files = build_file_hierarchy(self, account_id).await?; - files.modseq = change_id; - let files = Arc::new(files); - self.inner.cache.files.insert(account_id, files.clone()); - Ok(files) - } - } -} - -async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result { - let list = server - .fetch_folders::(account_id, Collection::FileNode) - .await - .caused_by(trc::location!())?; - let mut files = Files { - files: IdBimap::with_capacity(list.len()), - size: std::mem::size_of::() as u64, - modseq: None, - }; - - for expanded in list.into_iterator() { - files.size += (std::mem::size_of::() - + std::mem::size_of::() - + expanded.name.len()) as u64; - files.files.insert(FileItem { - document_id: expanded.document_id, - parent_id: expanded.parent_id, - name: expanded.name, - size: expanded.size, - is_container: expanded.is_container, - hierarchy_sequence: expanded.hierarchy_sequence, - }); - } - - Ok(files) -} diff --git a/crates/groupware/src/file/mod.rs b/crates/groupware/src/file/mod.rs index 5fba656d..9322e3a5 100644 --- a/crates/groupware/src/file/mod.rs +++ b/crates/groupware/src/file/mod.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -pub mod hierarchy; pub mod index; use dav_proto::schema::request::DeadProperty; diff --git a/crates/groupware/src/hierarchy.rs b/crates/groupware/src/hierarchy.rs new file mode 100644 index 00000000..57c7ad8a --- /dev/null +++ b/crates/groupware/src/hierarchy.rs @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::sync::Arc; + +use common::{DavResource, DavResourceId, DavResources, Server}; +use jmap_proto::types::{collection::Collection, property::Property}; +use store::{ + Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap, + write::key::DeserializeBigEndian, +}; +use trc::AddContext; +use utils::bimap::IdBimap; + +use crate::file::FileNode; + +pub trait DavHierarchy: Sync + Send { + fn fetch_dav_hierarchy( + &self, + account_id: u32, + collection: Collection, + ) -> impl Future>> + Send; +} + +impl DavHierarchy for Server { + async fn fetch_dav_hierarchy( + &self, + account_id: u32, + collection: Collection, + ) -> trc::Result> { + let change_id = self + .store() + .get_last_change_id(account_id, collection) + .await + .caused_by(trc::location!())?; + let resource_id = DavResourceId { + account_id, + collection: collection.into(), + }; + if let Some(files) = self + .inner + .cache + .dav + .get(&resource_id) + .filter(|x| x.modseq == change_id) + { + Ok(files) + } else { + let mut files = match collection { + Collection::Calendar | Collection::AddressBook => { + build_hierarchy(self, account_id, collection).await? + } + Collection::FileNode => build_file_hierarchy(self, account_id).await?, + _ => unreachable!(), + }; + + files.modseq = change_id; + let files = Arc::new(files); + self.inner.cache.dav.insert(resource_id, files.clone()); + Ok(files) + } + } +} + +#[derive(Default)] +struct DavTempResource { + name: String, + parent_id: Vec, +} + +async fn build_hierarchy( + server: &Server, + account_id: u32, + collection: Collection, +) -> trc::Result { + let collection = u8::from(collection); + let mut containers: AHashMap = AHashMap::with_capacity(16); + let mut resources: AHashMap = AHashMap::with_capacity(16); + + server + .store() + .iterate( + IterateParams::new( + IndexKey { + account_id, + collection, + document_id: 0, + field: 0, + key: 0u32.serialize(), + }, + IndexKey { + account_id, + collection: collection + 1, + document_id: u32::MAX, + field: u8::MAX, + key: u32::MAX.serialize(), + }, + ) + .no_values() + .ascending(), + |key, _| { + let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; + let value = key + .get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN) + .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; + let key_collection = key + .get(U32_LEN) + .copied() + .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; + let key_property = key + .get(U32_LEN + 1) + .copied() + .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?; + + let resource = if key_collection == collection { + containers.entry(document_id).or_default() + } else { + resources.entry(document_id).or_default() + }; + + if key_property == u8::from(Property::Value) { + resource.name = std::str::from_utf8(value) + .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))? + .to_string(); + } else if key_property == u8::from(Property::ParentId) { + resource.parent_id.push( + u32::deserialize(value) + .map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?, + ); + } + + Ok(true) + }, + ) + .await + .caused_by(trc::location!())?; + + let mut files = DavResources { + files: IdBimap::with_capacity(containers.len() + resources.len()), + size: std::mem::size_of::() as u64, + modseq: None, + }; + + for (document_id, resource) in resources { + for parent_id in resource.parent_id { + if let Some(container) = containers.get(&parent_id) { + let name = format!("{}/{}", container.name, resource.name); + files.size += (std::mem::size_of::() + + std::mem::size_of::() + + name.len()) as u64; + files.files.insert(DavResource { + document_id, + parent_id: parent_id.into(), + name, + size: 0, + is_container: false, + hierarchy_sequence: 1, + }); + } + } + } + + for (document_id, container) in containers { + files.size += (std::mem::size_of::() + + std::mem::size_of::() + + container.name.len()) as u64; + files.files.insert(DavResource { + document_id, + parent_id: None, + name: container.name, + size: 0, + is_container: true, + hierarchy_sequence: 0, + }); + } + + Ok(files) +} + +async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result { + let list = server + .fetch_folders::(account_id, Collection::FileNode) + .await + .caused_by(trc::location!())?; + let mut files = DavResources { + files: IdBimap::with_capacity(list.len()), + size: std::mem::size_of::() as u64, + modseq: None, + }; + + for expanded in list.into_iterator() { + files.size += (std::mem::size_of::() + + std::mem::size_of::() + + expanded.name.len()) as u64; + files.files.insert(DavResource { + document_id: expanded.document_id, + parent_id: expanded.parent_id, + name: expanded.name, + size: expanded.size, + is_container: expanded.is_container, + hierarchy_sequence: expanded.hierarchy_sequence, + }); + } + + Ok(files) +} diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index c78849e0..fbc592da 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -7,3 +7,4 @@ pub mod calendar; pub mod contact; pub mod file; +pub mod hierarchy; diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index e1a08aec..c6c5b942 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_proto" -version = "0.11.5" +version = "0.11.7" edition = "2024" resolver = "2" diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index e0022096..ecb32186 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.11.5" +version = "0.11.7" edition = "2024" resolver = "2" diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index f0b87d5d..2713c5b5 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "services" -version = "0.11.5" +version = "0.11.7" edition = "2024" resolver = "2"