From 1a8efb2182c4354c5395c4b587a323c5fd5b3347 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 18 Mar 2025 09:50:38 +0100 Subject: [PATCH] DAV Propfind partial impl --- crates/common/src/auth/access_token.rs | 49 ++-- crates/common/src/config/dav.rs | 2 + crates/common/src/lib.rs | 27 ++ crates/common/src/listener/acme/directory.rs | 4 +- crates/dav/src/common/lock.rs | 25 +- crates/dav/src/common/mod.rs | 60 ++++ crates/dav/src/common/propfind.rs | 139 ++++++++++ crates/dav/src/common/uri.rs | 40 ++- crates/dav/src/file/copy_move.rs | 37 +-- crates/dav/src/file/delete.rs | 9 +- crates/dav/src/file/get.rs | 7 +- crates/dav/src/file/mkcol.rs | 7 +- crates/dav/src/file/mod.rs | 26 +- crates/dav/src/file/propfind.rs | 271 ++++++++++++++++++- crates/dav/src/file/proppatch.rs | 7 +- crates/dav/src/file/update.rs | 7 +- crates/dav/src/principal/mod.rs | 2 + crates/dav/src/principal/propfind.rs | 41 +++ crates/dav/src/request.rs | 26 +- crates/email/src/identity/mod.rs | 4 +- crates/email/src/mailbox/mod.rs | 4 +- crates/email/src/message/crypto.rs | 4 +- crates/email/src/message/delete.rs | 1 + crates/email/src/message/metadata.rs | 6 +- crates/email/src/push/mod.rs | 4 +- crates/email/src/sieve/mod.rs | 4 +- crates/email/src/submission/mod.rs | 4 +- crates/email/src/thread/cache.rs | 15 - crates/groupware/src/file/mod.rs | 4 +- crates/http/src/auth/oauth/mod.rs | 4 +- crates/jmap-proto/src/types/keyword.rs | 6 - crates/jmap/src/principal/get.rs | 6 +- crates/smtp/src/queue/mod.rs | 4 +- crates/store/src/lib.rs | 20 +- crates/store/src/write/serialize.rs | 50 +++- 35 files changed, 770 insertions(+), 156 deletions(-) create mode 100644 crates/dav/src/common/propfind.rs create mode 100644 crates/dav/src/principal/propfind.rs diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 21537475..e34faa1c 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -6,12 +6,12 @@ use ahash::AHashSet; use directory::{ + Permission, Principal, QueryBy, Type, backend::internal::{ + PrincipalField, lookup::DirectoryStore, manage::{ChangedPrincipals, ManageDirectory}, - PrincipalField, }, - Permission, Principal, QueryBy, Type, }; use jmap_proto::{ request::RequestMethod, @@ -29,11 +29,11 @@ use utils::map::{ }; use crate::{ + KV_TOKEN_REVISION, Server, listener::limiter::{ConcurrencyLimiter, LimiterResult}, - Server, KV_TOKEN_REVISION, }; -use super::{roles::RolePermissions, AccessToken, ResourceToken, TenantInfo}; +use super::{AccessToken, ResourceToken, TenantInfo, roles::RolePermissions}; pub enum PrincipalOrId { Principal(Principal), @@ -189,7 +189,7 @@ impl Server { Ok(Some(principal)) => { return self .build_access_token_from_principal(principal, revision) - .await + .await; } Ok(None) => Err(trc::AuthEvent::Error .into_err() @@ -294,10 +294,11 @@ impl Server { } } Err(err) => { - trc::error!(err - .details("Failed to list principals") - .caused_by(trc::location!()) - .account_id(*id)); + trc::error!( + err.details("Failed to list principals") + .caused_by(trc::location!()) + .account_id(*id) + ); } } } else { @@ -330,10 +331,11 @@ impl Server { ids = members.into_iter(); } Err(err) => { - trc::error!(err - .details("Failed to obtain principal") - .caused_by(trc::location!()) - .account_id(id)); + trc::error!( + err.details("Failed to obtain principal") + .caused_by(trc::location!()) + .account_id(id) + ); } } } else if let Some(prev_ids) = ids_stack.pop() { @@ -354,9 +356,10 @@ impl Server { ) .await { - trc::error!(err - .details("Failed to increment principal revision") - .account_id(id)); + trc::error!( + err.details("Failed to increment principal revision") + .account_id(id) + ); } } @@ -371,9 +374,10 @@ impl Server { { Ok(revision) => (revision as u64).into(), Err(err) => { - trc::error!(err - .details("Failed to obtain principal revision") - .account_id(id)); + trc::error!( + err.details("Failed to obtain principal revision") + .account_id(id) + ); None } } @@ -436,6 +440,13 @@ impl AccessToken { .chain(self.access_to.iter().map(|(id, _)| id)) } + pub fn all_ids(&self) -> impl Iterator { + [self.primary_id] + .into_iter() + .chain(self.member_of.iter().copied()) + .chain(self.access_to.iter().map(|(id, _)| *id)) + } + pub fn is_member(&self, account_id: u32) -> bool { self.primary_id == account_id || self.member_of.contains(&account_id) diff --git a/crates/common/src/config/dav.rs b/crates/common/src/config/dav.rs index 4fd878b0..61a80112 100644 --- a/crates/common/src/config/dav.rs +++ b/crates/common/src/config/dav.rs @@ -12,6 +12,7 @@ pub struct DavConfig { pub dead_property_size: Option, pub live_property_size: usize, pub max_lock_timeout: u64, + pub max_changes: usize, } impl DavConfig { @@ -27,6 +28,7 @@ impl DavConfig { .property("dav.limits.size.live-property") .unwrap_or(250), max_lock_timeout: config.property("dav.limits.timeout.max-lock").unwrap_or(60), + max_changes: config.property("dav.limits.max-changes").unwrap_or(1000), } } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 3be14500..449b1f8c 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -41,6 +41,7 @@ use mail_auth::{MX, Txt}; use manager::webadmin::{Resource, WebAdminManager}; use nlp::bayes::{TokenHash, Weights}; use parking_lot::{Mutex, RwLock}; +use rkyv::munge::Borrow; use rustls::sign::CertifiedKey; use tokio::sync::{Notify, Semaphore, mpsc}; use tokio_rustls::TlsConnector; @@ -515,6 +516,12 @@ impl Files { }) } + 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 + }) + } + pub fn is_ancestor_of(&self, ancestor: u32, descendant: u32) -> bool { let ancestor = &self.files.by_id(ancestor).unwrap().name; let descendant = &self.files.by_id(descendant).unwrap().name; @@ -533,3 +540,23 @@ impl IdBimapItem for FileItem { &self.name } } + +impl std::hash::Hash for FileItem { + fn hash(&self, state: &mut H) { + self.document_id.hash(state); + } +} + +impl PartialEq for FileItem { + fn eq(&self, other: &Self) -> bool { + self.document_id == other.document_id + } +} + +impl Eq for FileItem {} + +impl std::borrow::Borrow for FileItem { + fn borrow(&self) -> &u32 { + &self.document_id + } +} diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs index ab37b0ec..00a51513 100644 --- a/crates/common/src/listener/acme/directory.rs +++ b/crates/common/src/listener/acme/directory.rs @@ -12,7 +12,7 @@ use ring::rand::SystemRandom; use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, EcdsaSigningAlgorithm}; use serde::Deserialize; use store::write::Archiver; -use store::{Serialize, SerializedVersion}; +use store::{Serialize, SerializedVersion, SERIALIZE_OBJ_01_V1}; use trc::AddContext; use trc::event::conv::AssertSuccess; @@ -212,7 +212,7 @@ pub struct SerializedCert { impl SerializedVersion for SerializedCert { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_01_V1 } } diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 2421690f..4af2c5ed 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -21,11 +21,11 @@ use jmap_proto::types::property::Property; use store::dispatch::lookup::KeyValue; use store::write::serialize::rkyv_deserialize; use store::write::{AlignedBytes, Archive, Archiver, now}; -use store::{Serialize, SerializedVersion, U32_LEN}; +use store::{SERIALIZE_OBJ_02_V1, Serialize, SerializedVersion, U32_LEN}; use trc::AddContext; use super::ETag; -use super::uri::{DavUriResource, UriResource}; +use super::uri::{DavUriResource, OwnedUri}; use crate::{DavError, DavErrorCondition, DavMethod}; #[derive(Debug, Clone)] @@ -63,14 +63,15 @@ impl LockRequestHandler for Server { headers: RequestHeaders<'_>, lock_info: Option, ) -> crate::Result { - let resource = self.validate_uri(access_token, headers.uri).await?; - let resource_hash = resource - .lock_key() - .ok_or(DavError::Code(StatusCode::CONFLICT))?; + let resource = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let resource_hash = resource.lock_key(); let resource_path = resource .resource .ok_or(DavError::Code(StatusCode::CONFLICT))?; - let account_id = resource.account_id.unwrap(); + let account_id = resource.account_id; if !access_token.is_member(account_id) { return Err(DavError::Code(StatusCode::FORBIDDEN)); } @@ -584,7 +585,7 @@ struct LockItem { impl SerializedVersion for LockData { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_02_V1 } } @@ -700,13 +701,13 @@ impl LockItem { } } -impl UriResource> { - pub fn lock_key(&self) -> Option> { +impl OwnedUri<'_> { + pub fn lock_key(&self) -> Vec { let mut result = Vec::with_capacity(U32_LEN + 2); result.push(KV_LOCK_DAV); - result.extend_from_slice(self.account_id?.to_be_bytes().as_slice()); + result.extend_from_slice(self.account_id.to_be_bytes().as_slice()); result.push(u8::from(self.collection)); - Some(result) + result } } diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index efa0ed7a..41ebd83a 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -4,16 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use dav_proto::{ + Depth, RequestHeaders, Return, + schema::request::{PropFind, SyncCollection}, +}; use jmap_proto::types::property::Property; use store::{ U32_LEN, write::{Archive, BatchBuilder, MaybeDynamicValue, Operation, ValueClass, ValueOp}, }; +use uri::OwnedUri; pub mod acl; pub mod lock; +pub mod propfind; pub mod uri; +pub(crate) struct DavQuery<'x> { + pub resource: OwnedUri<'x>, + pub base_uri: &'x str, + pub propfind: PropFind, + pub from_change_id: Option, + pub depth: usize, + pub limit: Option, + pub ret: Return, + pub depth_no_root: bool, +} + pub trait ETag { fn etag(&self) -> String; } @@ -48,3 +65,46 @@ impl ExtractETag for BatchBuilder { None } } + +impl<'x> DavQuery<'x> { + pub fn propfind( + resource: OwnedUri<'x>, + propfind: PropFind, + headers: RequestHeaders<'x>, + ) -> Self { + Self { + resource, + propfind, + base_uri: headers.base_uri().unwrap_or_default(), + from_change_id: None, + depth: match headers.depth { + Depth::Zero => 0, + _ => 1, + }, + limit: None, + ret: headers.ret, + depth_no_root: headers.depth_no_root, + } + } + + pub fn changes( + resource: OwnedUri<'x>, + changes: SyncCollection, + headers: RequestHeaders<'x>, + ) -> Self { + Self { + resource, + propfind: changes.properties, + base_uri: headers.base_uri().unwrap_or_default(), + from_change_id: changes.sync_token.and_then(|s| s.parse().ok()), + depth: if changes.level_inf { usize::MAX } else { 1 }, + limit: changes.limit, + ret: headers.ret, + depth_no_root: headers.depth_no_root, + } + } + + pub fn format_to_base_uri(&self, path: &str) -> String { + format!("{}/{}", self.base_uri, path) + } +} diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs new file mode 100644 index 00000000..23128d07 --- /dev/null +++ b/crates/dav/src/common/propfind.rs @@ -0,0 +1,139 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use dav_proto::{ + Depth, RequestHeaders, + schema::{ + request::PropFind, + response::{BaseCondition, MultiStatus, Response}, + }, +}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::collection::Collection; +use store::roaring::RoaringBitmap; + +use crate::{ + DavErrorCondition, + common::uri::DavUriResource, + file::propfind::HandleFilePropFindRequest, + principal::propfind::{PrincipalPropFind, PrincipalResource}, +}; + +use super::{DavQuery, uri::UriResource}; + +pub(crate) trait PropFindRequestHandler: Sync + Send { + fn handle_propfind_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: PropFind, + ) -> impl Future> + Send; +} + +impl PropFindRequestHandler for Server { + async fn handle_propfind_request( + &self, + access_token: &AccessToken, + headers: RequestHeaders<'_>, + request: PropFind, + ) -> crate::Result { + // Validate URI + let resource = self.validate_uri(access_token, headers.uri).await?; + + // Reject Infinity depth for certain queries + let return_children = match headers.depth { + Depth::One | Depth::None => true, + Depth::Zero => false, + Depth::Infinity => { + if resource.account_id.is_none() + || matches!(resource.collection, Collection::FileNode) + { + return Err(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::PropFindFiniteDepth, + ) + .into()); + } + true + } + }; + + // List shared resources + if let Some(account_id) = resource.account_id { + match resource.collection { + Collection::FileNode => { + self.handle_file_propfind_request( + access_token, + DavQuery::propfind( + UriResource::new_owned( + Collection::FileNode, + account_id, + resource.resource, + ), + request, + headers, + ), + ) + .await + } + Collection::Calendar => todo!(), + Collection::AddressBook => todo!(), + Collection::Principal => { + let mut response = MultiStatus::new(Vec::with_capacity(16)); + + if let Some(resource) = resource.resource { + response.add_response(Response::new_status( + [headers.format_to_base_uri(resource)], + StatusCode::NOT_FOUND, + )); + } else { + self.prepare_principal_propfind_response( + access_token, + PrincipalResource::Id(account_id), + &request, + &mut response, + ) + .await?; + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body(response.to_string())) + } + _ => unreachable!(), + } + } else { + let mut response = MultiStatus::new(Vec::with_capacity(16)); + + // Add container info + if !headers.depth_no_root { + let blah = 1; + } + + if return_children { + let ids = if !matches!(resource.collection, Collection::Principal) { + RoaringBitmap::from_iter(access_token.all_ids()) + } else { + // Return all principals + self.get_document_ids(u32::MAX, Collection::Principal) + .await? + .unwrap_or_default() + }; + + self.prepare_principal_propfind_response( + access_token, + PrincipalResource::Ids(ids), + &request, + &mut response, + ) + .await?; + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } + } +} diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 34c6100c..1d2f3670 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -14,18 +14,22 @@ use trc::AddContext; use crate::{DavError, DavResource}; -pub(crate) struct UriResource { +pub(crate) struct UriResource { pub collection: Collection, - pub account_id: Option, - pub resource: T, + pub account_id: A, + pub resource: R, } +pub(crate) type UnresolvedUri<'x> = UriResource, Option<&'x str>>; +pub(crate) type OwnedUri<'x> = UriResource>; +//pub(crate) type DocumentUri<'x> = UriResource; + pub(crate) trait DavUriResource: Sync + Send { fn validate_uri<'x>( &self, access_token: &AccessToken, uri: &'x str, - ) -> impl Future>>> + Send; + ) -> impl Future>> + Send; } impl DavUriResource for Server { @@ -33,7 +37,7 @@ impl DavUriResource for Server { &self, access_token: &AccessToken, uri: &'x str, - ) -> crate::Result>> { + ) -> crate::Result> { let (_, uri_parts) = uri .split_once("/dav/") .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; @@ -86,8 +90,28 @@ impl DavUriResource for Server { } } -impl UriResource { - pub fn account_id(&self) -> crate::Result { - self.account_id.ok_or(DavError::Code(StatusCode::FORBIDDEN)) +impl<'x> UnresolvedUri<'x> { + pub fn into_owned_uri(self) -> crate::Result> { + Ok(OwnedUri { + collection: self.collection, + account_id: self + .account_id + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?, + resource: self.resource, + }) + } +} + +impl OwnedUri<'_> { + pub fn new_owned( + collection: Collection, + account_id: u32, + resource: Option<&str>, + ) -> OwnedUri<'_> { + OwnedUri { + collection, + account_id, + resource, + } } } diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 45e75f95..35c41a01 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -16,7 +16,7 @@ use jmap_proto::types::{ }; use store::{ ahash::AHashMap, - write::{log::ChangeLogBuilder, now, AlignedBytes, Archive, BatchBuilder}, + write::{AlignedBytes, Archive, BatchBuilder, log::ChangeLogBuilder, now}, }; use trc::AddContext; use utils::map::bitmap::Bitmap; @@ -50,8 +50,11 @@ impl FileCopyMoveRequestHandler for Server { is_move: bool, ) -> crate::Result { // Validate source - let from_resource_ = self.validate_uri(access_token, headers.uri).await?; - let from_account_id = from_resource_.account_id()?; + let from_resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let from_account_id = from_resource_.account_id; let from_files = self .fetch_file_hierarchy(from_account_id) .await @@ -299,11 +302,11 @@ async fn move_container( access_token: &AccessToken, from_files: Arc, to_files: Arc, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, depth: Depth, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); @@ -360,7 +363,7 @@ async fn copy_container( server: &Server, access_token: &AccessToken, from_files: Arc, - from_resource: UriResource, + from_resource: UriResource, mut destination: Destination, depth: Depth, delete_source: bool, @@ -373,7 +376,7 @@ async fn copy_container( _ => true, }; - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); @@ -517,10 +520,10 @@ async fn copy_container( async fn overwrite_and_delete_item( server: &Server, access_token: &AccessToken, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let to_document_id = destination.document_id.unwrap(); @@ -589,10 +592,10 @@ async fn overwrite_and_delete_item( async fn overwrite_item( server: &Server, access_token: &AccessToken, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let to_document_id = destination.document_id.unwrap(); @@ -650,10 +653,10 @@ async fn overwrite_item( async fn move_item( server: &Server, access_token: &AccessToken, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); @@ -713,10 +716,10 @@ async fn move_item( async fn copy_item( server: &Server, access_token: &AccessToken, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let to_account_id = destination.account_id; let from_document_id = from_resource.resource.document_id; let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0); @@ -748,10 +751,10 @@ async fn copy_item( async fn rename_item( server: &Server, access_token: &AccessToken, - from_resource: UriResource, + from_resource: UriResource, destination: Destination, ) -> crate::Result { - let from_account_id = from_resource.account_id.unwrap(); + let from_account_id = from_resource.account_id; let from_document_id = from_resource.resource.document_id; let node = server diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index 1c3d9125..f5d7aa06 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -12,7 +12,7 @@ use hyper::StatusCode; use jmap_proto::types::{ acl::Acl, collection::Collection, property::Property, type_state::DataType, }; -use store::write::{log::ChangeLogBuilder, AlignedBytes, Archive, BatchBuilder}; +use store::write::{AlignedBytes, Archive, BatchBuilder, log::ChangeLogBuilder}; use trc::AddContext; use utils::map::bitmap::Bitmap; @@ -40,8 +40,11 @@ impl FileDeleteRequestHandler for Server { headers: RequestHeaders<'_>, ) -> crate::Result { // Validate URI - let resource = self.validate_uri(access_token, headers.uri).await?; - let account_id = resource.account_id()?; + let resource = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource.account_id; let delete_path = resource .resource .filter(|r| !r.is_empty()) diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index 53315e02..3ba0f13c 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -40,8 +40,11 @@ impl FileGetRequestHandler for Server { is_head: bool, ) -> crate::Result { // Validate URI - let resource_ = self.validate_uri(access_token, headers.uri).await?; - let account_id = resource_.account_id()?; + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource_.account_id; let files = self .fetch_file_hierarchy(account_id) .await diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 3e2fd9e9..77f93fcf 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -45,8 +45,11 @@ impl FileMkColRequestHandler for Server { request: Option, ) -> crate::Result { // Validate URI - let resource_ = self.validate_uri(access_token, headers.uri).await?; - let account_id = resource_.account_id()?; + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource_.account_id; let files = self .fetch_file_hierarchy(account_id) .await diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 74f0d51e..903f9384 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -11,12 +11,17 @@ use groupware::file::FileNode; use hyper::StatusCode; use jmap_proto::types::{collection::Collection, type_state::DataType}; use store::write::{ - log::{Changes, LogInsert}, now, Archive, BatchBuilder + Archive, BatchBuilder, + log::{Changes, LogInsert}, + now, }; use crate::{ DavError, - common::{ExtractETag, uri::UriResource}, + common::{ + ExtractETag, + uri::{OwnedUri, UriResource}, + }, }; pub mod acl; @@ -42,25 +47,26 @@ pub(crate) struct FileItemId { pub(crate) trait DavFileResource { fn map_resource( &self, - resource: &UriResource>, - ) -> crate::Result>; + resource: &OwnedUri<'_>, + ) -> crate::Result>; fn map_parent<'x, T: FromFileItem>( &self, resource: &'x str, ) -> Option<(Option, Cow<'x, str>)>; + #[allow(clippy::type_complexity)] fn map_parent_resource<'x, T: FromFileItem>( &self, - resource: &UriResource>, - ) -> crate::Result, Cow<'x, str>)>>; + resource: &OwnedUri<'x>, + ) -> crate::Result, Cow<'x, str>)>>; } impl DavFileResource for Files { fn map_resource( &self, - resource: &UriResource>, - ) -> crate::Result> { + resource: &OwnedUri<'_>, + ) -> crate::Result> { resource .resource .and_then(|r| self.files.by_name(r)) @@ -95,8 +101,8 @@ impl DavFileResource for Files { fn map_parent_resource<'x, T: FromFileItem>( &self, - resource: &UriResource>, - ) -> crate::Result, Cow<'x, str>)>> { + resource: &OwnedUri<'x>, + ) -> crate::Result, Cow<'x, str>)>> { if let Some(r) = resource.resource { if self.files.by_name(r).is_none() { self.map_parent(r) diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 9261f18d..73dff593 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -4,26 +4,279 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ - use common::{Server, auth::AccessToken}; -use dav_proto::{RequestHeaders, schema::request::PropFind}; +use common::{Server, auth::AccessToken}; +use dav_proto::schema::{ + property::{DavProperty, WebDavProperty}, + request::{DavPropertyValue, PropFind}, + response::{MultiStatus, PropStat, Response}, +}; +use groupware::file::{FileNode, hierarchy::FileHierarchy}; use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; +use store::{ + ahash::AHashMap, + dispatch::DocumentSet, + query::log::Query, + roaring::RoaringBitmap, + write::{AlignedBytes, Archive}, +}; +use trc::AddContext; +use utils::map::bitmap::Bitmap; -pub(crate) trait FilePropFindRequestHandler: Sync + Send { +use crate::{ + common::DavQuery, + principal::propfind::{PrincipalPropFind, PrincipalResource}, +}; + +pub(crate) trait HandleFilePropFindRequest: Sync + Send { fn handle_file_propfind_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: PropFind, + query: DavQuery<'_>, ) -> impl Future> + Send; } -impl FilePropFindRequestHandler for Server { +impl HandleFilePropFindRequest for Server { async fn handle_file_propfind_request( &self, access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: PropFind, + query: DavQuery<'_>, ) -> crate::Result { - todo!() + let account_id = query.resource.account_id; + let files = self + .fetch_file_hierarchy(account_id) + .await + .caused_by(trc::location!())?; + + // Obtain document ids + let mut document_ids = if !access_token.is_member(account_id) { + let todo = "query children acls"; + self.shared_containers( + access_token, + account_id, + Collection::FileNode, + Bitmap::::from_iter([Acl::ReadItems, Acl::Read]), + ) + .await + .caused_by(trc::location!())? + .into() + } else { + None + }; + + // Filter by changelog + let mut last_change_id = None; + if let Some(change_id) = query.from_change_id { + let changelog = self + .store() + .changes(account_id, Collection::FileNode, Query::Since(change_id)) + .await + .caused_by(trc::location!())?; + let limit = std::cmp::min( + query.limit.unwrap_or(u32::MAX) as usize, + self.core.dav.max_changes, + ); + if changelog.to_change_id != 0 { + last_change_id = Some(changelog.to_change_id); + } + let mut changes = + RoaringBitmap::from_iter(changelog.changes.iter().map(|change| change.id() as u32)); + if changes.len() as usize > limit { + changes = RoaringBitmap::from_sorted_iter(changes.into_iter().take(limit)).unwrap(); + } + if let Some(document_ids) = &mut document_ids { + *document_ids &= changes; + } else { + document_ids = Some(changes); + } + } + + let mut response = MultiStatus::new(Vec::with_capacity(16)); + let mut paths = if let Some(resource) = query.resource.resource { + files + .subtree_with_depth(resource, query.depth) + .filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + }) + .map(|item| { + ( + item.document_id, + (query.format_to_base_uri(&item.name), item.is_container), + ) + }) + .collect::>() + } else { + if !query.depth_no_root || query.from_change_id.is_none() { + self.prepare_principal_propfind_response( + access_token, + PrincipalResource::Id(account_id), + &query.propfind, + &mut response, + ) + .await?; + + if query.depth == 0 { + return Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body(response.to_string())); + } + } + files + .tree_with_depth(query.depth - 1) + .filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + }) + .map(|item| { + ( + item.document_id, + (query.format_to_base_uri(&item.name), item.is_container), + ) + }) + .collect::>() + }; + + if paths.is_empty() && query.from_change_id.is_none() { + response.add_response(Response::new_status( + [query.format_to_base_uri(query.resource.resource.unwrap_or_default())], + StatusCode::NOT_FOUND, + )); + + return Ok( + HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) + ); + } + + let todo = "prefer minimal"; + + // Prepare response + let (fields, is_all_prop) = match query.propfind { + PropFind::PropName => { + for (_, (path, is_container)) in paths { + response.add_response(Response::new_propstat( + path, + vec![PropStat::new_list(all_properties(is_container))], + )); + } + + return Ok( + HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()) + ); + } + PropFind::AllProp(items) => ( + items + .into_iter() + .filter(|v| matches!(v, DavProperty::DeadProperty(_))) + .map(DavPropertyValue::empty) + .collect::>(), + true, + ), + PropFind::Prop(items) => ( + items + .into_iter() + .map(DavPropertyValue::empty) + .collect::>(), + false, + ), + }; + + for (document_id, node_) in self + .get_properties::, _>( + account_id, + Collection::FileNode, + &Paths(&paths), + Property::Value, + ) + .await + .caused_by(trc::location!())? + { + let node = node_.unarchive::().caused_by(trc::location!())?; + let (node_path, _) = paths.remove(&document_id).unwrap(); + let is_container = node.file.is_none(); + let mut fields = if is_all_prop { + let mut all_fields = all_properties(is_container); + if !fields.is_empty() { + all_fields.extend(fields.iter().cloned()); + } + all_fields + } else { + fields.clone() + }; + + // Fill properties + for fields in &mut fields {} + + // Add response + response.add_response(Response::new_propstat( + node_path, + vec![PropStat::new_list(fields)], + )); + } + + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) + } +} + +fn all_properties(is_container: bool) -> Vec { + let mut props = vec![ + DavPropertyValue::empty(WebDavProperty::CreationDate), + DavPropertyValue::empty(WebDavProperty::DisplayName), + DavPropertyValue::empty(WebDavProperty::GetETag), + DavPropertyValue::empty(WebDavProperty::GetLastModified), + DavPropertyValue::empty(WebDavProperty::ResourceType), + DavPropertyValue::empty(WebDavProperty::LockDiscovery), + DavPropertyValue::empty(WebDavProperty::SupportedLock), + DavPropertyValue::empty(WebDavProperty::CurrentUserPrincipal), + DavPropertyValue::empty(WebDavProperty::SyncToken), + DavPropertyValue::empty(WebDavProperty::Owner), + DavPropertyValue::empty(WebDavProperty::SupportedPrivilegeSet), + DavPropertyValue::empty(WebDavProperty::CurrentUserPrivilegeSet), + DavPropertyValue::empty(WebDavProperty::Acl), + DavPropertyValue::empty(WebDavProperty::AclRestrictions), + DavPropertyValue::empty(WebDavProperty::InheritedAclSet), + DavPropertyValue::empty(WebDavProperty::PrincipalCollectionSet), + ]; + + if is_container { + props.extend([ + DavPropertyValue::empty(WebDavProperty::SupportedReportSet), + DavPropertyValue::empty(WebDavProperty::QuotaAvailableBytes), + DavPropertyValue::empty(WebDavProperty::QuotaUsedBytes), + ]); + } else { + props.extend([ + DavPropertyValue::empty(WebDavProperty::GetContentLanguage), + DavPropertyValue::empty(WebDavProperty::GetContentLength), + DavPropertyValue::empty(WebDavProperty::GetContentType), + ]); + } + + props +} + +struct Paths<'x>(&'x AHashMap); + +impl DocumentSet for Paths<'_> { + fn min(&self) -> u32 { + unimplemented!() + } + + fn max(&self) -> u32 { + unimplemented!() + } + + fn contains(&self, id: u32) -> bool { + self.0.contains_key(&id) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn iterate(&self) -> impl Iterator { + self.0.keys().copied() } } diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index 166cfd9b..faf53af9 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -57,9 +57,12 @@ impl FilePropPatchRequestHandler for Server { request: PropertyUpdate, ) -> crate::Result { // Validate URI - let resource_ = self.validate_uri(access_token, headers.uri).await?; + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; let uri = headers.uri; - let account_id = resource_.account_id()?; + let account_id = resource_.account_id; let files = self .fetch_file_hierarchy(account_id) .await diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index 9904e00d..cce7e5e4 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -52,8 +52,11 @@ impl FileUpdateRequestHandler for Server { _is_patch: bool, ) -> crate::Result { // Validate URI - let resource = self.validate_uri(access_token, headers.uri).await?; - let account_id = resource.account_id()?; + let resource = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource.account_id; let files = self .fetch_file_hierarchy(account_id) .await diff --git a/crates/dav/src/principal/mod.rs b/crates/dav/src/principal/mod.rs index c8a832f8..ba163334 100644 --- a/crates/dav/src/principal/mod.rs +++ b/crates/dav/src/principal/mod.rs @@ -3,3 +3,5 @@ * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ + +pub mod propfind; diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs new file mode 100644 index 00000000..3402492b --- /dev/null +++ b/crates/dav/src/principal/propfind.rs @@ -0,0 +1,41 @@ +/* + * 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, response::MultiStatus}, +}; +use http_proto::HttpResponse; +use store::roaring::RoaringBitmap; + +pub(crate) enum PrincipalResource<'x> { + Id(u32), + Uri(&'x str), + Ids(RoaringBitmap), +} + +pub(crate) trait PrincipalPropFind: Sync + Send { + fn prepare_principal_propfind_response( + &self, + access_token: &AccessToken, + resource: PrincipalResource<'_>, + request: &PropFind, + response: &mut MultiStatus, + ) -> impl Future> + Send; +} + +impl PrincipalPropFind for Server { + async fn prepare_principal_propfind_response( + &self, + access_token: &AccessToken, + resource: PrincipalResource<'_>, + request: &PropFind, + response: &mut MultiStatus, + ) -> crate::Result { + todo!() + } +} diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index 1d3eeffc..ec98073e 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -22,13 +22,12 @@ use hyper::{StatusCode, header}; use crate::{ DavError, DavMethod, DavResource, - common::lock::LockRequestHandler, + common::{lock::LockRequestHandler, propfind::PropFindRequestHandler}, file::{ acl::FileAclRequestHandler, changes::FileChangesRequestHandler, copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler, get::FileGetRequestHandler, mkcol::FileMkColRequestHandler, - propfind::FilePropFindRequestHandler, proppatch::FilePropPatchRequestHandler, - update::FileUpdateRequestHandler, + proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler, }, }; @@ -73,19 +72,14 @@ impl DavRequestDispatcher for Server { let todo = "lock tokens, headers, etc"; match method { - DavMethod::PROPFIND => match resource { - DavResource::Card => todo!(), - DavResource::Cal => todo!(), - DavResource::Principal => todo!(), - DavResource::File => { - self.handle_file_propfind_request( - &access_token, - headers, - PropFind::parse(&mut Tokenizer::new(&body))?, - ) - .await - } - }, + DavMethod::PROPFIND => { + self.handle_propfind_request( + &access_token, + headers, + PropFind::parse(&mut Tokenizer::new(&body))?, + ) + .await + } DavMethod::PROPPATCH => match resource { DavResource::Card => todo!(), DavResource::Cal => todo!(), diff --git a/crates/email/src/identity/mod.rs b/crates/email/src/identity/mod.rs index 4e6bf0c7..31dbb73e 100644 --- a/crates/email/src/identity/mod.rs +++ b/crates/email/src/identity/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_03_V1}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, @@ -26,6 +26,6 @@ pub struct EmailAddress { impl SerializedVersion for Identity { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_03_V1 } } diff --git a/crates/email/src/mailbox/mod.rs b/crates/email/src/mailbox/mod.rs index 0930c0a9..d0e92af6 100644 --- a/crates/email/src/mailbox/mod.rs +++ b/crates/email/src/mailbox/mod.rs @@ -6,7 +6,7 @@ use common::config::jmap::settings::SpecialUse; use jmap_proto::types::value::AclGrant; -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_04_V1}; pub mod destroy; pub mod index; @@ -41,7 +41,7 @@ pub struct UidMailbox { impl SerializedVersion for Mailbox { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_04_V1 } } diff --git a/crates/email/src/message/crypto.rs b/crates/email/src/message/crypto.rs index 1bcf10b7..54aef58e 100644 --- a/crates/email/src/message/crypto.rs +++ b/crates/email/src/message/crypto.rs @@ -26,7 +26,7 @@ use rasn_cms::{ }; use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey}; use sequoia_openpgp as openpgp; -use store::{Deserialize, SerializedVersion, write::Archive}; +use store::{write::Archive, Deserialize, SerializedVersion, SERIALIZE_OBJ_05_V1}; const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new(); @@ -86,7 +86,7 @@ pub struct EncryptionParams { impl SerializedVersion for EncryptionParams { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_05_V1 } } diff --git a/crates/email/src/message/delete.rs b/crates/email/src/message/delete.rs index 6ec30e44..364e6dd3 100644 --- a/crates/email/src/message/delete.rs +++ b/crates/email/src/message/delete.rs @@ -83,6 +83,7 @@ impl EmailDeletion for Server { .caused_by(trc::location!())?; let thread_id = u32::from(data.inner.thread_id); + // Log mailbox changes for mailbox in data.inner.mailboxes.iter() { changes.log_child_update(Collection::Mailbox, u32::from(mailbox.mailbox_id)); } diff --git a/crates/email/src/message/metadata.rs b/crates/email/src/message/metadata.rs index 18b317bc..b2af3f8f 100644 --- a/crates/email/src/message/metadata.rs +++ b/crates/email/src/message/metadata.rs @@ -20,7 +20,7 @@ use rkyv::{ string::ArchivedString, vec::ArchivedVec, }; -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_06_V1, SERIALIZE_OBJ_07_V1}; use utils::BlobHash; use crate::mailbox::{ArchivedUidMailbox, UidMailbox}; @@ -48,13 +48,13 @@ impl IndexableAndSerializableObject for MessageData {} impl SerializedVersion for MessageData { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_06_V1 } } impl SerializedVersion for MessageMetadata { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_07_V1 } } diff --git a/crates/email/src/push/mod.rs b/crates/email/src/push/mod.rs index 4b06fb2b..32b9d49f 100644 --- a/crates/email/src/push/mod.rs +++ b/crates/email/src/push/mod.rs @@ -5,7 +5,7 @@ */ use jmap_proto::types::type_state::DataType; -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_08_V1}; use utils::map::bitmap::Bitmap; #[derive( @@ -29,6 +29,6 @@ pub struct Keys { impl SerializedVersion for PushSubscription { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_08_V1 } } diff --git a/crates/email/src/sieve/mod.rs b/crates/email/src/sieve/mod.rs index 1455f0a2..d6a32d70 100644 --- a/crates/email/src/sieve/mod.rs +++ b/crates/email/src/sieve/mod.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use common::KV_SIEVE_ID; use sieve::Sieve; -use store::{SerializedVersion, blake3}; +use store::{blake3, SerializedVersion, SERIALIZE_OBJ_09_V1}; use utils::BlobHash; pub mod activate; @@ -38,7 +38,7 @@ pub struct SieveScript { impl SerializedVersion for SieveScript { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_09_V1 } } diff --git a/crates/email/src/submission/mod.rs b/crates/email/src/submission/mod.rs index a0f1599c..61b8d28a 100644 --- a/crates/email/src/submission/mod.rs +++ b/crates/email/src/submission/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_10_V1}; use utils::map::vec_map::VecMap; pub mod index; @@ -25,7 +25,7 @@ pub struct EmailSubmission { impl SerializedVersion for EmailSubmission { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_10_V1 } } diff --git a/crates/email/src/thread/cache.rs b/crates/email/src/thread/cache.rs index 2baad407..7f5c4b64 100644 --- a/crates/email/src/thread/cache.rs +++ b/crates/email/src/thread/cache.rs @@ -97,18 +97,3 @@ impl ThreadCache for Server { } } } - -/* - - - // Obtain threadIds for matching messages - let mut thread_ids = Vec::with_capacity(message_ids.size_hint().0); - for document_id in message_ids { - if let Some(thread_id) = thread_cache.threads.get(&document_id) { - thread_ids.push((document_id, *thread_id)); - } - } - - Ok(thread_ids) - -*/ diff --git a/crates/groupware/src/file/mod.rs b/crates/groupware/src/file/mod.rs index a56ab92a..aa35dbe5 100644 --- a/crates/groupware/src/file/mod.rs +++ b/crates/groupware/src/file/mod.rs @@ -9,7 +9,7 @@ pub mod index; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::value::AclGrant; -use store::SerializedVersion; +use store::{SerializedVersion, SERIALIZE_OBJ_11_V1}; use utils::BlobHash; #[derive( @@ -40,6 +40,6 @@ pub struct FileProperties { impl SerializedVersion for FileNode { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_11_V1 } } diff --git a/crates/http/src/auth/oauth/mod.rs b/crates/http/src/auth/oauth/mod.rs index c439cb03..51f023a0 100644 --- a/crates/http/src/auth/oauth/mod.rs +++ b/crates/http/src/auth/oauth/mod.rs @@ -7,7 +7,7 @@ use http_proto::{HttpRequest, request::fetch_body}; use hyper::header::CONTENT_TYPE; use serde::{Deserialize, Serialize}; -use store::SerializedVersion; +use store::{SERIALIZE_OBJ_12_V1, SerializedVersion}; use utils::map::vec_map::VecMap; pub mod auth; @@ -58,7 +58,7 @@ pub struct OAuthCode { impl SerializedVersion for OAuthCode { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_12_V1 } } diff --git a/crates/jmap-proto/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs index 79a9bfa3..5cd20794 100644 --- a/crates/jmap-proto/src/types/keyword.rs +++ b/crates/jmap-proto/src/types/keyword.rs @@ -68,12 +68,6 @@ pub enum Keyword { Other(String), } -impl SerializedVersion for Keyword { - fn serialize_version() -> u8 { - 0 - } -} - impl JsonObjectParser for Keyword { fn parse(parser: &mut Parser<'_>) -> trc::Result where diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 3b5f9957..5e7f61d2 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -39,14 +39,14 @@ impl PrincipalGet for Server { //Property::Timezone, //Property::Capabilities, ]); - let email_submission_ids = self - .get_document_ids(u32::MAX, Collection::EmailSubmission) + let principal_ids = self + .get_document_ids(u32::MAX, Collection::Principal) .await? .unwrap_or_default(); let ids = if let Some(ids) = ids { ids } else { - email_submission_ids + principal_ids .iter() .take(self.core.jmap.get_max_objects) .map(Into::into) diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index a0fad44e..1943e6b6 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -12,7 +12,7 @@ use std::{ use common::expr::{self, functions::ResolveVariable, *}; use smtp_proto::{ArchivedResponse, Response}; -use store::{SerializedVersion, write::now}; +use store::{SERIALIZE_OBJ_13_V1, SerializedVersion, write::now}; use utils::BlobHash; pub mod dsn; @@ -69,7 +69,7 @@ pub struct Message { impl SerializedVersion for Message { fn serialize_version() -> u8 { - 0 + SERIALIZE_OBJ_13_V1 } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 9efaf9c8..09932aca 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -68,6 +68,24 @@ pub trait SerializeInfallible { fn serialize(&self) -> Vec; } +// Max 64 versions (2 ^ 6) +pub const SERIALIZE_OBJ_01_V1: u8 = 0; +pub const SERIALIZE_OBJ_02_V1: u8 = 1; +pub const SERIALIZE_OBJ_03_V1: u8 = 2; +pub const SERIALIZE_OBJ_04_V1: u8 = 3; +pub const SERIALIZE_OBJ_05_V1: u8 = 4; +pub const SERIALIZE_OBJ_06_V1: u8 = 5; +pub const SERIALIZE_OBJ_07_V1: u8 = 6; +pub const SERIALIZE_OBJ_08_V1: u8 = 7; +pub const SERIALIZE_OBJ_09_V1: u8 = 8; +pub const SERIALIZE_OBJ_10_V1: u8 = 9; +pub const SERIALIZE_OBJ_11_V1: u8 = 10; +pub const SERIALIZE_OBJ_12_V1: u8 = 11; +pub const SERIALIZE_OBJ_13_V1: u8 = 12; +pub const SERIALIZE_OBJ_14_V1: u8 = 13; +pub const SERIALIZE_OBJ_15_V1: u8 = 14; +pub const SERIALIZE_OBJ_16_V1: u8 = 15; + pub trait SerializedVersion { fn serialize_version() -> u8; } @@ -796,6 +814,6 @@ impl Stores { impl SerializedVersion for () { fn serialize_version() -> u8 { - 0 + unreachable!() } } diff --git a/crates/store/src/write/serialize.rs b/crates/store/src/write/serialize.rs index fcb0f4ec..007914e3 100644 --- a/crates/store/src/write/serialize.rs +++ b/crates/store/src/write/serialize.rs @@ -112,7 +112,7 @@ impl Deserialize for Archive { }), _ => Err(trc::StoreEvent::DataCorruption .into_err() - .details("Invalid archive marker.") + .details("Invalid archive marker") .ctx(trc::Key::Value, bytes) .caused_by(trc::location!())), } @@ -209,18 +209,56 @@ impl Archive { rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>, > + rkyv::Deserialize>, { - if self.version == T::serialize_version() { + let bytes = self.as_bytes(); + if self.version == T::serialize_version() + && bytes.len() >= std::mem::size_of::() + { // SAFETY: Trusted and versioned input with integrity hash - Ok(unsafe { rkyv::access_unchecked::(self.as_bytes()) }) + Ok(unsafe { rkyv::access_unchecked::(bytes) }) } else { Err(trc::StoreEvent::DataCorruption .into_err() .details(format!( - "Archive version mismatch, expected {} but got {}", + "Archive version mismatch, expected {} ({} bytes) but got {} ({} bytes)", T::serialize_version(), - self.version + std::mem::size_of::(), + self.version, + bytes.len() )) - .ctx(trc::Key::Value, self.as_bytes()) + .ctx(trc::Key::Value, bytes) + .caused_by(trc::location!())) + } + } + + pub fn unarchive_untrusted(&self) -> trc::Result<&::Archived> + where + T: rkyv::Archive + SerializedVersion, + T::Archived: for<'a> rkyv::bytecheck::CheckBytes< + rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>, + > + rkyv::Deserialize>, + { + let bytes = self.as_bytes(); + if self.version == T::serialize_version() + && bytes.len() >= std::mem::size_of::() + { + rkyv::access::(bytes).map_err(|err| { + trc::StoreEvent::DeserializeError + .ctx(trc::Key::Value, self.as_bytes()) + .details("Archive access failed") + .caused_by(trc::location!()) + .reason(err) + }) + } else { + Err(trc::StoreEvent::DataCorruption + .into_err() + .details(format!( + "Archive version mismatch, expected {} ({} bytes) but got {} ({} bytes)", + T::serialize_version(), + std::mem::size_of::(), + self.version, + bytes.len() + )) + .ctx(trc::Key::Value, bytes) .caused_by(trc::location!())) } }